diff --git a/API_REFERENCE.md b/API_REFERENCE.md index 7dcde77..dda686b 100644 --- a/API_REFERENCE.md +++ b/API_REFERENCE.md @@ -1,6 +1,6 @@ # vCon Library API Reference -Complete API documentation for the vCon library - a Python implementation of the vCon 0.3.0 specification for Virtual Conversation objects. +Complete API documentation for the vCon library - a Python implementation of the latest vCon specification for Virtual Conversation objects. ## Table of Contents @@ -18,7 +18,7 @@ Complete API documentation for the vCon library - a Python implementation of the ## Overview -The vCon library provides a complete Python implementation of the vCon 0.3.0 specification for representing virtual conversations. It supports all features including parties, dialogs, attachments, analysis, digital signatures, and extensibility. +The vCon library provides a complete Python implementation of the latest vCon specification for representing virtual conversations. It supports all features including parties, dialogs, attachments, analysis, digital signatures, and extensibility. ## Installation @@ -40,7 +40,7 @@ The main class for working with vCon objects. #### Constructor ```python -Vcon(vcon_dict: Dict[str, Any] = None, property_handling: str = "default", strict_version: bool = False) +Vcon(vcon_dict: Dict[str, Any] = None, property_handling: str = "default") ``` **Parameters:** @@ -49,7 +49,6 @@ Vcon(vcon_dict: Dict[str, Any] = None, property_handling: str = "default", stric - `"default"`: Keep non-standard properties (default) - `"strict"`: Remove non-standard properties - `"meta"`: Move non-standard properties to meta object -- `strict_version` (bool): If True, reject vCons not at version "0.3.0". Defaults to False. #### Class Methods @@ -60,14 +59,15 @@ Create a new vCon object with default values. vcon = Vcon.build_new() ``` -##### `build_from_json(json_str: str, property_handling: str = "default", strict_version: bool = False) -> Vcon` +##### `build_from_json(json_str: str, property_handling: str = "default") -> Vcon` Create a vCon object from JSON string. ```python -vcon = Vcon.build_from_json('{"uuid": "123", "vcon": "0.3.0"}') +vcon = Vcon.build_from_json('{"uuid": "123", "created_at": "2024-01-01T00:00:00Z"}') ``` -##### `load(file_path_or_url: str, property_handling: str = "default", strict_version: bool = False) -> Vcon` +##### `load(file_path_or_url: str, property_handling: str = "default") -> Vcon` + Load a vCon from file or URL. ```python @@ -78,10 +78,11 @@ vcon = Vcon.load("conversation.vcon.json") vcon = Vcon.load("https://example.com/conversation.vcon.json") ``` -##### `load_from_file(file_path: str, property_handling: str = "default", strict_version: bool = False) -> Vcon` +##### `load_from_file(file_path: str, property_handling: str = "default") -> Vcon` Load a vCon from a local file. -##### `load_from_url(url: str, property_handling: str = "default", strict_version: bool = False) -> Vcon` +##### `load_from_url(url: str, property_handling: str = "default") -> Vcon` + Load a vCon from a URL. ##### `validate_file(file_path: str) -> Tuple[bool, List[str]]` @@ -343,8 +344,8 @@ Set the update timestamp. ##### `uuid -> str` Get the vCon UUID. -##### `vcon -> str` -Get the vCon version. +##### `vcon -> Optional[str]` +Get the vCon version (optional field). ##### `subject -> Optional[str]` Get the vCon subject. diff --git a/CHANGELOG.md b/CHANGELOG.md index 43e1d75..d95a974 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,75 @@ # Changelog +## [0.8.0] - 2025-01-26 + +### ๐ŸŽ‰ Major Release: Version Management Simplification + +This release aligns with the upcoming vCon draft specification by removing mandatory version management and enforcement, making the version field optional while maintaining full backward compatibility. + +### โœจ Added + +#### **Flexible Versioning** +- **Optional Version Field**: The `vcon` field is now optional in vCon objects +- **Version Preservation**: Existing vCons with version fields continue to work unchanged +- **Simplified Creation**: New vCons can be created without version fields + +### ๐Ÿ”„ Changed + +#### **Version Management Simplification** +- **Removed `strict_version` Parameter**: Eliminated from all Vcon methods (`__init__`, `build_from_json`, `build_new`, `load`, `load_from_file`, `load_from_url`) +- **No Automatic Version Assignment**: vCon objects no longer automatically get a version field +- **No Version Migration**: Removed automatic migration from older versions +- **Updated Validation**: The `is_valid()` method no longer requires the version field + +#### **Method Signatures Updated** +- `Vcon(vcon_dict=None, property_handling="default")` - removed `strict_version` parameter +- `build_from_json(json_str, property_handling="default")` - removed `strict_version` parameter +- `build_new(created_at=None, property_handling="default")` - removed `strict_version` parameter +- `load(source, property_handling="default")` - removed `strict_version` parameter +- `load_from_file(file_path, property_handling="default")` - removed `strict_version` parameter +- `load_from_url(url, property_handling="default")` - removed `strict_version` parameter + +### ๐Ÿงช Testing + +#### **Updated Test Suite** +- **Replaced Version Migration Tests**: Updated tests to cover optional version field behavior +- **New Test Cases**: Added comprehensive tests for versionless vCon creation and preservation +- **Backward Compatibility Tests**: Ensured existing vCons with version fields continue to work + +### ๐Ÿ“„ Documentation + +#### **Updated Documentation** +- **README.md**: Updated to reflect version field being optional +- **API_REFERENCE.md**: Removed `strict_version` parameter from all method signatures +- **GUIDE.md**: Updated examples to show versionless vCon creation +- **MIGRATION_GUIDE.md**: Added migration steps for version management changes + +#### **Updated Sample Files** +- **Removed Version Fields**: All sample vCon JSON files updated to demonstrate versionless vCons +- **Backward Compatibility**: Existing vCons with version fields continue to work + +### ๐Ÿ”ง Technical Details + +#### **Implementation Changes** +- **Removed Version Logic**: Eliminated version checking, migration, and enforcement code +- **Simplified Initialization**: Streamlined vCon object creation process +- **Preserved Functionality**: All other features remain unchanged + +#### **Backward Compatibility** +- **Existing vCons**: Continue to work without any changes +- **Version Fields**: Preserved when present, not added when absent +- **API Compatibility**: All other methods and properties remain unchanged + +### ๐ŸŽฏ Benefits + +1. **Enhanced Flexibility**: vCon objects can now exist without mandatory versioning +2. **Simplified Implementation**: Reduced complexity in version management +3. **Better Privacy Support**: Enables creation of redacted versions without version conflicts +4. **Multiple Version Support**: Allows different versions of the same vCon to coexist +5. **Specification Compliance**: Aligns with upcoming vCon draft specification + +--- + ## [0.7.0] - 2025-07-19 ### ๐ŸŽ‰ Major Release: Complete vCon 0.3.0 Specification Compliance diff --git a/GUIDE.md b/GUIDE.md index 81fab97..13b06ec 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -24,7 +24,7 @@ from vcon import Vcon #### Properties - `uuid`: Unique identifier -- `vcon`: Version number +- `vcon`: Version number (optional) - `created_at`: Creation timestamp - `updated_at`: Last update timestamp - `parties`: List of participants @@ -84,7 +84,7 @@ from vcon.dialog import Dialog vcon = Vcon.build_new() # Create from dictionary -vcon = Vcon({"uuid": "...", "vcon": "0.3.0"}) +vcon = Vcon({"uuid": "...", "created_at": "2024-01-01T00:00:00Z"}) # Create from JSON vcon = Vcon.build_from_json(json_string) @@ -276,6 +276,6 @@ if not is_valid: is_valid, errors = Vcon.validate_file("path/to/vcon.json") # Validate a vCon JSON string -json_str = '{"uuid": "123", "vcon": "0.3.0", ...}' +json_str = '{"uuid": "123", "created_at": "2024-01-01T00:00:00Z", ...}' is_valid, errors = Vcon.validate_json(json_str) ``` \ No newline at end of file diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index adc9f10..45919f6 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -1,10 +1,23 @@ -# Migration Guide: New Required Fields +# Migration Guide: Version Management Changes -This guide helps you migrate your existing vCon code to use the new required fields introduced in the latest version. +This guide helps you migrate your existing vCon code to work with the updated version management system. ## Overview -The vCon library now supports additional fields as specified in the IETF vCon specification. All new fields are **optional** and **backward compatible**, so existing code will continue to work without changes. +The vCon library has been updated to align with the latest vCon specification changes. The most significant change is that the **version field is now optional** and version management has been simplified. All changes are **backward compatible**, so existing code will continue to work without changes. + +## Key Changes + +### Version Field is Now Optional +- The `vcon` field is no longer required in vCon objects +- No automatic version assignment or migration +- Existing vCons with version fields continue to work unchanged +- New vCons can be created without version fields + +### Removed Version Management +- Removed `strict_version` parameter from all methods +- No more automatic version migration +- No more version enforcement or validation ## What's New @@ -24,7 +37,42 @@ The vCon library now supports additional fields as specified in the IETF vCon sp ## Migration Steps -### Step 1: Add Extensions (Optional) +### Step 1: Update Method Calls (Required if using strict_version) + +If you were using the `strict_version` parameter, you need to remove it: + +```python +# Old code (will cause errors) +vcon = Vcon.load("file.json", strict_version=True) +vcon = Vcon.build_from_json(json_str, strict_version=True) +vcon = Vcon(data, strict_version=True) + +# New code (remove strict_version parameter) +vcon = Vcon.load("file.json") +vcon = Vcon.build_from_json(json_str) +vcon = Vcon(data) +``` + +### Step 2: Version Field Handling (Optional) + +The version field is now optional. You can choose to: + +**Option A: Remove version fields from new vCons** +```python +# Old code +vcon = Vcon({"uuid": "123", "vcon": "0.3.0", "created_at": "2024-01-01T00:00:00Z"}) + +# New code (version field optional) +vcon = Vcon({"uuid": "123", "created_at": "2024-01-01T00:00:00Z"}) +``` + +**Option B: Keep existing version fields** +```python +# This still works - no changes needed +vcon = Vcon({"uuid": "123", "vcon": "0.3.0", "created_at": "2024-01-01T00:00:00Z"}) +``` + +### Step 3: Add Extensions (Optional) If you want to declare extension capabilities: diff --git a/README.md b/README.md index 7d58837..6f334b8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A Python library for working with vCon (Virtual Conversation) objects according ## Overview -The vCon library provides a complete implementation of the vCon format for representing conversations and related metadata. It supports all features defined in the vCon 0.3.0 specification including: +The vCon library provides a complete implementation of the vCon format for representing conversations and related metadata. It supports all features defined in the latest vCon specification including: - **Conversation Management**: Parties, dialogs, attachments, and analysis - **Contact Information**: Multiple contact methods (tel, email, SIP, DID) @@ -14,15 +14,15 @@ The vCon library provides a complete implementation of the vCon format for repre - **Location Data**: Civic address information (GEOPRIV) - **Event Tracking**: Party history with join/drop/hold/mute events -## New in vCon 0.3.0 +## Key Features -This library implements the latest vCon specification (0.3.0) with the following new features: +This library implements the latest vCon specification with the following features: ### Enhanced Party Information ```python from vcon import Vcon, Party -# Create a party with new vCon 0.3.0 fields +# Create a party with enhanced contact information party = Party( tel="+1234567890", name="John Doe", @@ -158,9 +158,6 @@ vcon = Vcon.load("conversation.vcon.json") # Load from URL vcon = Vcon.load("https://example.com/conversation.vcon.json") - -# Load with strict version checking -vcon = Vcon.load("conversation.vcon.json", strict_version=True) ``` ### Validation @@ -288,7 +285,7 @@ vcon.add_analysis( ## Specification Compliance -This library implements the vCon 0.3.0 specification with: +This library implements the latest vCon specification with: - โœ… All required fields and validation - โœ… Proper media type support @@ -297,6 +294,7 @@ This library implements the vCon 0.3.0 specification with: - โœ… Transfer dialog support - โœ… Content hashing and security - โœ… Extensions and must_support +- โœ… Flexible versioning (version field is optional) - โœ… Backward compatibility ## Testing @@ -307,12 +305,13 @@ Run the test suite: pytest tests/ ``` -All 149 tests pass, covering: +All tests pass, covering: - Basic functionality -- New vCon 0.3.0 features +- Enhanced vCon features - Validation and error handling - Media type support - Security features +- Flexible versioning - Backward compatibility ## License diff --git a/docs/index.html b/docs/index.html index 49a674a..ce09613 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2,6 +2,11 @@ - + + vCon Documentation - Redirecting... + +

Redirecting to the latest documentation...

+

If you are not redirected automatically, click here.

+ diff --git a/docs/source/conf.py b/docs/source/conf.py index 423dca2..050ec70 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,8 +15,8 @@ copyright = "2024, Thomas McCarthy-Howe" author = "Thomas McCarthy-Howe" -version = "0.3.9" -release = "0.3.9" +version = "0.7.0" +release = "0.7.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/source/index.rst b/docs/source/index.rst index 2c0d30d..dc93059 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,6 +15,7 @@ vcon is a Python library for working with vCon (Video Conference) containers, wh installation usage new_required_fields + version_management api/modules Installation diff --git a/docs/source/new_required_fields.rst b/docs/source/new_required_fields.rst index df9d317..237cc51 100644 --- a/docs/source/new_required_fields.rst +++ b/docs/source/new_required_fields.rst @@ -258,7 +258,6 @@ The resulting JSON will include: { "uuid": "...", - "vcon": "0.3.0", "extensions": ["video"], "must_support": ["encryption"], "parties": [{ diff --git a/docs/source/version_management.rst b/docs/source/version_management.rst new file mode 100644 index 0000000..cd8f46d --- /dev/null +++ b/docs/source/version_management.rst @@ -0,0 +1,179 @@ +Version Management Changes +========================== + +This document describes the changes to version management in the vCon library, including the removal of mandatory version enforcement and the optional nature of the version field. + +Overview +-------- + +Starting with version 0.7.0, the vCon library has simplified version management by making the version field optional and removing automatic version enforcement. This aligns with the latest vCon specification changes and provides more flexibility for users. + +Key Changes +----------- + +### Version Field is Now Optional + +The ``vcon`` field in vCon objects is no longer required: + +- New vCon objects created with ``Vcon.build_new()`` do not include a version field by default +- Existing vCon objects with version fields continue to work unchanged +- The version field can be manually added if needed + +### Removed Version Management Parameters + +The following parameters have been removed from all Vcon methods: + +- ``strict_version`` parameter removed from: + - ``Vcon.__init__()`` + - ``Vcon.build_from_json()`` + - ``Vcon.build_new()`` + - ``Vcon.load()`` + - ``Vcon.load_from_file()`` + - ``Vcon.load_from_url()`` + +### Updated Method Signatures + +All method signatures have been updated to remove version management: + +.. code-block:: python + + # Old signatures (no longer supported) + Vcon(vcon_dict=None, strict_version=True) + Vcon.build_new(created_at=None, strict_version=True) + Vcon.build_from_json(json_str, strict_version=True) + Vcon.load(source, strict_version=True) + + # New signatures + Vcon(vcon_dict=None, property_handling="default") + Vcon.build_new(created_at=None, property_handling="default") + Vcon.build_from_json(json_str, property_handling="default") + Vcon.load(source, property_handling="default") + +Migration Guide +--------------- + +### For Existing Code + +If you were using the ``strict_version`` parameter, simply remove it: + +.. code-block:: python + + # Old code (will cause errors) + vcon = Vcon.load("file.json", strict_version=True) + vcon = Vcon.build_from_json(json_str, strict_version=True) + vcon = Vcon(data, strict_version=True) + + # New code (remove strict_version parameter) + vcon = Vcon.load("file.json") + vcon = Vcon.build_from_json(json_str) + vcon = Vcon(data) + +### For Version Field Handling + +The version field is now optional. You can choose to: + +1. **Ignore version fields** (recommended for new code): + - Simply don't set or check version fields + - The library will work without them + +2. **Manually manage version fields** (if needed): + - Add version fields manually when creating vCon objects + - Check version fields in your application logic + +.. code-block:: python + + # Option 1: Ignore version fields (recommended) + vcon = Vcon.build_new() + # vcon.vcon will be None + + # Option 2: Manually add version field if needed + vcon = Vcon.build_new() + vcon.vcon_dict["vcon"] = "0.3.0" + # vcon.vcon will be "0.3.0" + +### Backward Compatibility + +All changes are backward compatible: + +- Existing vCon objects with version fields continue to work +- The ``vcon`` property returns the version field if present, or ``None`` if not +- No breaking changes to existing functionality + +Examples +-------- + +### Creating Versionless vCons + +.. code-block:: python + + from vcon import Vcon + + # Create a new vCon without version field + vcon = Vcon.build_new() + print(vcon.vcon) # None + + # Add version field manually if needed + vcon.vcon_dict["vcon"] = "0.3.0" + print(vcon.vcon) # "0.3.0" + +### Working with Existing vCons + +.. code-block:: python + + # Load existing vCon (with or without version field) + vcon = Vcon.load("existing.vcon.json") + + # Check if version field exists + if vcon.vcon: + print(f"Version: {vcon.vcon}") + else: + print("No version field") + +### Validation Changes + +The validation logic has been updated to reflect the optional nature of the version field: + +.. code-block:: python + + # Validation no longer requires version field + is_valid, errors = vcon.is_valid() + + # Only uuid and created_at are required fields + # Version field is optional and not validated + +Benefits +-------- + +The simplified version management provides several benefits: + +1. **Reduced Complexity**: No need to manage version parameters +2. **More Flexibility**: Version fields are optional and can be added as needed +3. **Better Interoperability**: Works with vCon objects from different sources +4. **Simplified API**: Cleaner method signatures without version management +5. **Future-Proof**: Aligns with evolving vCon specification + +Troubleshooting +-------------- + +### Common Issues + +1. **"strict_version" parameter errors**: + - Remove the ``strict_version`` parameter from all method calls + - Update method signatures to use ``property_handling`` instead + +2. **Version field not found**: + - Check if the vCon object has a version field: ``vcon.vcon is not None`` + - Add version field manually if needed: ``vcon.vcon_dict["vcon"] = "0.3.0"`` + +3. **Validation errors**: + - Ensure required fields (uuid, created_at) are present + - Version field is no longer required for validation + +### Getting Help + +If you encounter issues with the version management changes: + +1. Check the migration guide above +2. Review the changelog for detailed changes +3. Test with the updated method signatures +4. Ensure backward compatibility with existing vCon objects diff --git a/docs/vcon.html b/docs/vcon.html deleted file mode 100644 index 04e81c2..0000000 --- a/docs/vcon.html +++ /dev/null @@ -1,2103 +0,0 @@ - - - - - - - vcon API documentation - - - - - - - - - -
-
-

-vcon

- - - - - - -
1from .vcon import Vcon
-2
-3__all__ = ["Vcon"]
-
- - -
-
- -
- - class - Vcon: - - - -
- -
 24class Vcon:
- 25    def __init__(self, vcon_dict={}) -> None:
- 26        # deep copy
- 27        """
- 28        Initialize a Vcon object from a dictionary.
- 29
- 30        :param vcon_dict: a dictionary representing a vCon
- 31        :type vcon_dict: dict
- 32        """
- 33        self.vcon_dict = json.loads(json.dumps(vcon_dict))
- 34
- 35    @classmethod
- 36    def build_from_json(cls, json_string: str) -> Vcon:
- 37        """
- 38        Initialize a Vcon object from a JSON string.
- 39
- 40        :param json_string: a JSON string representing a vCon
- 41        :type json_string: str
- 42        :return: a Vcon object
- 43        :rtype: Vcon
- 44        """
- 45        return cls(json.loads(json_string))
- 46
- 47    @classmethod
- 48    def build_new(cls) -> Vcon:
- 49        """
- 50        Initialize a Vcon object with default values.
- 51
- 52        :return: a Vcon object
- 53        :rtype: Vcon
- 54        """
- 55        vcon_dict = {
- 56            "uuid": cls.uuid8_domain_name("strolid.com"),
- 57            "vcon": "0.0.1",
- 58            "created_at": datetime.now(timezone.utc).isoformat()[:-3] + "+00:00",
- 59            "redacted": {},
- 60            "group": [],
- 61            "parties": [],
- 62            "dialog": [],
- 63            "attachments": [],
- 64            "analysis": [],
- 65        }
- 66        return cls(vcon_dict)
- 67
- 68    @property
- 69    def tags(self) -> Optional[dict]:
- 70        """
- 71        Returns the tags attachment.
- 72        
- 73        :return: the tags attachment
- 74        :rtype: dict or None
- 75        """
- 76        return self.find_attachment_by_type("tags")
- 77
- 78    def get_tag(self, tag_name) -> Optional[dict]:
- 79        """
- 80        Returns the value of a tag by name.
- 81
- 82        :param tag_name: the name of the tag
- 83        :type tag_name: str
- 84        :return: the value of the tag or None if not found
- 85        :rtype: str or None
- 86        """
- 87        tags_attachment = self.find_attachment_by_type("tags")
- 88        if not tags_attachment:
- 89            return None
- 90        tag = next(
- 91            (t for t in tags_attachment["body"] if t.startswith(f"{tag_name}:")), None
- 92        )
- 93        if not tag:
- 94            return None
- 95        tag_value = tag.split(":")[1]
- 96        return tag_value
- 97
- 98    def add_tag(self, tag_name, tag_value) -> None:
- 99        """
-100        Adds a tag to the vCon.
-101
-102        :param tag_name: the name of the tag
-103        :type tag_name: str
-104        :param tag_value: the value of the tag
-105        :type tag_value: str
-106        :return: None
-107        :rtype: None
-108        """
-109        tags_attachment = self.find_attachment_by_type("tags")
-110        if not tags_attachment:
-111            tags_attachment = {
-112                "type": "tags",
-113                "body": [],
-114                "encoding": "json",
-115            }
-116            self.vcon_dict["attachments"].append(tags_attachment)
-117        tags_attachment["body"].append(f"{tag_name}:{tag_value}")
-118
-119    def find_attachment_by_type(self, type: str) -> Optional[dict]:
-120        """
-121        Finds an attachment by type.
-122
-123        :param type: the type of the attachment
-124        :type type: str
-125        :return: the attachment or None if not found
-126        :rtype: dict or None
-127        """
-128        return next(
-129            (a for a in self.vcon_dict["attachments"] if a["type"] == type), None
-130        )
-131
-132    def add_attachment(self, *, body: Union[dict, list, str], type: str, encoding="none") -> None:
-133        """
-134        Adds an attachment to the vCon.
-135
-136        :param body: the body of the attachment
-137        :type body: Union[dict, list, str]
-138        :param type: the type of the attachment
-139        :type type: str
-140        :param encoding: the encoding of the attachment body
-141        :type encoding: str
-142        :return: None
-143        :rtype: None
-144        """
-145        if encoding not in ['json', 'none', 'base64url']:
-146            raise Exception("Invalid encoding")
-147        
-148        if encoding == "json":
-149            try:
-150                json.loads(body)
-151            except Exception as e:
-152                raise Exception("Invalid JSON body: ", e)
-153            
-154        if encoding == 'base64url':
-155            try:
-156                base64.urlsafe_b64decode(body)
-157            except Exception as e:
-158                raise Exception("Invalid base64url body: ", e)
-159
-160        attachment = {
-161            "type": type,
-162            "body": body,
-163            "encoding": encoding,
-164        }
-165        self.vcon_dict["attachments"].append(attachment)
-166
-167    def find_analysis_by_type(self, type) -> Any | None:
-168        """
-169        Finds an analysis by type.
-170
-171        :param type: the type of the analysis
-172        :type type: str
-173        :return: the analysis or None if not found
-174        :rtype: dict or None
-175        """
-176        return next((a for a in self.vcon_dict["analysis"] if a["type"] == type), None)
-177
-178    def add_analysis(self, *, type: str, dialog: Union[list, int], vendor: str, body: Union[dict, list, str], encoding="none", extra={}) -> None:
-179        """
-180        Adds analysis data to the vCon.
-181
-182        :param type: the type of the analysis
-183        :type type: str
-184        :param dialog: the dialog(s) associated with the analysis
-185        :type dialog: Union[list, int]
-186        :param vendor: the vendor of the analysis
-187        :type vendor: str
-188        :param body: the body of the analysis
-189        :type body: Union[dict, list, str]
-190        :param encoding: the encoding of the body
-191        :type encoding: str
-192        :param extra: extra key-value pairs to include in the analysis
-193        :type extra: dict
-194        :return: None
-195        :rtype: None
-196        """
-197        if encoding not in ['json', 'none', 'base64url']:
-198            raise Exception("Invalid encoding")
-199        
-200        if encoding == "json":
-201            try:
-202                json.loads(body)
-203            except Exception as e:
-204                raise Exception("Invalid JSON body: ", e)
-205            
-206        if encoding == 'base64url':
-207            try:
-208                base64.urlsafe_b64decode(body)
-209            except Exception as e:
-210                raise Exception("Invalid base64url body: ", e)
-211
-212        analysis = {
-213            "type": type,
-214            "dialog": dialog,
-215            "vendor": vendor,
-216            "body": body,
-217            "encoding": encoding,
-218            **extra,
-219        }
-220        self.vcon_dict["analysis"].append(analysis)
-221
-222    def add_party(self, party: Party) -> None:
-223        """
-224        Adds a party to the vCon.
-225
-226        :param party: the party to add
-227        :type party: Party
-228        :return: None
-229        :rtype: None
-230        """
-231        self.vcon_dict["parties"].append(party.to_dict())
-232
-233    def find_party_index(self, by: str, val: str) -> Optional[int]:
-234        """
-235        Find the index of a party in the vCon given a key-value pair.
-236
-237        :param by: the key to look for
-238        :type by: str
-239        :param val: the value to look for
-240        :type val: str
-241        :return: The index of the party if found, None otherwise
-242        :rtype: Optional[int]
-243        """
-244        return next(
-245            (
-246                ind
-247                for ind, party in enumerate(self.vcon_dict["parties"])
-248                if _get(party, by) == val
-249            ),
-250            None,
-251        )
-252
-253
-254    def find_dialog(self, by: str, val: str) -> Optional[Dialog]:
-255        """
-256        Find a dialog in the vCon given a key-value pair. Convert the dialog to a Dialog object.
-257
-258        :param by: the key to look for
-259        :type by: str
-260        :param val: the value to look for
-261        :type val: str
-262        :return: The dialog if found, None otherwise
-263        :rtype: Optional[dict]
-264        """
-265        dialog = next(
-266            (
-267                dialog
-268                for dialog in self.vcon_dict["dialog"]
-269                if _get(dialog, by) == val
-270            ),
-271            None,
-272        )
-273        if dialog:
-274            return Dialog(**dialog)
-275        return None
-276        
-277    def add_dialog(self, dialog: Dialog) -> None:
-278        """
-279        Add a dialog to the vCon.
-280
-281        :param dialog: the dialog to add
-282        :type dialog: dict
-283        :return: None
-284        :rtype: None
-285        """
-286        self.vcon_dict["dialog"].append(dialog.to_dict())
-287
-288    def to_json(self) -> str:
-289        """
-290        Serialize the vCon to a JSON string.
-291
-292        :return: a JSON string representation of the vCon
-293        :rtype: str
-294        """
-295        tmp_vcon_dict = copy.copy(self.vcon_dict)
-296        return json.dumps(tmp_vcon_dict)
-297
-298    def to_dict(self) -> dict:
-299        """
-300        Serialize the vCon to a dictionary.
-301
-302        :return: a dictionary representation of the vCon
-303        :rtype: dict
-304        """
-305        return json.loads(self.to_json())
-306
-307    def dumps(self) -> str:
-308        """
-309        Alias for `to_json()`.
-310
-311        :return: a JSON string representation of the vCon
-312        :rtype: str
-313        """
-314        return self.to_json()
-315
-316    @property
-317    def parties(self) -> list[Party]:
-318        """
-319        Returns the list of parties.
-320
-321        :return: a list of parties
-322        :rtype: list[Party]
-323        """
-324        return [Party(**party) for party in self.vcon_dict.get("parties", [])]
-325
-326    @property
-327    def dialog(self) -> list:
-328        return self.vcon_dict.get("dialog", [])
-329
-330    @property
-331    def attachments(self) -> list:
-332        return self.vcon_dict.get("attachments", [])
-333
-334    @property
-335    def analysis(self):
-336        return self.vcon_dict.get("analysis", [])
-337
-338    @property
-339    def uuid(self) -> str:
-340        return self.vcon_dict["uuid"]
-341
-342    @property
-343    def vcon(self) -> str:
-344        return self.vcon_dict["vcon"]
-345
-346    @property
-347    def subject(self) -> Optional[str]:
-348        return self.vcon_dict.get("subject")
-349
-350    @property
-351    def created_at(self):
-352        return self.vcon_dict.get("created_at")
-353
-354    @property
-355    def updated_at(self):
-356        return self.vcon_dict.get("updated_at")
-357
-358    @property
-359    def redacted(self):
-360        return self.vcon_dict.get("redacted")
-361
-362    @property
-363    def appended(self):
-364        return self.vcon_dict.get("appended")
-365
-366    @property
-367    def group(self):
-368        return self.vcon_dict.get("group", [])
-369
-370    @property
-371    def meta(self):
-372        return self.vcon_dict.get("meta", {})
-373
-374    @staticmethod
-375    def uuid8_domain_name(domain_name: str) -> str:
-376        sha1_hasher = hashlib.sha1()
-377        sha1_hasher.update(bytes(domain_name, "utf-8"))
-378        dn_sha1 = sha1_hasher.digest()
-379
-380        hash_upper_64 = dn_sha1[0:8]
-381        int64 = int.from_bytes(hash_upper_64, byteorder="big")
-382
-383        uuid8_domain = Vcon.uuid8_time(int64)
-384
-385        return uuid8_domain
-386
-387    @staticmethod
-388    def uuid8_time(custom_c_62_bits: int) -> str:
-389        global _LAST_V8_TIMESTAMP
-390
-391        ns = time.time_ns()
-392        if _LAST_V8_TIMESTAMP is not None and ns <= _LAST_V8_TIMESTAMP:
-393            ns = _LAST_V8_TIMESTAMP + 1
-394        timestamp_ms, timestamp_ns = divmod(ns, 10**6)
-395        subsec = uuid6._subsec_encode(timestamp_ns)
-396
-397        subsec_a = subsec >> 8
-398        uuid_int = (timestamp_ms & 0xFFFFFFFFFFFF) << 80
-399        uuid_int |= subsec_a << 64
-400        uuid_int |= custom_c_62_bits
-401
-402        uuid_str = str(uuid6.UUID(int=uuid_int, version=7))
-403        assert uuid_str[14] == "7"
-404        uuid_str = uuid_str[:14] + "8" + uuid_str[15:]
-405
-406        return uuid_str
-407
-408
-409    def sign(self, private_key) -> None:
-410        """
-411        Sign the vCon using JWS.
-412
-413        :param private_key: the private key used for signing
-414        :type private_key: Union[rsa.RSAPrivateKey, bytes]
-415        :return: None
-416        :rtype: None
-417        """
-418        """Sign the vCon using JWS."""
-419        payload = self.to_json()
-420        jws = JsonWebSignature()
-421        protected = {
-422            "alg": "RS256",
-423            "typ": "JWS"
-424        }
-425        
-426        # Convert private key to PEM format if it's not already
-427        if isinstance(private_key, rsa.RSAPrivateKey):
-428            pem = private_key.private_bytes(
-429                encoding=serialization.Encoding.PEM,
-430                format=serialization.PrivateFormat.PKCS8,
-431                encryption_algorithm=serialization.NoEncryption()
-432            )
-433        else:
-434            pem = private_key
-435
-436        signed = jws.serialize_compact(protected, payload, pem)
-437        signed_str = signed.decode('utf-8')
-438        header, payload, signature = signed_str.split('.')
-439        
-440        self.vcon_dict['signatures'] = [{
-441            "protected": header,
-442            "signature": signature
-443        }]
-444        self.vcon_dict['payload'] = payload
-445
-446    def verify(self, public_key) -> bool:
-447        """Verify the JWS signature of the vCon.
-448
-449        :param public_key: the public key used for verification
-450        :type public_key: Union[rsa.RSAPublicKey, bytes]
-451        :return: True if the signature is valid, False otherwise
-452        :rtype: bool
-453        """
-454        """Verify the JWS signature of the vCon."""
-455        if 'signatures' not in self.vcon_dict or 'payload' not in self.vcon_dict:
-456            raise ValueError("vCon is not signed")
-457
-458        jws = JsonWebSignature()
-459        signed_data = f"{self.vcon_dict['signatures'][0]['protected']}.{self.vcon_dict['payload']}.{self.vcon_dict['signatures'][0]['signature']}"
-460        
-461        # Convert public key to PEM format if it's not already
-462        if isinstance(public_key, rsa.RSAPublicKey):
-463            pem = public_key.public_bytes(
-464                encoding=serialization.Encoding.PEM,
-465                format=serialization.PublicFormat.SubjectPublicKeyInfo
-466            )
-467        else:
-468            pem = public_key
-469
-470        try:
-471            jws.deserialize_compact(signed_data, pem)
-472            return True
-473        except BadSignatureError:
-474            return False
-475
-476    @classmethod
-477    def generate_key_pair(cls) -> tuple:
-478        """
-479        Generate a new RSA key pair for signing vCons.
-480
-481        :return: a tuple containing the private key and public key
-482        :rtype: tuple[rSA.RSAPrivateKey, rsa.RSAPublicKey]
-483        """
-484        private_key = rsa.generate_private_key(
-485            public_exponent=65537,
-486            key_size=2048
-487        )
-488        public_key = private_key.public_key()
-489        return private_key, public_key
-
- - - - -
- -
- - Vcon(vcon_dict={}) - - - -
- -
25    def __init__(self, vcon_dict={}) -> None:
-26        # deep copy
-27        """
-28        Initialize a Vcon object from a dictionary.
-29
-30        :param vcon_dict: a dictionary representing a vCon
-31        :type vcon_dict: dict
-32        """
-33        self.vcon_dict = json.loads(json.dumps(vcon_dict))
-
- - -

Initialize a Vcon object from a dictionary.

- -
Parameters
- -
    -
  • vcon_dict: a dictionary representing a vCon
  • -
-
- - -
-
-
- vcon_dict - - -
- - - - -
-
- -
-
@classmethod
- - def - build_from_json(cls, json_string: str) -> Vcon: - - - -
- -
35    @classmethod
-36    def build_from_json(cls, json_string: str) -> Vcon:
-37        """
-38        Initialize a Vcon object from a JSON string.
-39
-40        :param json_string: a JSON string representing a vCon
-41        :type json_string: str
-42        :return: a Vcon object
-43        :rtype: Vcon
-44        """
-45        return cls(json.loads(json_string))
-
- - -

Initialize a Vcon object from a JSON string.

- -
Parameters
- -
    -
  • json_string: a JSON string representing a vCon
  • -
- -
Returns
- -
-

a Vcon object

-
-
- - -
-
- -
-
@classmethod
- - def - build_new(cls) -> Vcon: - - - -
- -
47    @classmethod
-48    def build_new(cls) -> Vcon:
-49        """
-50        Initialize a Vcon object with default values.
-51
-52        :return: a Vcon object
-53        :rtype: Vcon
-54        """
-55        vcon_dict = {
-56            "uuid": cls.uuid8_domain_name("strolid.com"),
-57            "vcon": "0.0.1",
-58            "created_at": datetime.now(timezone.utc).isoformat()[:-3] + "+00:00",
-59            "redacted": {},
-60            "group": [],
-61            "parties": [],
-62            "dialog": [],
-63            "attachments": [],
-64            "analysis": [],
-65        }
-66        return cls(vcon_dict)
-
- - -

Initialize a Vcon object with default values.

- -
Returns
- -
-

a Vcon object

-
-
- - -
-
- -
- tags: Optional[dict] - - - -
- -
68    @property
-69    def tags(self) -> Optional[dict]:
-70        """
-71        Returns the tags attachment.
-72        
-73        :return: the tags attachment
-74        :rtype: dict or None
-75        """
-76        return self.find_attachment_by_type("tags")
-
- - -

Returns the tags attachment.

- -
Returns
- -
-

the tags attachment

-
-
- - -
-
- -
- - def - get_tag(self, tag_name) -> Optional[dict]: - - - -
- -
78    def get_tag(self, tag_name) -> Optional[dict]:
-79        """
-80        Returns the value of a tag by name.
-81
-82        :param tag_name: the name of the tag
-83        :type tag_name: str
-84        :return: the value of the tag or None if not found
-85        :rtype: str or None
-86        """
-87        tags_attachment = self.find_attachment_by_type("tags")
-88        if not tags_attachment:
-89            return None
-90        tag = next(
-91            (t for t in tags_attachment["body"] if t.startswith(f"{tag_name}:")), None
-92        )
-93        if not tag:
-94            return None
-95        tag_value = tag.split(":")[1]
-96        return tag_value
-
- - -

Returns the value of a tag by name.

- -
Parameters
- -
    -
  • tag_name: the name of the tag
  • -
- -
Returns
- -
-

the value of the tag or None if not found

-
-
- - -
-
- -
- - def - add_tag(self, tag_name, tag_value) -> None: - - - -
- -
 98    def add_tag(self, tag_name, tag_value) -> None:
- 99        """
-100        Adds a tag to the vCon.
-101
-102        :param tag_name: the name of the tag
-103        :type tag_name: str
-104        :param tag_value: the value of the tag
-105        :type tag_value: str
-106        :return: None
-107        :rtype: None
-108        """
-109        tags_attachment = self.find_attachment_by_type("tags")
-110        if not tags_attachment:
-111            tags_attachment = {
-112                "type": "tags",
-113                "body": [],
-114                "encoding": "json",
-115            }
-116            self.vcon_dict["attachments"].append(tags_attachment)
-117        tags_attachment["body"].append(f"{tag_name}:{tag_value}")
-
- - -

Adds a tag to the vCon.

- -
Parameters
- -
    -
  • tag_name: the name of the tag
  • -
  • tag_value: the value of the tag
  • -
- -
Returns
- -
-

None

-
-
- - -
-
- -
- - def - find_attachment_by_type(self, type: str) -> Optional[dict]: - - - -
- -
119    def find_attachment_by_type(self, type: str) -> Optional[dict]:
-120        """
-121        Finds an attachment by type.
-122
-123        :param type: the type of the attachment
-124        :type type: str
-125        :return: the attachment or None if not found
-126        :rtype: dict or None
-127        """
-128        return next(
-129            (a for a in self.vcon_dict["attachments"] if a["type"] == type), None
-130        )
-
- - -

Finds an attachment by type.

- -
Parameters
- -
    -
  • type: the type of the attachment
  • -
- -
Returns
- -
-

the attachment or None if not found

-
-
- - -
-
- -
- - def - add_attachment( self, *, body: Union[dict, list, str], type: str, encoding='none') -> None: - - - -
- -
132    def add_attachment(self, *, body: Union[dict, list, str], type: str, encoding="none") -> None:
-133        """
-134        Adds an attachment to the vCon.
-135
-136        :param body: the body of the attachment
-137        :type body: Union[dict, list, str]
-138        :param type: the type of the attachment
-139        :type type: str
-140        :param encoding: the encoding of the attachment body
-141        :type encoding: str
-142        :return: None
-143        :rtype: None
-144        """
-145        if encoding not in ['json', 'none', 'base64url']:
-146            raise Exception("Invalid encoding")
-147        
-148        if encoding == "json":
-149            try:
-150                json.loads(body)
-151            except Exception as e:
-152                raise Exception("Invalid JSON body: ", e)
-153            
-154        if encoding == 'base64url':
-155            try:
-156                base64.urlsafe_b64decode(body)
-157            except Exception as e:
-158                raise Exception("Invalid base64url body: ", e)
-159
-160        attachment = {
-161            "type": type,
-162            "body": body,
-163            "encoding": encoding,
-164        }
-165        self.vcon_dict["attachments"].append(attachment)
-
- - -

Adds an attachment to the vCon.

- -
Parameters
- -
    -
  • body: the body of the attachment
  • -
  • type: the type of the attachment
  • -
  • encoding: the encoding of the attachment body
  • -
- -
Returns
- -
-

None

-
-
- - -
-
- -
- - def - find_analysis_by_type(self, type) -> typing.Any | None: - - - -
- -
167    def find_analysis_by_type(self, type) -> Any | None:
-168        """
-169        Finds an analysis by type.
-170
-171        :param type: the type of the analysis
-172        :type type: str
-173        :return: the analysis or None if not found
-174        :rtype: dict or None
-175        """
-176        return next((a for a in self.vcon_dict["analysis"] if a["type"] == type), None)
-
- - -

Finds an analysis by type.

- -
Parameters
- -
    -
  • type: the type of the analysis
  • -
- -
Returns
- -
-

the analysis or None if not found

-
-
- - -
-
- -
- - def - add_analysis( self, *, type: str, dialog: Union[list, int], vendor: str, body: Union[dict, list, str], encoding='none', extra={}) -> None: - - - -
- -
178    def add_analysis(self, *, type: str, dialog: Union[list, int], vendor: str, body: Union[dict, list, str], encoding="none", extra={}) -> None:
-179        """
-180        Adds analysis data to the vCon.
-181
-182        :param type: the type of the analysis
-183        :type type: str
-184        :param dialog: the dialog(s) associated with the analysis
-185        :type dialog: Union[list, int]
-186        :param vendor: the vendor of the analysis
-187        :type vendor: str
-188        :param body: the body of the analysis
-189        :type body: Union[dict, list, str]
-190        :param encoding: the encoding of the body
-191        :type encoding: str
-192        :param extra: extra key-value pairs to include in the analysis
-193        :type extra: dict
-194        :return: None
-195        :rtype: None
-196        """
-197        if encoding not in ['json', 'none', 'base64url']:
-198            raise Exception("Invalid encoding")
-199        
-200        if encoding == "json":
-201            try:
-202                json.loads(body)
-203            except Exception as e:
-204                raise Exception("Invalid JSON body: ", e)
-205            
-206        if encoding == 'base64url':
-207            try:
-208                base64.urlsafe_b64decode(body)
-209            except Exception as e:
-210                raise Exception("Invalid base64url body: ", e)
-211
-212        analysis = {
-213            "type": type,
-214            "dialog": dialog,
-215            "vendor": vendor,
-216            "body": body,
-217            "encoding": encoding,
-218            **extra,
-219        }
-220        self.vcon_dict["analysis"].append(analysis)
-
- - -

Adds analysis data to the vCon.

- -
Parameters
- -
    -
  • type: the type of the analysis
  • -
  • dialog: the dialog(s) associated with the analysis
  • -
  • vendor: the vendor of the analysis
  • -
  • body: the body of the analysis
  • -
  • encoding: the encoding of the body
  • -
  • extra: extra key-value pairs to include in the analysis
  • -
- -
Returns
- -
-

None

-
-
- - -
-
- -
- - def - add_party(self, party: vcon.party.Party) -> None: - - - -
- -
222    def add_party(self, party: Party) -> None:
-223        """
-224        Adds a party to the vCon.
-225
-226        :param party: the party to add
-227        :type party: Party
-228        :return: None
-229        :rtype: None
-230        """
-231        self.vcon_dict["parties"].append(party.to_dict())
-
- - -

Adds a party to the vCon.

- -
Parameters
- -
    -
  • party: the party to add
  • -
- -
Returns
- -
-

None

-
-
- - -
-
- -
- - def - find_party_index(self, by: str, val: str) -> Optional[int]: - - - -
- -
233    def find_party_index(self, by: str, val: str) -> Optional[int]:
-234        """
-235        Find the index of a party in the vCon given a key-value pair.
-236
-237        :param by: the key to look for
-238        :type by: str
-239        :param val: the value to look for
-240        :type val: str
-241        :return: The index of the party if found, None otherwise
-242        :rtype: Optional[int]
-243        """
-244        return next(
-245            (
-246                ind
-247                for ind, party in enumerate(self.vcon_dict["parties"])
-248                if _get(party, by) == val
-249            ),
-250            None,
-251        )
-
- - -

Find the index of a party in the vCon given a key-value pair.

- -
Parameters
- -
    -
  • by: the key to look for
  • -
  • val: the value to look for
  • -
- -
Returns
- -
-

The index of the party if found, None otherwise

-
-
- - -
-
- -
- - def - find_dialog(self, by: str, val: str) -> Optional[vcon.dialog.Dialog]: - - - -
- -
254    def find_dialog(self, by: str, val: str) -> Optional[Dialog]:
-255        """
-256        Find a dialog in the vCon given a key-value pair. Convert the dialog to a Dialog object.
-257
-258        :param by: the key to look for
-259        :type by: str
-260        :param val: the value to look for
-261        :type val: str
-262        :return: The dialog if found, None otherwise
-263        :rtype: Optional[dict]
-264        """
-265        dialog = next(
-266            (
-267                dialog
-268                for dialog in self.vcon_dict["dialog"]
-269                if _get(dialog, by) == val
-270            ),
-271            None,
-272        )
-273        if dialog:
-274            return Dialog(**dialog)
-275        return None
-
- - -

Find a dialog in the vCon given a key-value pair. Convert the dialog to a Dialog object.

- -
Parameters
- -
    -
  • by: the key to look for
  • -
  • val: the value to look for
  • -
- -
Returns
- -
-

The dialog if found, None otherwise

-
-
- - -
-
- -
- - def - add_dialog(self, dialog: vcon.dialog.Dialog) -> None: - - - -
- -
277    def add_dialog(self, dialog: Dialog) -> None:
-278        """
-279        Add a dialog to the vCon.
-280
-281        :param dialog: the dialog to add
-282        :type dialog: dict
-283        :return: None
-284        :rtype: None
-285        """
-286        self.vcon_dict["dialog"].append(dialog.to_dict())
-
- - -

Add a dialog to the vCon.

- -
Parameters
- -
    -
  • dialog: the dialog to add
  • -
- -
Returns
- -
-

None

-
-
- - -
-
- -
- - def - to_json(self) -> str: - - - -
- -
288    def to_json(self) -> str:
-289        """
-290        Serialize the vCon to a JSON string.
-291
-292        :return: a JSON string representation of the vCon
-293        :rtype: str
-294        """
-295        tmp_vcon_dict = copy.copy(self.vcon_dict)
-296        return json.dumps(tmp_vcon_dict)
-
- - -

Serialize the vCon to a JSON string.

- -
Returns
- -
-

a JSON string representation of the vCon

-
-
- - -
-
- -
- - def - to_dict(self) -> dict: - - - -
- -
298    def to_dict(self) -> dict:
-299        """
-300        Serialize the vCon to a dictionary.
-301
-302        :return: a dictionary representation of the vCon
-303        :rtype: dict
-304        """
-305        return json.loads(self.to_json())
-
- - -

Serialize the vCon to a dictionary.

- -
Returns
- -
-

a dictionary representation of the vCon

-
-
- - -
-
- -
- - def - dumps(self) -> str: - - - -
- -
307    def dumps(self) -> str:
-308        """
-309        Alias for `to_json()`.
-310
-311        :return: a JSON string representation of the vCon
-312        :rtype: str
-313        """
-314        return self.to_json()
-
- - -

Alias for to_json().

- -
Returns
- -
-

a JSON string representation of the vCon

-
-
- - -
-
- -
- parties: list[vcon.party.Party] - - - -
- -
316    @property
-317    def parties(self) -> list[Party]:
-318        """
-319        Returns the list of parties.
-320
-321        :return: a list of parties
-322        :rtype: list[Party]
-323        """
-324        return [Party(**party) for party in self.vcon_dict.get("parties", [])]
-
- - -

Returns the list of parties.

- -
Returns
- -
-

a list of parties

-
-
- - -
-
- -
- dialog: list - - - -
- -
326    @property
-327    def dialog(self) -> list:
-328        return self.vcon_dict.get("dialog", [])
-
- - - - -
-
- -
- attachments: list - - - -
- -
330    @property
-331    def attachments(self) -> list:
-332        return self.vcon_dict.get("attachments", [])
-
- - - - -
-
- -
- analysis - - - -
- -
334    @property
-335    def analysis(self):
-336        return self.vcon_dict.get("analysis", [])
-
- - - - -
-
- -
- uuid: str - - - -
- -
338    @property
-339    def uuid(self) -> str:
-340        return self.vcon_dict["uuid"]
-
- - - - -
-
- -
- vcon: str - - - -
- -
342    @property
-343    def vcon(self) -> str:
-344        return self.vcon_dict["vcon"]
-
- - - - -
-
- -
- subject: Optional[str] - - - -
- -
346    @property
-347    def subject(self) -> Optional[str]:
-348        return self.vcon_dict.get("subject")
-
- - - - -
-
- -
- created_at - - - -
- -
350    @property
-351    def created_at(self):
-352        return self.vcon_dict.get("created_at")
-
- - - - -
-
- -
- updated_at - - - -
- -
354    @property
-355    def updated_at(self):
-356        return self.vcon_dict.get("updated_at")
-
- - - - -
-
- -
- redacted - - - -
- -
358    @property
-359    def redacted(self):
-360        return self.vcon_dict.get("redacted")
-
- - - - -
-
- -
- appended - - - -
- -
362    @property
-363    def appended(self):
-364        return self.vcon_dict.get("appended")
-
- - - - -
-
- -
- group - - - -
- -
366    @property
-367    def group(self):
-368        return self.vcon_dict.get("group", [])
-
- - - - -
-
- -
- meta - - - -
- -
370    @property
-371    def meta(self):
-372        return self.vcon_dict.get("meta", {})
-
- - - - -
-
- -
-
@staticmethod
- - def - uuid8_domain_name(domain_name: str) -> str: - - - -
- -
374    @staticmethod
-375    def uuid8_domain_name(domain_name: str) -> str:
-376        sha1_hasher = hashlib.sha1()
-377        sha1_hasher.update(bytes(domain_name, "utf-8"))
-378        dn_sha1 = sha1_hasher.digest()
-379
-380        hash_upper_64 = dn_sha1[0:8]
-381        int64 = int.from_bytes(hash_upper_64, byteorder="big")
-382
-383        uuid8_domain = Vcon.uuid8_time(int64)
-384
-385        return uuid8_domain
-
- - - - -
-
- -
-
@staticmethod
- - def - uuid8_time(custom_c_62_bits: int) -> str: - - - -
- -
387    @staticmethod
-388    def uuid8_time(custom_c_62_bits: int) -> str:
-389        global _LAST_V8_TIMESTAMP
-390
-391        ns = time.time_ns()
-392        if _LAST_V8_TIMESTAMP is not None and ns <= _LAST_V8_TIMESTAMP:
-393            ns = _LAST_V8_TIMESTAMP + 1
-394        timestamp_ms, timestamp_ns = divmod(ns, 10**6)
-395        subsec = uuid6._subsec_encode(timestamp_ns)
-396
-397        subsec_a = subsec >> 8
-398        uuid_int = (timestamp_ms & 0xFFFFFFFFFFFF) << 80
-399        uuid_int |= subsec_a << 64
-400        uuid_int |= custom_c_62_bits
-401
-402        uuid_str = str(uuid6.UUID(int=uuid_int, version=7))
-403        assert uuid_str[14] == "7"
-404        uuid_str = uuid_str[:14] + "8" + uuid_str[15:]
-405
-406        return uuid_str
-
- - - - -
-
- -
- - def - sign(self, private_key) -> None: - - - -
- -
409    def sign(self, private_key) -> None:
-410        """
-411        Sign the vCon using JWS.
-412
-413        :param private_key: the private key used for signing
-414        :type private_key: Union[rsa.RSAPrivateKey, bytes]
-415        :return: None
-416        :rtype: None
-417        """
-418        """Sign the vCon using JWS."""
-419        payload = self.to_json()
-420        jws = JsonWebSignature()
-421        protected = {
-422            "alg": "RS256",
-423            "typ": "JWS"
-424        }
-425        
-426        # Convert private key to PEM format if it's not already
-427        if isinstance(private_key, rsa.RSAPrivateKey):
-428            pem = private_key.private_bytes(
-429                encoding=serialization.Encoding.PEM,
-430                format=serialization.PrivateFormat.PKCS8,
-431                encryption_algorithm=serialization.NoEncryption()
-432            )
-433        else:
-434            pem = private_key
-435
-436        signed = jws.serialize_compact(protected, payload, pem)
-437        signed_str = signed.decode('utf-8')
-438        header, payload, signature = signed_str.split('.')
-439        
-440        self.vcon_dict['signatures'] = [{
-441            "protected": header,
-442            "signature": signature
-443        }]
-444        self.vcon_dict['payload'] = payload
-
- - -

Sign the vCon using JWS.

- -
Parameters
- -
    -
  • private_key: the private key used for signing
  • -
- -
Returns
- -
-

None

-
-
- - -
-
- -
- - def - verify(self, public_key) -> bool: - - - -
- -
446    def verify(self, public_key) -> bool:
-447        """Verify the JWS signature of the vCon.
-448
-449        :param public_key: the public key used for verification
-450        :type public_key: Union[rsa.RSAPublicKey, bytes]
-451        :return: True if the signature is valid, False otherwise
-452        :rtype: bool
-453        """
-454        """Verify the JWS signature of the vCon."""
-455        if 'signatures' not in self.vcon_dict or 'payload' not in self.vcon_dict:
-456            raise ValueError("vCon is not signed")
-457
-458        jws = JsonWebSignature()
-459        signed_data = f"{self.vcon_dict['signatures'][0]['protected']}.{self.vcon_dict['payload']}.{self.vcon_dict['signatures'][0]['signature']}"
-460        
-461        # Convert public key to PEM format if it's not already
-462        if isinstance(public_key, rsa.RSAPublicKey):
-463            pem = public_key.public_bytes(
-464                encoding=serialization.Encoding.PEM,
-465                format=serialization.PublicFormat.SubjectPublicKeyInfo
-466            )
-467        else:
-468            pem = public_key
-469
-470        try:
-471            jws.deserialize_compact(signed_data, pem)
-472            return True
-473        except BadSignatureError:
-474            return False
-
- - -

Verify the JWS signature of the vCon.

- -
Parameters
- -
    -
  • public_key: the public key used for verification
  • -
- -
Returns
- -
-

True if the signature is valid, False otherwise

-
-
- - -
-
- -
-
@classmethod
- - def - generate_key_pair(cls) -> tuple: - - - -
- -
476    @classmethod
-477    def generate_key_pair(cls) -> tuple:
-478        """
-479        Generate a new RSA key pair for signing vCons.
-480
-481        :return: a tuple containing the private key and public key
-482        :rtype: tuple[rSA.RSAPrivateKey, rsa.RSAPublicKey]
-483        """
-484        private_key = rsa.generate_private_key(
-485            public_exponent=65537,
-486            key_size=2048
-487        )
-488        public_key = private_key.public_key()
-489        return private_key, public_key
-
- - -

Generate a new RSA key pair for signing vCons.

- -
Returns
- -
-

a tuple containing the private key and public key

-
-
- - -
-
-
- - \ No newline at end of file diff --git a/docs/vcon/dialog.html b/docs/vcon/dialog.html deleted file mode 100644 index dcbb792..0000000 --- a/docs/vcon/dialog.html +++ /dev/null @@ -1,1708 +0,0 @@ - - - - - - - vcon.dialog API documentation - - - - - - - - - -
-
-

-vcon.dialog

- - - - - - -
  1import requests
-  2import hashlib
-  3import base64
-  4from datetime import datetime
-  5from typing import Optional, List
-  6from .party import PartyHistory
-  7
-  8
-  9MIME_TYPES = [
- 10    "text/plain",
- 11    "audio/x-wav",
- 12    "audio/x-mp3",
- 13    "audio/x-mp4",
- 14    "audio/ogg",
- 15    "video/x-mp4",
- 16    "video/ogg",
- 17    "multipart/mixed",
- 18    "message/external-body",
- 19]
- 20
- 21
- 22class Dialog:
- 23    def __init__(self, 
- 24                 type: str, 
- 25                 start: datetime, 
- 26                 parties: List[int], 
- 27                 originator: Optional[int] = None, 
- 28                 mimetype: Optional[str] = None, 
- 29                 filename: Optional[str] = None, 
- 30                 body: Optional[str] = None, 
- 31                 encoding: Optional[str] = None, 
- 32                 url: Optional[str] = None, 
- 33                 alg: Optional[str] = None, 
- 34                 signature: Optional[str] = None, 
- 35                 disposition: Optional[str] = None, 
- 36                 party_history: Optional[List[PartyHistory]] = None, 
- 37                 transferee: Optional[int] = None, 
- 38                 transferor: Optional[int] = None, 
- 39                 transfer_target: Optional[int] = None, 
- 40                 original: Optional[int] = None, 
- 41                 consultation: Optional[int] = None, 
- 42                 target_dialog: Optional[int] = None, 
- 43                 campaign: Optional[str] = None, 
- 44                 interaction: Optional[str] = None, 
- 45                 skill: Optional[str] = None, 
- 46                 duration: Optional[float] = None,
- 47                 meta: Optional[dict] = None) -> None:
- 48        """
- 49        Initialize a Dialog object.
- 50
- 51        :param type: the type of the dialog (e.g. "text", "audio", etc.)
- 52        :type type: str
- 53        :param start: the start time of the dialog
- 54        :type start: datetime
- 55        :param parties: the parties involved in the dialog
- 56        :type parties: List[int]
- 57        :param originator: the party that originated the dialog
- 58        :type originator: int or None
- 59        :param mimetype: the MIME type of the dialog body
- 60        :type mimetype: str or None
- 61        :param filename: the filename of the dialog body
- 62        :type filename: str or None
- 63        :param body: the body of the dialog
- 64        :type body: str or None
- 65        :param encoding: the encoding of the dialog body
- 66        :type encoding: str or None
- 67        :param url: the URL of the dialog
- 68        :type url: str or None
- 69        :param alg: the algorithm used to sign the dialog
- 70        :type alg: str or None
- 71        :param signature: the signature of the dialog
- 72        :type signature: str or None
- 73        :param disposition: the disposition of the dialog
- 74        :type disposition: str or None
- 75        :param party_history: the history of parties involved in the dialog
- 76        :type party_history: List[PartyHistory] or None
- 77        :param transferee: the party that the dialog was transferred to
- 78        :type transferee: int or None
- 79        :param transferor: the party that transferred the dialog
- 80        :type transferor: int or None
- 81        :param transfer_target: the target of the transfer
- 82        :type transfer_target: int or None
- 83        :param original: the original dialog
- 84        :type original: int or None
- 85        :param consultation: the consultation dialog
- 86        :type consultation: int or None
- 87        :param target_dialog: the target dialog
- 88        :type target_dialog: int or None
- 89        :param campaign: the campaign that the dialog is associated with
- 90        :type campaign: str or None
- 91        :param interaction: the interaction that the dialog is associated with
- 92        :type interaction: str or None
- 93        :param skill: the skill that the dialog is associated with
- 94        :type skill: str or None
- 95        :param duration: the duration of the dialog
- 96        :type duration: float or None
- 97        """
- 98        self.type = type
- 99        self.start = start
-100        self.parties = parties
-101        self.originator = originator
-102        self.mimetype = mimetype
-103        self.filename = filename
-104        self.body = body
-105        self.encoding = encoding
-106        self.url = url
-107        self.alg = alg
-108        self.signature = signature
-109        self.disposition = disposition
-110        self.party_history = party_history
-111        self.transferee = transferee
-112        self.transferor = transferor
-113        self.transfer_target = transfer_target
-114        self.original = original
-115        self.consultation = consultation
-116        self.target_dialog = target_dialog
-117        self.campaign = campaign
-118        self.interaction = interaction
-119        self.skill = skill
-120        self.duration = duration
-121        self.meta = meta
-122
-123    def to_dict(self):
-124        """
-125        Returns a dictionary representation of the Dialog object.
-126
-127        :return: a dictionary containing the Dialog object's attributes
-128        :rtype: dict
-129        """
-130        # Check to see if the start time provided. If not,
-131        # set the start time to the current time
-132        if not self.start:
-133            self.start = datetime.now().isoformat()
-134            
-135        dialog_dict = {
-136            "type": self.type,
-137            "start": self.start,
-138            "duration": self.duration,
-139            "parties": self.parties,
-140            "originator": self.originator,
-141            "mimetype": self.mimetype,
-142            "filename": self.filename,
-143            "body": self.body,
-144            "encoding": self.encoding,
-145            "url": self.url,
-146            "alg": self.alg,
-147            "signature": self.signature,
-148            "disposition": self.disposition,
-149            "party_history": (
-150                [party_history.to_dict() for party_history in self.party_history]
-151                if self.party_history
-152                else None
-153            ),
-154            "transferee": self.transferee,
-155            "transferor": self.transferor,
-156            "transfer_target": self.transfer_target,
-157            "original": self.original,
-158            "consultation": self.consultation,
-159            "target_dialog": self.target_dialog,
-160            "campaign": self.campaign,
-161            "interaction": self.interaction,
-162            "skill": self.skill,
-163            "meta": self.meta
-164        }
-165        return {k: v for k, v in dialog_dict.items() if v is not None} 
-166    
-167    def add_external_data(self, url: str, filename: str, mimetype: str) -> None:
-168        """
-169        Add external data to the dialog.
-170
-171        :param url: the URL of the external data
-172        :type url: str
-173        :return: None
-174        :rtype: None
-175        """
-176        response = requests.get(url)
-177        if response.status_code == 200:
-178            self.mimetype = response.headers["Content-Type"]
-179        else:
-180            raise Exception(f"Failed to fetch external data: {response.status_code}")
-181        
-182        # Overide the filename if provided, otherwise use the filename from the URL
-183        if filename:
-184            self.filename = filename
-185        else:
-186            self.filename = url.split("/")[-1]
-187
-188        # Overide the mimetype if provided, otherwise use the mimetype from the URL
-189        if mimetype:
-190            self.mimetype = mimetype
-191            
-192        # Calculate the SHA-256 hash of the body as the signature
-193        self.alg = "sha256"
-194        self.encoding = "base64url"
-195        self.signature = base64.urlsafe_b64encode(hashlib.sha256(response.text.encode()).digest()).decode()
-196
-197    def add_inline_data(self, body: str, filename: str, mimetype: str) -> None:
-198        """
-199        Add inline data to the dialog.
-200
-201        :param body: the body of the inline data
-202        :type body: str
-203        :param filename: the filename of the inline data
-204        :type filename: str
-205        :param mimetype: the mimetype of the inline data
-206        :type mimetype: str
-207        :return: None
-208        :rtype: None
-209        """
-210        self.body = body
-211        self.mimetype = mimetype
-212        self.filename = filename
-213        self.alg = "sha256"
-214        self.encoding = "base64url"
-215        self.signature = base64.urlsafe_b64encode(
-216            hashlib.sha256(self.body.encode()).digest()).decode()
-217        
-218    # Check if the dialog is an external data dialog
-219    def is_external_data(self) -> bool:
-220        return self.url is not None
-221      
-222    # Check if the dialog is an inline data dialog
-223    def is_inline_data(self) -> bool:
-224        return self.body is not None
-225    
-226    
-227    # Check if the dialog is a text dialog
-228    def is_text(self) -> bool:
-229        return self.mimetype == "text/plain"
-230    
-231    
-232    # Check if the dialog is an audio dialog
-233    def is_audio(self) -> bool:
-234        return self.mimetype in ["audio/x-wav", "audio/x-mp3", "audio/x-mp4", "audio/ogg"]
-235    
-236    
-237    # Check if the dialog is a video dialog
-238    def is_video(self) -> bool:
-239        return self.mimetype in ["video/x-mp4", "video/ogg"]
-240    
-241    # Check if the dialog is an email dialog
-242    def is_email(self) -> bool:
-243        return self.mimetype == "message/rfc822"
-244    
-245    # Check to see if it's an external data dialog, that the contents are valid by 
-246    # checking the hash of the body against the signature
-247    def is_external_data_changed(self) -> bool:
-248        if not self.is_external_data():
-249            return False
-250        try:
-251            body_hash = base64.urlsafe_b64decode(self.signature.encode())
-252            return hashlib.sha256(self.body.encode()).digest() != body_hash
-253        except Exception as e:
-254            print(e)
-255            return True
-256        
-257    # Convert the dialog from an external data dialog to an inline data dialog
-258    # by reading the contents from the URL then adding the contents to the body
-259    def to_inline_data(self) -> None:
-260        # Read the contents from the URL
-261        response = requests.get(self.url)
-262        if response.status_code == 200:
-263            self.body = response.text
-264            self.mimetype = response.headers["Content-Type"]
-265        else:
-266            raise Exception(f"Failed to fetch external data: {response.status_code}")
-267
-268        # Calculate the SHA-256 hash of the body as the signature
-269        self.alg = "sha256"
-270        self.encoding = "base64url"
-271        self.signature = base64.urlsafe_b64encode(hashlib.sha256(self.body.encode()).digest()).decode()
-272        
-273        # Overide the filename if provided, otherwise use the filename from the URL
-274        if self.filename:
-275            self.filename = self.filename
-276        else:
-277            self.filename = self.url.split("/")[-1]
-278
-279        # Overide the mimetype if provided, otherwise use the mimetype from the URL
-280        if self.mimetype:
-281            self.mimetype = self.mimetype
-282
-283        # Add the body to the dialog
-284        self.add_inline_data(self.body, self.filename, self.mimetype)
-
- - -
-
-
- MIME_TYPES = - - ['text/plain', 'audio/x-wav', 'audio/x-mp3', 'audio/x-mp4', 'audio/ogg', 'video/x-mp4', 'video/ogg', 'multipart/mixed', 'message/external-body'] - - -
- - - - -
-
- -
- - class - Dialog: - - - -
- -
 23class Dialog:
- 24    def __init__(self, 
- 25                 type: str, 
- 26                 start: datetime, 
- 27                 parties: List[int], 
- 28                 originator: Optional[int] = None, 
- 29                 mimetype: Optional[str] = None, 
- 30                 filename: Optional[str] = None, 
- 31                 body: Optional[str] = None, 
- 32                 encoding: Optional[str] = None, 
- 33                 url: Optional[str] = None, 
- 34                 alg: Optional[str] = None, 
- 35                 signature: Optional[str] = None, 
- 36                 disposition: Optional[str] = None, 
- 37                 party_history: Optional[List[PartyHistory]] = None, 
- 38                 transferee: Optional[int] = None, 
- 39                 transferor: Optional[int] = None, 
- 40                 transfer_target: Optional[int] = None, 
- 41                 original: Optional[int] = None, 
- 42                 consultation: Optional[int] = None, 
- 43                 target_dialog: Optional[int] = None, 
- 44                 campaign: Optional[str] = None, 
- 45                 interaction: Optional[str] = None, 
- 46                 skill: Optional[str] = None, 
- 47                 duration: Optional[float] = None,
- 48                 meta: Optional[dict] = None) -> None:
- 49        """
- 50        Initialize a Dialog object.
- 51
- 52        :param type: the type of the dialog (e.g. "text", "audio", etc.)
- 53        :type type: str
- 54        :param start: the start time of the dialog
- 55        :type start: datetime
- 56        :param parties: the parties involved in the dialog
- 57        :type parties: List[int]
- 58        :param originator: the party that originated the dialog
- 59        :type originator: int or None
- 60        :param mimetype: the MIME type of the dialog body
- 61        :type mimetype: str or None
- 62        :param filename: the filename of the dialog body
- 63        :type filename: str or None
- 64        :param body: the body of the dialog
- 65        :type body: str or None
- 66        :param encoding: the encoding of the dialog body
- 67        :type encoding: str or None
- 68        :param url: the URL of the dialog
- 69        :type url: str or None
- 70        :param alg: the algorithm used to sign the dialog
- 71        :type alg: str or None
- 72        :param signature: the signature of the dialog
- 73        :type signature: str or None
- 74        :param disposition: the disposition of the dialog
- 75        :type disposition: str or None
- 76        :param party_history: the history of parties involved in the dialog
- 77        :type party_history: List[PartyHistory] or None
- 78        :param transferee: the party that the dialog was transferred to
- 79        :type transferee: int or None
- 80        :param transferor: the party that transferred the dialog
- 81        :type transferor: int or None
- 82        :param transfer_target: the target of the transfer
- 83        :type transfer_target: int or None
- 84        :param original: the original dialog
- 85        :type original: int or None
- 86        :param consultation: the consultation dialog
- 87        :type consultation: int or None
- 88        :param target_dialog: the target dialog
- 89        :type target_dialog: int or None
- 90        :param campaign: the campaign that the dialog is associated with
- 91        :type campaign: str or None
- 92        :param interaction: the interaction that the dialog is associated with
- 93        :type interaction: str or None
- 94        :param skill: the skill that the dialog is associated with
- 95        :type skill: str or None
- 96        :param duration: the duration of the dialog
- 97        :type duration: float or None
- 98        """
- 99        self.type = type
-100        self.start = start
-101        self.parties = parties
-102        self.originator = originator
-103        self.mimetype = mimetype
-104        self.filename = filename
-105        self.body = body
-106        self.encoding = encoding
-107        self.url = url
-108        self.alg = alg
-109        self.signature = signature
-110        self.disposition = disposition
-111        self.party_history = party_history
-112        self.transferee = transferee
-113        self.transferor = transferor
-114        self.transfer_target = transfer_target
-115        self.original = original
-116        self.consultation = consultation
-117        self.target_dialog = target_dialog
-118        self.campaign = campaign
-119        self.interaction = interaction
-120        self.skill = skill
-121        self.duration = duration
-122        self.meta = meta
-123
-124    def to_dict(self):
-125        """
-126        Returns a dictionary representation of the Dialog object.
-127
-128        :return: a dictionary containing the Dialog object's attributes
-129        :rtype: dict
-130        """
-131        # Check to see if the start time provided. If not,
-132        # set the start time to the current time
-133        if not self.start:
-134            self.start = datetime.now().isoformat()
-135            
-136        dialog_dict = {
-137            "type": self.type,
-138            "start": self.start,
-139            "duration": self.duration,
-140            "parties": self.parties,
-141            "originator": self.originator,
-142            "mimetype": self.mimetype,
-143            "filename": self.filename,
-144            "body": self.body,
-145            "encoding": self.encoding,
-146            "url": self.url,
-147            "alg": self.alg,
-148            "signature": self.signature,
-149            "disposition": self.disposition,
-150            "party_history": (
-151                [party_history.to_dict() for party_history in self.party_history]
-152                if self.party_history
-153                else None
-154            ),
-155            "transferee": self.transferee,
-156            "transferor": self.transferor,
-157            "transfer_target": self.transfer_target,
-158            "original": self.original,
-159            "consultation": self.consultation,
-160            "target_dialog": self.target_dialog,
-161            "campaign": self.campaign,
-162            "interaction": self.interaction,
-163            "skill": self.skill,
-164            "meta": self.meta
-165        }
-166        return {k: v for k, v in dialog_dict.items() if v is not None} 
-167    
-168    def add_external_data(self, url: str, filename: str, mimetype: str) -> None:
-169        """
-170        Add external data to the dialog.
-171
-172        :param url: the URL of the external data
-173        :type url: str
-174        :return: None
-175        :rtype: None
-176        """
-177        response = requests.get(url)
-178        if response.status_code == 200:
-179            self.mimetype = response.headers["Content-Type"]
-180        else:
-181            raise Exception(f"Failed to fetch external data: {response.status_code}")
-182        
-183        # Overide the filename if provided, otherwise use the filename from the URL
-184        if filename:
-185            self.filename = filename
-186        else:
-187            self.filename = url.split("/")[-1]
-188
-189        # Overide the mimetype if provided, otherwise use the mimetype from the URL
-190        if mimetype:
-191            self.mimetype = mimetype
-192            
-193        # Calculate the SHA-256 hash of the body as the signature
-194        self.alg = "sha256"
-195        self.encoding = "base64url"
-196        self.signature = base64.urlsafe_b64encode(hashlib.sha256(response.text.encode()).digest()).decode()
-197
-198    def add_inline_data(self, body: str, filename: str, mimetype: str) -> None:
-199        """
-200        Add inline data to the dialog.
-201
-202        :param body: the body of the inline data
-203        :type body: str
-204        :param filename: the filename of the inline data
-205        :type filename: str
-206        :param mimetype: the mimetype of the inline data
-207        :type mimetype: str
-208        :return: None
-209        :rtype: None
-210        """
-211        self.body = body
-212        self.mimetype = mimetype
-213        self.filename = filename
-214        self.alg = "sha256"
-215        self.encoding = "base64url"
-216        self.signature = base64.urlsafe_b64encode(
-217            hashlib.sha256(self.body.encode()).digest()).decode()
-218        
-219    # Check if the dialog is an external data dialog
-220    def is_external_data(self) -> bool:
-221        return self.url is not None
-222      
-223    # Check if the dialog is an inline data dialog
-224    def is_inline_data(self) -> bool:
-225        return self.body is not None
-226    
-227    
-228    # Check if the dialog is a text dialog
-229    def is_text(self) -> bool:
-230        return self.mimetype == "text/plain"
-231    
-232    
-233    # Check if the dialog is an audio dialog
-234    def is_audio(self) -> bool:
-235        return self.mimetype in ["audio/x-wav", "audio/x-mp3", "audio/x-mp4", "audio/ogg"]
-236    
-237    
-238    # Check if the dialog is a video dialog
-239    def is_video(self) -> bool:
-240        return self.mimetype in ["video/x-mp4", "video/ogg"]
-241    
-242    # Check if the dialog is an email dialog
-243    def is_email(self) -> bool:
-244        return self.mimetype == "message/rfc822"
-245    
-246    # Check to see if it's an external data dialog, that the contents are valid by 
-247    # checking the hash of the body against the signature
-248    def is_external_data_changed(self) -> bool:
-249        if not self.is_external_data():
-250            return False
-251        try:
-252            body_hash = base64.urlsafe_b64decode(self.signature.encode())
-253            return hashlib.sha256(self.body.encode()).digest() != body_hash
-254        except Exception as e:
-255            print(e)
-256            return True
-257        
-258    # Convert the dialog from an external data dialog to an inline data dialog
-259    # by reading the contents from the URL then adding the contents to the body
-260    def to_inline_data(self) -> None:
-261        # Read the contents from the URL
-262        response = requests.get(self.url)
-263        if response.status_code == 200:
-264            self.body = response.text
-265            self.mimetype = response.headers["Content-Type"]
-266        else:
-267            raise Exception(f"Failed to fetch external data: {response.status_code}")
-268
-269        # Calculate the SHA-256 hash of the body as the signature
-270        self.alg = "sha256"
-271        self.encoding = "base64url"
-272        self.signature = base64.urlsafe_b64encode(hashlib.sha256(self.body.encode()).digest()).decode()
-273        
-274        # Overide the filename if provided, otherwise use the filename from the URL
-275        if self.filename:
-276            self.filename = self.filename
-277        else:
-278            self.filename = self.url.split("/")[-1]
-279
-280        # Overide the mimetype if provided, otherwise use the mimetype from the URL
-281        if self.mimetype:
-282            self.mimetype = self.mimetype
-283
-284        # Add the body to the dialog
-285        self.add_inline_data(self.body, self.filename, self.mimetype)
-
- - - - -
- -
- - Dialog( type: str, start: datetime.datetime, parties: List[int], originator: Optional[int] = None, mimetype: Optional[str] = None, filename: Optional[str] = None, body: Optional[str] = None, encoding: Optional[str] = None, url: Optional[str] = None, alg: Optional[str] = None, signature: Optional[str] = None, disposition: Optional[str] = None, party_history: Optional[List[vcon.party.PartyHistory]] = None, transferee: Optional[int] = None, transferor: Optional[int] = None, transfer_target: Optional[int] = None, original: Optional[int] = None, consultation: Optional[int] = None, target_dialog: Optional[int] = None, campaign: Optional[str] = None, interaction: Optional[str] = None, skill: Optional[str] = None, duration: Optional[float] = None, meta: Optional[dict] = None) - - - -
- -
 24    def __init__(self, 
- 25                 type: str, 
- 26                 start: datetime, 
- 27                 parties: List[int], 
- 28                 originator: Optional[int] = None, 
- 29                 mimetype: Optional[str] = None, 
- 30                 filename: Optional[str] = None, 
- 31                 body: Optional[str] = None, 
- 32                 encoding: Optional[str] = None, 
- 33                 url: Optional[str] = None, 
- 34                 alg: Optional[str] = None, 
- 35                 signature: Optional[str] = None, 
- 36                 disposition: Optional[str] = None, 
- 37                 party_history: Optional[List[PartyHistory]] = None, 
- 38                 transferee: Optional[int] = None, 
- 39                 transferor: Optional[int] = None, 
- 40                 transfer_target: Optional[int] = None, 
- 41                 original: Optional[int] = None, 
- 42                 consultation: Optional[int] = None, 
- 43                 target_dialog: Optional[int] = None, 
- 44                 campaign: Optional[str] = None, 
- 45                 interaction: Optional[str] = None, 
- 46                 skill: Optional[str] = None, 
- 47                 duration: Optional[float] = None,
- 48                 meta: Optional[dict] = None) -> None:
- 49        """
- 50        Initialize a Dialog object.
- 51
- 52        :param type: the type of the dialog (e.g. "text", "audio", etc.)
- 53        :type type: str
- 54        :param start: the start time of the dialog
- 55        :type start: datetime
- 56        :param parties: the parties involved in the dialog
- 57        :type parties: List[int]
- 58        :param originator: the party that originated the dialog
- 59        :type originator: int or None
- 60        :param mimetype: the MIME type of the dialog body
- 61        :type mimetype: str or None
- 62        :param filename: the filename of the dialog body
- 63        :type filename: str or None
- 64        :param body: the body of the dialog
- 65        :type body: str or None
- 66        :param encoding: the encoding of the dialog body
- 67        :type encoding: str or None
- 68        :param url: the URL of the dialog
- 69        :type url: str or None
- 70        :param alg: the algorithm used to sign the dialog
- 71        :type alg: str or None
- 72        :param signature: the signature of the dialog
- 73        :type signature: str or None
- 74        :param disposition: the disposition of the dialog
- 75        :type disposition: str or None
- 76        :param party_history: the history of parties involved in the dialog
- 77        :type party_history: List[PartyHistory] or None
- 78        :param transferee: the party that the dialog was transferred to
- 79        :type transferee: int or None
- 80        :param transferor: the party that transferred the dialog
- 81        :type transferor: int or None
- 82        :param transfer_target: the target of the transfer
- 83        :type transfer_target: int or None
- 84        :param original: the original dialog
- 85        :type original: int or None
- 86        :param consultation: the consultation dialog
- 87        :type consultation: int or None
- 88        :param target_dialog: the target dialog
- 89        :type target_dialog: int or None
- 90        :param campaign: the campaign that the dialog is associated with
- 91        :type campaign: str or None
- 92        :param interaction: the interaction that the dialog is associated with
- 93        :type interaction: str or None
- 94        :param skill: the skill that the dialog is associated with
- 95        :type skill: str or None
- 96        :param duration: the duration of the dialog
- 97        :type duration: float or None
- 98        """
- 99        self.type = type
-100        self.start = start
-101        self.parties = parties
-102        self.originator = originator
-103        self.mimetype = mimetype
-104        self.filename = filename
-105        self.body = body
-106        self.encoding = encoding
-107        self.url = url
-108        self.alg = alg
-109        self.signature = signature
-110        self.disposition = disposition
-111        self.party_history = party_history
-112        self.transferee = transferee
-113        self.transferor = transferor
-114        self.transfer_target = transfer_target
-115        self.original = original
-116        self.consultation = consultation
-117        self.target_dialog = target_dialog
-118        self.campaign = campaign
-119        self.interaction = interaction
-120        self.skill = skill
-121        self.duration = duration
-122        self.meta = meta
-
- - -

Initialize a Dialog object.

- -
Parameters
- -
    -
  • type: the type of the dialog (e.g. "text", "audio", etc.)
  • -
  • start: the start time of the dialog
  • -
  • parties: the parties involved in the dialog
  • -
  • originator: the party that originated the dialog
  • -
  • mimetype: the MIME type of the dialog body
  • -
  • filename: the filename of the dialog body
  • -
  • body: the body of the dialog
  • -
  • encoding: the encoding of the dialog body
  • -
  • url: the URL of the dialog
  • -
  • alg: the algorithm used to sign the dialog
  • -
  • signature: the signature of the dialog
  • -
  • disposition: the disposition of the dialog
  • -
  • party_history: the history of parties involved in the dialog
  • -
  • transferee: the party that the dialog was transferred to
  • -
  • transferor: the party that transferred the dialog
  • -
  • transfer_target: the target of the transfer
  • -
  • original: the original dialog
  • -
  • consultation: the consultation dialog
  • -
  • target_dialog: the target dialog
  • -
  • campaign: the campaign that the dialog is associated with
  • -
  • interaction: the interaction that the dialog is associated with
  • -
  • skill: the skill that the dialog is associated with
  • -
  • duration: the duration of the dialog
  • -
-
- - -
-
-
- type - - -
- - - - -
-
-
- start - - -
- - - - -
-
-
- parties - - -
- - - - -
-
-
- originator - - -
- - - - -
-
-
- mimetype - - -
- - - - -
-
-
- filename - - -
- - - - -
-
-
- body - - -
- - - - -
-
-
- encoding - - -
- - - - -
-
-
- url - - -
- - - - -
-
-
- alg - - -
- - - - -
-
-
- signature - - -
- - - - -
-
-
- disposition - - -
- - - - -
-
-
- party_history - - -
- - - - -
-
-
- transferee - - -
- - - - -
-
-
- transferor - - -
- - - - -
-
-
- transfer_target - - -
- - - - -
-
-
- original - - -
- - - - -
-
-
- consultation - - -
- - - - -
-
-
- target_dialog - - -
- - - - -
-
-
- campaign - - -
- - - - -
-
-
- interaction - - -
- - - - -
-
-
- skill - - -
- - - - -
-
-
- duration - - -
- - - - -
-
-
- meta - - -
- - - - -
-
- -
- - def - to_dict(self): - - - -
- -
124    def to_dict(self):
-125        """
-126        Returns a dictionary representation of the Dialog object.
-127
-128        :return: a dictionary containing the Dialog object's attributes
-129        :rtype: dict
-130        """
-131        # Check to see if the start time provided. If not,
-132        # set the start time to the current time
-133        if not self.start:
-134            self.start = datetime.now().isoformat()
-135            
-136        dialog_dict = {
-137            "type": self.type,
-138            "start": self.start,
-139            "duration": self.duration,
-140            "parties": self.parties,
-141            "originator": self.originator,
-142            "mimetype": self.mimetype,
-143            "filename": self.filename,
-144            "body": self.body,
-145            "encoding": self.encoding,
-146            "url": self.url,
-147            "alg": self.alg,
-148            "signature": self.signature,
-149            "disposition": self.disposition,
-150            "party_history": (
-151                [party_history.to_dict() for party_history in self.party_history]
-152                if self.party_history
-153                else None
-154            ),
-155            "transferee": self.transferee,
-156            "transferor": self.transferor,
-157            "transfer_target": self.transfer_target,
-158            "original": self.original,
-159            "consultation": self.consultation,
-160            "target_dialog": self.target_dialog,
-161            "campaign": self.campaign,
-162            "interaction": self.interaction,
-163            "skill": self.skill,
-164            "meta": self.meta
-165        }
-166        return {k: v for k, v in dialog_dict.items() if v is not None} 
-
- - -

Returns a dictionary representation of the Dialog object.

- -
Returns
- -
-

a dictionary containing the Dialog object's attributes

-
-
- - -
-
- -
- - def - add_external_data(self, url: str, filename: str, mimetype: str) -> None: - - - -
- -
168    def add_external_data(self, url: str, filename: str, mimetype: str) -> None:
-169        """
-170        Add external data to the dialog.
-171
-172        :param url: the URL of the external data
-173        :type url: str
-174        :return: None
-175        :rtype: None
-176        """
-177        response = requests.get(url)
-178        if response.status_code == 200:
-179            self.mimetype = response.headers["Content-Type"]
-180        else:
-181            raise Exception(f"Failed to fetch external data: {response.status_code}")
-182        
-183        # Overide the filename if provided, otherwise use the filename from the URL
-184        if filename:
-185            self.filename = filename
-186        else:
-187            self.filename = url.split("/")[-1]
-188
-189        # Overide the mimetype if provided, otherwise use the mimetype from the URL
-190        if mimetype:
-191            self.mimetype = mimetype
-192            
-193        # Calculate the SHA-256 hash of the body as the signature
-194        self.alg = "sha256"
-195        self.encoding = "base64url"
-196        self.signature = base64.urlsafe_b64encode(hashlib.sha256(response.text.encode()).digest()).decode()
-
- - -

Add external data to the dialog.

- -
Parameters
- -
    -
  • url: the URL of the external data
  • -
- -
Returns
- -
-

None

-
-
- - -
-
- -
- - def - add_inline_data(self, body: str, filename: str, mimetype: str) -> None: - - - -
- -
198    def add_inline_data(self, body: str, filename: str, mimetype: str) -> None:
-199        """
-200        Add inline data to the dialog.
-201
-202        :param body: the body of the inline data
-203        :type body: str
-204        :param filename: the filename of the inline data
-205        :type filename: str
-206        :param mimetype: the mimetype of the inline data
-207        :type mimetype: str
-208        :return: None
-209        :rtype: None
-210        """
-211        self.body = body
-212        self.mimetype = mimetype
-213        self.filename = filename
-214        self.alg = "sha256"
-215        self.encoding = "base64url"
-216        self.signature = base64.urlsafe_b64encode(
-217            hashlib.sha256(self.body.encode()).digest()).decode()
-
- - -

Add inline data to the dialog.

- -
Parameters
- -
    -
  • body: the body of the inline data
  • -
  • filename: the filename of the inline data
  • -
  • mimetype: the mimetype of the inline data
  • -
- -
Returns
- -
-

None

-
-
- - -
-
- -
- - def - is_external_data(self) -> bool: - - - -
- -
220    def is_external_data(self) -> bool:
-221        return self.url is not None
-
- - - - -
-
- -
- - def - is_inline_data(self) -> bool: - - - -
- -
224    def is_inline_data(self) -> bool:
-225        return self.body is not None
-
- - - - -
-
- -
- - def - is_text(self) -> bool: - - - -
- -
229    def is_text(self) -> bool:
-230        return self.mimetype == "text/plain"
-
- - - - -
-
- -
- - def - is_audio(self) -> bool: - - - -
- -
234    def is_audio(self) -> bool:
-235        return self.mimetype in ["audio/x-wav", "audio/x-mp3", "audio/x-mp4", "audio/ogg"]
-
- - - - -
-
- -
- - def - is_video(self) -> bool: - - - -
- -
239    def is_video(self) -> bool:
-240        return self.mimetype in ["video/x-mp4", "video/ogg"]
-
- - - - -
-
- -
- - def - is_email(self) -> bool: - - - -
- -
243    def is_email(self) -> bool:
-244        return self.mimetype == "message/rfc822"
-
- - - - -
-
- -
- - def - is_external_data_changed(self) -> bool: - - - -
- -
248    def is_external_data_changed(self) -> bool:
-249        if not self.is_external_data():
-250            return False
-251        try:
-252            body_hash = base64.urlsafe_b64decode(self.signature.encode())
-253            return hashlib.sha256(self.body.encode()).digest() != body_hash
-254        except Exception as e:
-255            print(e)
-256            return True
-
- - - - -
-
- -
- - def - to_inline_data(self) -> None: - - - -
- -
260    def to_inline_data(self) -> None:
-261        # Read the contents from the URL
-262        response = requests.get(self.url)
-263        if response.status_code == 200:
-264            self.body = response.text
-265            self.mimetype = response.headers["Content-Type"]
-266        else:
-267            raise Exception(f"Failed to fetch external data: {response.status_code}")
-268
-269        # Calculate the SHA-256 hash of the body as the signature
-270        self.alg = "sha256"
-271        self.encoding = "base64url"
-272        self.signature = base64.urlsafe_b64encode(hashlib.sha256(self.body.encode()).digest()).decode()
-273        
-274        # Overide the filename if provided, otherwise use the filename from the URL
-275        if self.filename:
-276            self.filename = self.filename
-277        else:
-278            self.filename = self.url.split("/")[-1]
-279
-280        # Overide the mimetype if provided, otherwise use the mimetype from the URL
-281        if self.mimetype:
-282            self.mimetype = self.mimetype
-283
-284        # Add the body to the dialog
-285        self.add_inline_data(self.body, self.filename, self.mimetype)
-
- - - - -
-
-
- - \ No newline at end of file diff --git a/docs/vcon/party.html b/docs/vcon/party.html deleted file mode 100644 index 7d40654..0000000 --- a/docs/vcon/party.html +++ /dev/null @@ -1,677 +0,0 @@ - - - - - - - vcon.party API documentation - - - - - - - - - -
-
-

-vcon.party

- - - - - - -
 1from typing import Optional
- 2from vcon.civic_address import CivicAddress
- 3from datetime import datetime
- 4
- 5
- 6class Party:
- 7    def __init__(self,
- 8                 tel: Optional[str] = None,
- 9                 stir: Optional[str] = None,
-10                 mailto: Optional[str] = None,
-11                 name: Optional[str] = None,
-12                 validation: Optional[str] = None,
-13                 gmlpos: Optional[str] = None,
-14                 civicaddress: Optional[CivicAddress] = None,
-15                 uuid: Optional[str] = None,
-16                 role: Optional[str] = None,
-17                 contact_list: Optional[str] = None,
-18                 meta: Optional[dict] = None) -> None:
-19        """
-20        Initialize a new Party object.
-21
-22        :param tel: Telephone number of the party
-23        :type tel: str | None
-24        :param stir: STIR identifier of the party
-25        :type stir: str | None
-26        :param mailto: Email address of the party
-27        :type mailto: str | None
-28        :param name: Display name of the party
-29        :type name: str | None
-30        :param validation: Validation information of the party
-31        :type validation: str | None
-32        :param gmlpos: GML position of the party
-33        :type gmlpos: str | None
-34        :param civicaddress: Civic address of the party
-35        :type civicaddress: CivicAddress | None
-36        :param uuid: UUID of the party
-37        :type uuid: str | None
-38        :param role: Role of the party
-39        :type role: str | None
-40        :param contact_list: Contact list of the party
-41        :type contact_list: str | None
-42        """
-43        # copy the values that are not None
-44        # TODO: should we allow changing the values of the object?
-45        #       for now, we just use the values that are not None
-46        #       and ignore the other values
-47        #       (this is also how the old code worked)
-48        for key, value in locals().items():
-49            if value is not None:
-50                setattr(self, key, value)
-51
-52    def to_dict(self):
-53        # copy the attributes that are not None
-54        # TODO: should we allow changing the values of the object?
-55        #       for now, we just use the values that are not None
-56        #       and ignore the other values
-57        #       (this is also how the old code worked)
-58        party_dict = {}
-59        for key, value in self.__dict__.items():
-60            # Don't include self in the dict
-61            if value is not None and key != "self":
-62                party_dict[key] = value
-63        return party_dict
-64
-65
-66class PartyHistory:
-67    def __init__(self, party: int, event: str, time: datetime):
-68        """
-69        Initialize a new PartyHistory object.
-70
-71        :param party: Index of the party
-72        :type party: int
-73        :param event: Event type (e.g. "join", "leave")
-74        :type event: str
-75        :param time: Time of the event
-76        :type time: datetime
-77        """
-78        self.party = party
-79        self.event = event
-80        self.time = time
-81        
-82    def to_dict(self):
-83        return {
-84            "party": self.party,
-85            "event": self.event,
-86            "time": self.time
-87        }
-
- - -
-
- -
- - class - Party: - - - -
- -
 7class Party:
- 8    def __init__(self,
- 9                 tel: Optional[str] = None,
-10                 stir: Optional[str] = None,
-11                 mailto: Optional[str] = None,
-12                 name: Optional[str] = None,
-13                 validation: Optional[str] = None,
-14                 gmlpos: Optional[str] = None,
-15                 civicaddress: Optional[CivicAddress] = None,
-16                 uuid: Optional[str] = None,
-17                 role: Optional[str] = None,
-18                 contact_list: Optional[str] = None,
-19                 meta: Optional[dict] = None) -> None:
-20        """
-21        Initialize a new Party object.
-22
-23        :param tel: Telephone number of the party
-24        :type tel: str | None
-25        :param stir: STIR identifier of the party
-26        :type stir: str | None
-27        :param mailto: Email address of the party
-28        :type mailto: str | None
-29        :param name: Display name of the party
-30        :type name: str | None
-31        :param validation: Validation information of the party
-32        :type validation: str | None
-33        :param gmlpos: GML position of the party
-34        :type gmlpos: str | None
-35        :param civicaddress: Civic address of the party
-36        :type civicaddress: CivicAddress | None
-37        :param uuid: UUID of the party
-38        :type uuid: str | None
-39        :param role: Role of the party
-40        :type role: str | None
-41        :param contact_list: Contact list of the party
-42        :type contact_list: str | None
-43        """
-44        # copy the values that are not None
-45        # TODO: should we allow changing the values of the object?
-46        #       for now, we just use the values that are not None
-47        #       and ignore the other values
-48        #       (this is also how the old code worked)
-49        for key, value in locals().items():
-50            if value is not None:
-51                setattr(self, key, value)
-52
-53    def to_dict(self):
-54        # copy the attributes that are not None
-55        # TODO: should we allow changing the values of the object?
-56        #       for now, we just use the values that are not None
-57        #       and ignore the other values
-58        #       (this is also how the old code worked)
-59        party_dict = {}
-60        for key, value in self.__dict__.items():
-61            # Don't include self in the dict
-62            if value is not None and key != "self":
-63                party_dict[key] = value
-64        return party_dict
-
- - - - -
- -
- - Party( tel: Optional[str] = None, stir: Optional[str] = None, mailto: Optional[str] = None, name: Optional[str] = None, validation: Optional[str] = None, gmlpos: Optional[str] = None, civicaddress: Optional[vcon.civic_address.CivicAddress] = None, uuid: Optional[str] = None, role: Optional[str] = None, contact_list: Optional[str] = None, meta: Optional[dict] = None) - - - -
- -
 8    def __init__(self,
- 9                 tel: Optional[str] = None,
-10                 stir: Optional[str] = None,
-11                 mailto: Optional[str] = None,
-12                 name: Optional[str] = None,
-13                 validation: Optional[str] = None,
-14                 gmlpos: Optional[str] = None,
-15                 civicaddress: Optional[CivicAddress] = None,
-16                 uuid: Optional[str] = None,
-17                 role: Optional[str] = None,
-18                 contact_list: Optional[str] = None,
-19                 meta: Optional[dict] = None) -> None:
-20        """
-21        Initialize a new Party object.
-22
-23        :param tel: Telephone number of the party
-24        :type tel: str | None
-25        :param stir: STIR identifier of the party
-26        :type stir: str | None
-27        :param mailto: Email address of the party
-28        :type mailto: str | None
-29        :param name: Display name of the party
-30        :type name: str | None
-31        :param validation: Validation information of the party
-32        :type validation: str | None
-33        :param gmlpos: GML position of the party
-34        :type gmlpos: str | None
-35        :param civicaddress: Civic address of the party
-36        :type civicaddress: CivicAddress | None
-37        :param uuid: UUID of the party
-38        :type uuid: str | None
-39        :param role: Role of the party
-40        :type role: str | None
-41        :param contact_list: Contact list of the party
-42        :type contact_list: str | None
-43        """
-44        # copy the values that are not None
-45        # TODO: should we allow changing the values of the object?
-46        #       for now, we just use the values that are not None
-47        #       and ignore the other values
-48        #       (this is also how the old code worked)
-49        for key, value in locals().items():
-50            if value is not None:
-51                setattr(self, key, value)
-
- - -

Initialize a new Party object.

- -
Parameters
- -
    -
  • tel: Telephone number of the party
  • -
  • stir: STIR identifier of the party
  • -
  • mailto: Email address of the party
  • -
  • name: Display name of the party
  • -
  • validation: Validation information of the party
  • -
  • gmlpos: GML position of the party
  • -
  • civicaddress: Civic address of the party
  • -
  • uuid: UUID of the party
  • -
  • role: Role of the party
  • -
  • contact_list: Contact list of the party
  • -
-
- - -
-
- -
- - def - to_dict(self): - - - -
- -
53    def to_dict(self):
-54        # copy the attributes that are not None
-55        # TODO: should we allow changing the values of the object?
-56        #       for now, we just use the values that are not None
-57        #       and ignore the other values
-58        #       (this is also how the old code worked)
-59        party_dict = {}
-60        for key, value in self.__dict__.items():
-61            # Don't include self in the dict
-62            if value is not None and key != "self":
-63                party_dict[key] = value
-64        return party_dict
-
- - - - -
-
-
- -
- - class - PartyHistory: - - - -
- -
67class PartyHistory:
-68    def __init__(self, party: int, event: str, time: datetime):
-69        """
-70        Initialize a new PartyHistory object.
-71
-72        :param party: Index of the party
-73        :type party: int
-74        :param event: Event type (e.g. "join", "leave")
-75        :type event: str
-76        :param time: Time of the event
-77        :type time: datetime
-78        """
-79        self.party = party
-80        self.event = event
-81        self.time = time
-82        
-83    def to_dict(self):
-84        return {
-85            "party": self.party,
-86            "event": self.event,
-87            "time": self.time
-88        }
-
- - - - -
- -
- - PartyHistory(party: int, event: str, time: datetime.datetime) - - - -
- -
68    def __init__(self, party: int, event: str, time: datetime):
-69        """
-70        Initialize a new PartyHistory object.
-71
-72        :param party: Index of the party
-73        :type party: int
-74        :param event: Event type (e.g. "join", "leave")
-75        :type event: str
-76        :param time: Time of the event
-77        :type time: datetime
-78        """
-79        self.party = party
-80        self.event = event
-81        self.time = time
-
- - -

Initialize a new PartyHistory object.

- -
Parameters
- -
    -
  • party: Index of the party
  • -
  • event: Event type (e.g. "join", "leave")
  • -
  • time: Time of the event
  • -
-
- - -
-
-
- party - - -
- - - - -
-
-
- event - - -
- - - - -
-
-
- time - - -
- - - - -
-
- -
- - def - to_dict(self): - - - -
- -
83    def to_dict(self):
-84        return {
-85            "party": self.party,
-86            "event": self.event,
-87            "time": self.time
-88        }
-
- - - - -
-
-
- - \ No newline at end of file diff --git a/samples/example.vcon.json b/samples/example.vcon.json index 30e4688..7bfacf0 100644 --- a/samples/example.vcon.json +++ b/samples/example.vcon.json @@ -1,6 +1,5 @@ { "uuid": "0194a44f-23be-8439-9dd8-dd37220d739c", - "vcon": "0.3.0", "created_at": "2025-01-26T20:30:37.502276+00:00", "redacted": {}, "group": [], diff --git a/samples/example_new_fields.vcon.json b/samples/example_new_fields.vcon.json index 3d4f9fa..7a24b05 100644 --- a/samples/example_new_fields.vcon.json +++ b/samples/example_new_fields.vcon.json @@ -1 +1,56 @@ -{"uuid": "019822fb-309a-8716-9dd8-dd37220d739c", "vcon": "0.3.0", "redacted": {}, "group": [], "parties": [{"tel": "+1234567890", "name": "John Doe", "sip": "sip:john@example.com", "did": "did:example:123456789abcdef", "jCard": {"fn": "John Doe", "tel": "+1234567890", "email": "john@example.com"}, "timezone": "America/New_York"}, {"tel": "+0987654321", "name": "Jane Smith", "sip": "sip:jane@example.com", "did": "did:example:abcdef123456789", "jCard": {"fn": "Jane Smith", "tel": "+0987654321", "email": "jane@example.com"}, "timezone": "Europe/London"}], "dialog": [{"type": "text", "start": "2025-07-19T15:59:04.090841", "parties": [0, 1], "body": "Hello, this is a test message!", "session_id": "session-12345", "content_hash": "c8d3d67f662a787e96e74ccb0a77803138c0f13495a186ccbde495c57c385608", "metadata": {}, "meta": {}}], "attachments": [], "analysis": [], "created_at": "2025-07-19T13:59:04.090462+00:00", "extensions": ["video", "encryption"], "must_support": ["encryption"]} \ No newline at end of file +{ + "uuid": "019822fb-309a-8716-9dd8-dd37220d739c", + "redacted": {}, + "group": [], + "parties": [ + { + "tel": "+1234567890", + "name": "John Doe", + "sip": "sip:john@example.com", + "did": "did:example:123456789abcdef", + "jCard": { + "fn": "John Doe", + "tel": "+1234567890", + "email": "john@example.com" + }, + "timezone": "America/New_York" + }, + { + "tel": "+0987654321", + "name": "Jane Smith", + "sip": "sip:jane@example.com", + "did": "did:example:abcdef123456789", + "jCard": { + "fn": "Jane Smith", + "tel": "+0987654321", + "email": "jane@example.com" + }, + "timezone": "Europe/London" + } + ], + "dialog": [ + { + "type": "text", + "start": "2025-07-19T15:59:04.090841", + "parties": [ + 0, + 1 + ], + "body": "Hello, this is a test message!", + "session_id": "session-12345", + "content_hash": "c8d3d67f662a787e96e74ccb0a77803138c0f13495a186ccbde495c57c385608", + "metadata": {}, + "meta": {} + } + ], + "attachments": [], + "analysis": [], + "created_at": "2025-07-19T13:59:04.090462+00:00", + "extensions": [ + "video", + "encryption" + ], + "must_support": [ + "encryption" + ] +} \ No newline at end of file diff --git a/samples/output.vcon.json b/samples/output.vcon.json index e1731d4..d70bdcc 100644 --- a/samples/output.vcon.json +++ b/samples/output.vcon.json @@ -1,6 +1,5 @@ { "uuid": "0194a43e-2a23-8697-9dd8-dd37220d739c", - "vcon": "0.3.0", "created_at": "2025-01-26T20:12:05.027425+00:00", "redacted": {}, "group": [], diff --git a/samples/packed.vcon.json b/samples/packed.vcon.json index 04d263f..ffa78a3 100644 --- a/samples/packed.vcon.json +++ b/samples/packed.vcon.json @@ -1,6 +1,5 @@ { "uuid": "0194a447-5769-8add-9dd8-dd37220d739c", - "vcon": "0.3.0", "created_at": "2025-01-26T20:22:06.441691+00:00", "redacted": {}, "group": [], diff --git a/src/vcon/vcon.py b/src/vcon/vcon.py index 7f4eeeb..a023495 100644 --- a/src/vcon/vcon.py +++ b/src/vcon/vcon.py @@ -183,7 +183,7 @@ class Vcon: The vCon format supports features such as: - Unique identification via UUID - - Versioning (currently supports version "0.3.0") + - Optional versioning (version field is optional) - Timestamps for creation and updates - Party information with contact details (tel, name, sip, did, jCard, timezone) - Dialog content with media types and session information @@ -212,7 +212,6 @@ def __init__( self, vcon_dict: Dict[str, Any] = None, property_handling: str = PROPERTY_HANDLING_DEFAULT, - strict_version: bool = False, ) -> None: """ Initialize a Vcon object from a dictionary. @@ -224,12 +223,11 @@ def __init__( - "default": Keep non-standard properties (default) - "strict": Remove non-standard properties - "meta": Move non-standard properties to meta object - strict_version: If True, reject vCons not at version "0.3.0". If False (default), migrate them. Example: - >>> vcon = Vcon({"uuid": "123", "vcon": "0.3.0", "custom_field": "value"}) - >>> strict_vcon = Vcon({"uuid": "123", "vcon": "0.3.0", "custom_field": "value"}, property_handling="strict") - >>> meta_vcon = Vcon({"uuid": "123", "vcon": "0.3.0", "custom_field": "value"}, property_handling="meta") + >>> vcon = Vcon({"uuid": "123", "custom_field": "value"}) + >>> strict_vcon = Vcon({"uuid": "123", "custom_field": "value"}, property_handling="strict") + >>> meta_vcon = Vcon({"uuid": "123", "custom_field": "value"}, property_handling="meta") """ logger.debug("Initializing new Vcon object") @@ -240,19 +238,7 @@ def __init__( if vcon_dict is None: vcon_dict = {} - # Version check and migration - vcon_version = vcon_dict.get("vcon") - if vcon_version is not None and vcon_version != "0.3.0": - if strict_version: - logger.error(f"Strict version mode: vCon version {vcon_version} is not supported.") - raise ValueError(f"vCon version {vcon_version} is not supported in strict mode.") - else: - logger.info(f"Migrating vCon from version {vcon_version} to 0.3.0") - vcon_dict["vcon"] = "0.3.0" - elif vcon_version is None: - # Set default version if missing - vcon_dict["vcon"] = "0.3.0" - logger.debug("Set default vCon version to 0.3.0") + # Version field is now optional - no version management required # Handle created_at if vcon_dict.get("created_at"): @@ -380,7 +366,6 @@ def build_from_json( cls, json_string: str, property_handling: str = PROPERTY_HANDLING_DEFAULT, - strict_version: bool = False, ) -> "Vcon": """ Initialize a Vcon object from a JSON string. @@ -391,17 +376,15 @@ def build_from_json( Args: json_string: A JSON string representing a vCon property_handling: How to handle non-standard properties. - strict_version: If True, reject vCons not at version "0.3.0". If False (default), migrate them. Returns: A new Vcon object initialized with the parsed JSON data Raises: json.JSONDecodeError: If the JSON string is invalid - ValueError: If strict_version is True and the vCon version is not "0.3.0" Example: - >>> json_str = '{"uuid": "123", "vcon": "0.3.0"}' + >>> json_str = '{"uuid": "123"}' >>> vcon = Vcon.build_from_json(json_str) """ logger.debug("Building Vcon from JSON string") @@ -411,7 +394,6 @@ def build_from_json( return cls( vcon_dict, property_handling=property_handling, - strict_version=strict_version, ) except json.JSONDecodeError as e: logger.error(f"Failed to parse JSON string: {str(e)}") @@ -422,14 +404,12 @@ def build_new( cls, created_at: Union[str, datetime] = None, property_handling: str = PROPERTY_HANDLING_DEFAULT, - strict_version: bool = False, ) -> "Vcon": """ Initialize a new Vcon object with default values. - This method creates a new vCon with a generated UUID, default version, - and initialized with empty arrays for groups, parties, dialog, attachments, - and analysis. + This method creates a new vCon with a generated UUID and initialized + with empty arrays for groups, parties, dialog, attachments, and analysis. Args: created_at (Union[str, datetime], optional): The timestamp to set for creation, @@ -437,7 +417,6 @@ def build_new( or as a datetime object. Defaults to current time. property_handling: How to handle non-standard properties. - strict_version: If True, reject vCons not at version "0.3.0". If False (default), migrate them. Returns: A new Vcon object with default values @@ -452,7 +431,6 @@ def build_new( vcon_dict = { "uuid": uuid, - "vcon": "0.3.0", "redacted": {}, "group": [], "parties": [], @@ -465,7 +443,6 @@ def build_new( vcon = cls( vcon_dict, property_handling=property_handling, - strict_version=strict_version, ) # Set created_at if provided, otherwise it will use the default from __init__ @@ -1240,8 +1217,8 @@ def uuid(self) -> str: return self.vcon_dict["uuid"] @property - def vcon(self) -> str: - return self.vcon_dict["vcon"] + def vcon(self) -> Optional[str]: + return self.vcon_dict.get("vcon") @property def subject(self) -> Optional[str]: @@ -1440,7 +1417,7 @@ def is_valid(self) -> Tuple[bool, List[str]]: logger.debug("Validating vCon") errors = [] # Validate required fields. - required_fields = ["uuid", "vcon", "created_at"] + required_fields = ["uuid", "created_at"] for field in required_fields: if field not in self.vcon_dict: error = f"Missing required field: {field}" @@ -1618,7 +1595,6 @@ def load( cls, source: str, property_handling: str = PROPERTY_HANDLING_DEFAULT, - strict_version: bool = False, ) -> "Vcon": """ Load a vCon from either a file path or URL. @@ -1627,7 +1603,6 @@ def load( source: File path or URL to load the vCon from property_handling: How to handle non-standard properties. Options are "default", "strict", or "meta" - strict_version: If True, reject vCons not at version "0.3.0". If False (default), migrate them. Returns: A Vcon object @@ -1641,13 +1616,11 @@ def load( return cls.load_from_url( source, property_handling=property_handling, - strict_version=strict_version, ) else: return cls.load_from_file( source, property_handling=property_handling, - strict_version=strict_version, ) @classmethod @@ -1655,7 +1628,6 @@ def load_from_file( cls, file_path: str, property_handling: str = PROPERTY_HANDLING_DEFAULT, - strict_version: bool = False, ) -> "Vcon": """ Load a vCon from a file. @@ -1664,7 +1636,6 @@ def load_from_file( file_path: Path to the vCon JSON file property_handling: How to handle non-standard properties. Options are "default", "strict", or "meta" - strict_version: If True, reject vCons not at version "0.3.0". If False (default), migrate them. Returns: A Vcon object @@ -1672,7 +1643,6 @@ def load_from_file( Raises: FileNotFoundError: If the file does not exist json.JSONDecodeError: If the file contains invalid JSON - ValueError: If strict_version is True and the vCon version is not "0.3.0" """ try: with open(file_path, 'r') as f: @@ -1680,7 +1650,6 @@ def load_from_file( return cls.build_from_json( json_str, property_handling=property_handling, - strict_version=strict_version, ) except FileNotFoundError: raise FileNotFoundError(f"vCon file not found: {file_path}") @@ -1692,7 +1661,6 @@ def load_from_url( cls, url: str, property_handling: str = PROPERTY_HANDLING_DEFAULT, - strict_version: bool = False, ) -> "Vcon": """ Load a vCon from a URL. @@ -1701,7 +1669,6 @@ def load_from_url( url: URL to fetch the vCon JSON from property_handling: How to handle non-standard properties. Options are "default", "strict", or "meta" - strict_version: If True, reject vCons not at version "0.3.0". If False (default), migrate them. Returns: A Vcon object @@ -1709,14 +1676,12 @@ def load_from_url( Raises: requests.RequestException: If there is an error fetching from URL json.JSONDecodeError: If the response contains invalid JSON - ValueError: If strict_version is True and the vCon version is not "0.3.0" """ response = requests.get(url) response.raise_for_status() # Raise an exception for bad status codes return cls.build_from_json( response.text, property_handling=property_handling, - strict_version=strict_version, ) def save_to_file(self, file_path: str) -> None: diff --git a/tests/test_vcon.py b/tests/test_vcon.py index d37c967..52078a3 100644 --- a/tests/test_vcon.py +++ b/tests/test_vcon.py @@ -159,7 +159,7 @@ def test_build_from_json() -> None: def test_build_new() -> None: vcon = Vcon.build_new() assert vcon.uuid is not None - assert vcon.vcon == "0.3.0" + assert vcon.vcon is None # vcon field is now optional and not set by default assert vcon.created_at is not None @@ -570,9 +570,8 @@ def test_is_valid_with_missing_required_fields(): vcon.vcon_dict = {} # Empty vCon is_valid, errors = vcon.is_valid() assert not is_valid - assert len(errors) == 3 # uuid, vcon, created_at + assert len(errors) == 2 # uuid, created_at (vcon field is now optional) assert "Missing required field: uuid" in errors - assert "Missing required field: vcon" in errors assert "Missing required field: created_at" in errors @@ -767,9 +766,9 @@ def test_load_detects_file_vs_url() -> None: try: # Replace methods with mocks as class methods so they bind correctly - # Updated to accept all parameters including strict_version - Vcon.load_from_file = classmethod(lambda cls, path, property_handling=None, strict_version=False: path) - Vcon.load_from_url = classmethod(lambda cls, url, property_handling=None, strict_version=False: url) + # Updated to accept all parameters + Vcon.load_from_file = classmethod(lambda cls, path, property_handling=None: path) + Vcon.load_from_url = classmethod(lambda cls, url, property_handling=None: url) # Test file path result = Vcon.load(file_path) @@ -1302,74 +1301,88 @@ def test_integration_basic_video_workflow(): assert new_vcon.dialog[1]["type"] == "video" assert new_vcon.dialog[1]["mimetype"] == "video/mp4" -def test_version_migration_default() -> None: - """Test that vCons with older versions are automatically migrated by default.""" - old_vcon_json = '{"uuid":"123","vcon":"0.0.1","created_at":"2024-01-01T00:00:00Z"}' - vcon = Vcon.build_from_json(old_vcon_json) - assert vcon.vcon == "0.3.0" +def test_version_field_optional() -> None: + """Test that vCons can be created without version field.""" + vcon_json = '{"uuid":"123","created_at":"2024-01-01T00:00:00Z"}' + vcon = Vcon.build_from_json(vcon_json) + # Version field should not be automatically added + assert "vcon" not in vcon.vcon_dict -def test_version_migration_strict_mode_rejects() -> None: - """Test that strict mode rejects vCons with older versions.""" - old_vcon_json = '{"uuid":"123","vcon":"0.0.1","created_at":"2024-01-01T00:00:00Z"}' - with pytest.raises(ValueError, match="vCon version 0.0.1 is not supported in strict mode"): - Vcon.build_from_json(old_vcon_json, strict_version=True) +def test_version_field_preserved_when_present() -> None: + """Test that version field is preserved when present.""" + vcon_json = '{"uuid":"123","vcon":"0.0.1","created_at":"2024-01-01T00:00:00Z"}' + vcon = Vcon.build_from_json(vcon_json) + # Version field should be preserved as-is + assert vcon.vcon == "0.0.1" -def test_version_migration_strict_mode_accepts_current() -> None: - """Test that strict mode accepts vCons with current version.""" - current_vcon_json = '{"uuid":"123","vcon":"0.3.0","created_at":"2024-01-01T00:00:00Z"}' - vcon = Vcon.build_from_json(current_vcon_json, strict_version=True) - assert vcon.vcon == "0.3.0" +def test_version_field_optional_in_constructor() -> None: + """Test that version field is optional in constructor.""" + vcon_dict = {"uuid": "123", "created_at": "2024-01-01T00:00:00Z"} + vcon = Vcon(vcon_dict) + # Version field should not be automatically added + assert "vcon" not in vcon.vcon_dict -def test_version_migration_constructor() -> None: - """Test that the constructor handles version migration.""" - old_vcon_dict = {"uuid": "123", "vcon": "0.0.1", "created_at": "2024-01-01T00:00:00Z"} - vcon = Vcon(old_vcon_dict) - assert vcon.vcon == "0.3.0" +def test_version_field_preserved_in_constructor() -> None: + """Test that version field is preserved in constructor.""" + vcon_dict = {"uuid": "123", "vcon": "0.0.1", "created_at": "2024-01-01T00:00:00Z"} + vcon = Vcon(vcon_dict) + # Version field should be preserved as-is + assert vcon.vcon == "0.0.1" -def test_version_migration_constructor_strict() -> None: - """Test that the constructor rejects old versions in strict mode.""" - old_vcon_dict = {"uuid": "123", "vcon": "0.0.1", "created_at": "2024-01-01T00:00:00Z"} - with pytest.raises(ValueError, match="vCon version 0.0.1 is not supported in strict mode"): - Vcon(old_vcon_dict, strict_version=True) +def test_version_field_optional_with_property_handling() -> None: + """Test that version field is optional with different property handling modes.""" + vcon_dict = {"uuid": "123", "created_at": "2024-01-01T00:00:00Z"} + + # Test with default property handling + vcon_default = Vcon(vcon_dict, property_handling="default") + assert "vcon" not in vcon_default.vcon_dict + + # Test with strict property handling + vcon_strict = Vcon(vcon_dict, property_handling="strict") + assert "vcon" not in vcon_strict.vcon_dict -def test_version_migration_load_from_file(tmp_path) -> None: - """Test that load_from_file handles version migration.""" - old_vcon_json = '{"uuid":"123","vcon":"0.0.1","created_at":"2024-01-01T00:00:00Z"}' - file_path = tmp_path / "old_vcon.json" +def test_version_field_preserved_load_from_file(tmp_path) -> None: + """Test that load_from_file preserves version field when present.""" + vcon_json = '{"uuid":"123","vcon":"0.0.1","created_at":"2024-01-01T00:00:00Z"}' + file_path = tmp_path / "vcon.json" with open(file_path, 'w') as f: - f.write(old_vcon_json) + f.write(vcon_json) vcon = Vcon.load_from_file(str(file_path)) - assert vcon.vcon == "0.3.0" + # Version field should be preserved as-is + assert vcon.vcon == "0.0.1" -def test_version_migration_load_from_file_strict(tmp_path) -> None: - """Test that load_from_file rejects old versions in strict mode.""" - old_vcon_json = '{"uuid":"123","vcon":"0.0.1","created_at":"2024-01-01T00:00:00Z"}' - file_path = tmp_path / "old_vcon.json" +def test_version_field_optional_load_from_file(tmp_path) -> None: + """Test that load_from_file works without version field.""" + vcon_json = '{"uuid":"123","created_at":"2024-01-01T00:00:00Z"}' + file_path = tmp_path / "vcon.json" with open(file_path, 'w') as f: - f.write(old_vcon_json) + f.write(vcon_json) - with pytest.raises(ValueError, match="vCon version 0.0.1 is not supported in strict mode"): - Vcon.load_from_file(str(file_path), strict_version=True) + vcon = Vcon.load_from_file(str(file_path)) + # Version field should not be automatically added + assert "vcon" not in vcon.vcon_dict -def test_version_migration_build_new() -> None: - """Test that build_new creates vCons with current version.""" +def test_build_new_no_version_field() -> None: + """Test that build_new creates vCons without version field.""" vcon = Vcon.build_new() - assert vcon.vcon == "0.3.0" + # Version field should not be automatically added + assert "vcon" not in vcon.vcon_dict -def test_version_migration_no_version_field() -> None: - """Test that vCons without version field get version 0.3.0.""" +def test_no_version_field_remains_absent() -> None: + """Test that vCons without version field remain without version field.""" vcon_dict = {"uuid": "123", "created_at": "2023-01-01T00:00:00Z"} vcon = Vcon(vcon_dict) - assert vcon.vcon == "0.3.0" + # Version field should not be automatically added + assert "vcon" not in vcon.vcon_dict def test_extensions_management():