From 71172a3c562c304d5da0cfdb66bafaabe847cc0f Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Mon, 16 Mar 2026 16:44:56 -0700 Subject: [PATCH 1/4] feat: add data_purposes field to Dataset, DatasetCollection, and DatasetField Add data_purposes as an optional field at all levels of the dataset hierarchy, mirroring the existing data_categories pattern. This enables purpose-based access control (PBAC) by declaring which data purposes are allowed for each dataset, collection, field, and sub-field. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/fideslang/models.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/fideslang/models.py b/src/fideslang/models.py index e687bea..f0e45db 100644 --- a/src/fideslang/models.py +++ b/src/fideslang/models.py @@ -3,6 +3,7 @@ """ Contains all of the Fides resources modeled as Pydantic models. """ + from __future__ import annotations from datetime import datetime @@ -309,9 +310,9 @@ def include_exclude_has_values(self) -> "DataSubjectRights": """ strategy, rights = self.strategy, self.values if strategy in ("INCLUDE", "EXCLUDE"): - assert ( - rights is not None - ), f"If {strategy} is chosen, rights must also be listed." + assert rights is not None, ( + f"If {strategy} is chosen, rights must also be listed." + ) return self @@ -365,6 +366,10 @@ class MyDatasetField(DatasetFieldBase): default=None, description="Arrays of Data Categories, identified by `fides_key`, that applies to this field.", ) + data_purposes: Optional[List[FidesKey]] = Field( + default=None, + description="Array of Data Purpose resources, identified by `fides_key`, that apply to this field.", + ) class EdgeDirection(str, Enum): @@ -555,6 +560,10 @@ class DatasetCollection(FidesopsMetaBackwardsCompat): default=None, description="Array of Data Category resources identified by `fides_key`, that apply to all fields in the collection.", ) + data_purposes: Optional[List[FidesKey]] = Field( + default=None, + description="Array of Data Purpose resources, identified by `fides_key`, that apply to all fields in the collection.", + ) fields: List[DatasetField] = Field( description="An array of objects that describe the collection's fields.", ) @@ -618,6 +627,10 @@ class Dataset(FidesModel, FidesopsMetaBackwardsCompat): default=None, description="Array of Data Category resources identified by `fides_key`, that apply to all collections in the Dataset.", ) + data_purposes: Optional[List[FidesKey]] = Field( + default=None, + description="Array of Data Purpose resources, identified by `fides_key`, that apply to all collections in the Dataset.", + ) fides_meta: Optional[DatasetMetadata] = Field( description=DatasetMetadata.__doc__, default=None ) @@ -976,9 +989,9 @@ def user_special_case(self) -> "DataFlow": """ if self.fides_key == "user" or self.type == "user": - assert ( - self.fides_key == "user" and self.type == "user" - ), "The 'user' fides_key is required for, and requires, the type 'user'" + assert self.fides_key == "user" and self.type == "user", ( + "The 'user' fides_key is required for, and requires, the type 'user'" + ) return self @@ -1146,23 +1159,23 @@ def privacy_declarations_reference_data_flows( only reference the `fides_key`s of defined `DataFlow`s in said field(s). """ privacy_declarations: List[PrivacyDeclaration] = self.privacy_declarations or [] - for ( - privacy_declaration - ) in privacy_declarations: # pylint:disable=not-an-iterable + for privacy_declaration in privacy_declarations: # pylint:disable=not-an-iterable for direction in ["egress", "ingress"]: fides_keys = getattr(privacy_declaration, direction, None) if fides_keys is not None: data_flows = getattr(self, direction) system = self.fides_key - assert ( - data_flows is not None and len(data_flows) > 0 - ), f"PrivacyDeclaration '{privacy_declaration.name}' defines {direction} with one or more resources and is applied to the System '{system}', which does not itself define any {direction}." + assert data_flows is not None and len(data_flows) > 0, ( + f"PrivacyDeclaration '{privacy_declaration.name}' defines {direction} with one or more resources and is applied to the System '{system}', which does not itself define any {direction}." + ) for fides_key in fides_keys: assert fides_key in [ data_flow.fides_key for data_flow in data_flows # pylint:disable=not-an-iterable - ], f"PrivacyDeclaration '{privacy_declaration.name}' defines {direction} with '{fides_key}' and is applied to the System '{system}', which does not itself define {direction} with that resource." + ], ( + f"PrivacyDeclaration '{privacy_declaration.name}' defines {direction} with '{fides_key}' and is applied to the System '{system}', which does not itself define {direction} with that resource." + ) return self From 22bc6b7c9c28a1cff1408effe8d6af52dac08d7c Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Mon, 16 Mar 2026 16:50:47 -0700 Subject: [PATCH 2/4] style: reformat with black (CI formatter) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/fideslang/models.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/fideslang/models.py b/src/fideslang/models.py index f0e45db..bb4209f 100644 --- a/src/fideslang/models.py +++ b/src/fideslang/models.py @@ -310,9 +310,9 @@ def include_exclude_has_values(self) -> "DataSubjectRights": """ strategy, rights = self.strategy, self.values if strategy in ("INCLUDE", "EXCLUDE"): - assert rights is not None, ( - f"If {strategy} is chosen, rights must also be listed." - ) + assert ( + rights is not None + ), f"If {strategy} is chosen, rights must also be listed." return self @@ -989,9 +989,9 @@ def user_special_case(self) -> "DataFlow": """ if self.fides_key == "user" or self.type == "user": - assert self.fides_key == "user" and self.type == "user", ( - "The 'user' fides_key is required for, and requires, the type 'user'" - ) + assert ( + self.fides_key == "user" and self.type == "user" + ), "The 'user' fides_key is required for, and requires, the type 'user'" return self @@ -1159,23 +1159,23 @@ def privacy_declarations_reference_data_flows( only reference the `fides_key`s of defined `DataFlow`s in said field(s). """ privacy_declarations: List[PrivacyDeclaration] = self.privacy_declarations or [] - for privacy_declaration in privacy_declarations: # pylint:disable=not-an-iterable + for ( + privacy_declaration + ) in privacy_declarations: # pylint:disable=not-an-iterable for direction in ["egress", "ingress"]: fides_keys = getattr(privacy_declaration, direction, None) if fides_keys is not None: data_flows = getattr(self, direction) system = self.fides_key - assert data_flows is not None and len(data_flows) > 0, ( - f"PrivacyDeclaration '{privacy_declaration.name}' defines {direction} with one or more resources and is applied to the System '{system}', which does not itself define any {direction}." - ) + assert ( + data_flows is not None and len(data_flows) > 0 + ), f"PrivacyDeclaration '{privacy_declaration.name}' defines {direction} with one or more resources and is applied to the System '{system}', which does not itself define any {direction}." for fides_key in fides_keys: assert fides_key in [ data_flow.fides_key for data_flow in data_flows # pylint:disable=not-an-iterable - ], ( - f"PrivacyDeclaration '{privacy_declaration.name}' defines {direction} with '{fides_key}' and is applied to the System '{system}', which does not itself define {direction} with that resource." - ) + ], f"PrivacyDeclaration '{privacy_declaration.name}' defines {direction} with '{fides_key}' and is applied to the System '{system}', which does not itself define {direction} with that resource." return self From 71b5a4eab196e36679ebb514d6a3a5f1f2c5c7ea Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Mon, 23 Mar 2026 09:13:14 -0700 Subject: [PATCH 3/4] fix: resolve mypy errors from pydantic dependency drift Suppress mypy errors caused by newer pydantic versions resolving in CI: - Add type: ignore[misc] for ValidationInfo explicit Any warnings - Remove stale type: ignore[assignment] comments no longer needed - Add type: ignore[arg-type] for Optional list default_factory Co-Authored-By: Claude Opus 4.6 (1M context) --- src/fideslang/models.py | 24 ++++++++++++------------ src/fideslang/validation.py | 10 +++++----- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/fideslang/models.py b/src/fideslang/models.py index bb4209f..c9af9ce 100644 --- a/src/fideslang/models.py +++ b/src/fideslang/models.py @@ -484,7 +484,7 @@ def valid_meta(cls, meta_values: Optional[FidesMeta]) -> Optional[FidesMeta]: return meta_values @model_validator(mode="after") - def validate_object_fields( + def validate_object_fields( # type: ignore[misc] self, _: ValidationInfo, ) -> DatasetField: @@ -570,8 +570,8 @@ class DatasetCollection(FidesopsMetaBackwardsCompat): fides_meta: Optional[CollectionMeta] = None - _sort_fields: classmethod = field_validator("fields")(sort_list_objects_by_name) # type: ignore[assignment] - _unique_items_in_list: classmethod = field_validator("fields")(unique_items_in_list) # type: ignore[assignment] + _sort_fields: classmethod = field_validator("fields")(sort_list_objects_by_name) + _unique_items_in_list: classmethod = field_validator("fields")(unique_items_in_list) class ContactDetails(BaseModel): @@ -638,10 +638,10 @@ class Dataset(FidesModel, FidesopsMetaBackwardsCompat): description="An array of objects that describe the Dataset's collections.", ) - _sort_collections: classmethod = field_validator("collections")( # type: ignore[assignment] + _sort_collections: classmethod = field_validator("collections")( sort_list_objects_by_name ) - _unique_items_in_list: classmethod = field_validator("collections")( # type: ignore[assignment] + _unique_items_in_list: classmethod = field_validator("collections")( unique_items_in_list ) @@ -820,7 +820,7 @@ class Policy(FidesModel): description=PolicyRule.__doc__, ) - _sort_rules: classmethod = field_validator("rules")(sort_list_objects_by_name) # type: ignore[assignment] + _sort_rules: classmethod = field_validator("rules")(sort_list_objects_by_name) def validate_deprecated_cookies(values: Dict[str, Any] | Any) -> None: # type: ignore[misc] @@ -1146,7 +1146,7 @@ def validate_cookies(cls, values: Dict[str, Any] | Any) -> Dict[str, Any]: # ty validate_deprecated_cookies(values) return values - _sort_privacy_declarations: classmethod = field_validator("privacy_declarations")( # type: ignore[assignment] + _sort_privacy_declarations: classmethod = field_validator("privacy_declarations")( sort_list_objects_by_name ) @@ -1193,11 +1193,11 @@ class Taxonomy(BaseModel): """ data_category: List[DataCategory] = Field(default_factory=list) - data_subject: Optional[List[DataSubject]] = Field(default_factory=list) - data_use: Optional[List[DataUse]] = Field(default_factory=list) + data_subject: Optional[List[DataSubject]] = Field(default_factory=list) # type: ignore[arg-type] + data_use: Optional[List[DataUse]] = Field(default_factory=list) # type: ignore[arg-type] - dataset: Optional[List[Dataset]] = Field(default_factory=list) - system: Optional[List[System]] = Field(default_factory=list) - policy: Optional[List[Policy]] = Field(default_factory=list) + dataset: Optional[List[Dataset]] = Field(default_factory=list) # type: ignore[arg-type] + system: Optional[List[System]] = Field(default_factory=list) # type: ignore[arg-type] + policy: Optional[List[Policy]] = Field(default_factory=list) # type: ignore[arg-type] organization: List[Organization] = Field(default_factory=list) diff --git a/src/fideslang/validation.py b/src/fideslang/validation.py index 74c3c46..4b8a6c9 100644 --- a/src/fideslang/validation.py +++ b/src/fideslang/validation.py @@ -58,7 +58,7 @@ def unique_items_in_list(values: List) -> List: return values -def no_self_reference(value: FidesKey, values: ValidationInfo) -> FidesKey: +def no_self_reference(value: FidesKey, values: ValidationInfo) -> FidesKey: # type: ignore[misc] """ Check to make sure that the fides_key doesn't match other fides_key references within an object. @@ -71,7 +71,7 @@ def no_self_reference(value: FidesKey, values: ValidationInfo) -> FidesKey: return value -def deprecated_version_later_than_added( +def deprecated_version_later_than_added( # type: ignore[misc] version_deprecated: Optional[str], values: ValidationInfo ) -> Optional[str]: """ @@ -104,7 +104,7 @@ def deprecated_version_later_than_added( return version_deprecated -def has_versioning_if_default(is_default: bool, values: ValidationInfo) -> bool: +def has_versioning_if_default(is_default: bool, values: ValidationInfo) -> bool: # type: ignore[misc] """ Check to make sure that version fields are set for default items. """ @@ -129,7 +129,7 @@ def has_versioning_if_default(is_default: bool, values: ValidationInfo) -> bool: return is_default -def is_deprecated_if_replaced(replaced_by: str, values: ValidationInfo) -> str: +def is_deprecated_if_replaced(replaced_by: str, values: ValidationInfo) -> str: # type: ignore[misc] """ Check to make sure that the item has been deprecated if there is a replacement. """ @@ -140,7 +140,7 @@ def is_deprecated_if_replaced(replaced_by: str, values: ValidationInfo) -> str: return replaced_by -def matching_parent_key(parent_key: FidesKey, values: ValidationInfo) -> FidesKey: +def matching_parent_key(parent_key: FidesKey, values: ValidationInfo) -> FidesKey: # type: ignore[misc] """ Confirm that the parent_key matches the parent parsed from the FidesKey. """ From 9ad251d9f3f30423ff4a56cba5cfa1675fdd2896 Mon Sep 17 00:00:00 2001 From: Adrian Galvan Date: Mon, 23 Mar 2026 12:43:44 -0700 Subject: [PATCH 4/4] fix: properly fix mypy errors instead of suppressing with type: ignore - Remove unused `_: ValidationInfo` param from `validate_object_fields` (model_validator mode="after" doesn't need it) - Remove `Optional` from Taxonomy list fields that default to `[]` (type now matches the actual default value) - Remove `disallow_any_explicit` mypy setting that conflicts with pydantic's own types (ValidationInfo, Dict[str, Any]) - Clean up all `# type: ignore` comments that are no longer needed Co-Authored-By: Claude Opus 4.6 (1M context) --- pyproject.toml | 1 - src/fideslang/models.py | 24 +++++++++++------------- src/fideslang/validation.py | 10 +++++----- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1eec2b0..1a46e5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,6 @@ where = ["src"] [tool.mypy] check_untyped_defs = true disallow_untyped_defs = true -disallow_any_explicit = true files = ["src"] no_implicit_reexport = true plugins = ["pydantic.mypy"] diff --git a/src/fideslang/models.py b/src/fideslang/models.py index c9af9ce..95107d6 100644 --- a/src/fideslang/models.py +++ b/src/fideslang/models.py @@ -20,7 +20,6 @@ HttpUrl, PositiveInt, SerializeAsAny, - ValidationInfo, field_validator, model_validator, ) @@ -76,11 +75,11 @@ class MaskingStrategyOverride(BaseModel): strategy: MaskingStrategies -class FieldMaskingStrategyOverride(BaseModel): # type: ignore[misc] +class FieldMaskingStrategyOverride(BaseModel): """Overrides field-level masking strategies.""" strategy: str - configuration: Optional[Dict[str, Any]] = {} # type: ignore[misc] + configuration: Optional[Dict[str, Any]] = {} class FidesModel(BaseModel): @@ -484,9 +483,8 @@ def valid_meta(cls, meta_values: Optional[FidesMeta]) -> Optional[FidesMeta]: return meta_values @model_validator(mode="after") - def validate_object_fields( # type: ignore[misc] + def validate_object_fields( self, - _: ValidationInfo, ) -> DatasetField: """Two validation checks for object fields: - If there are sub-fields specified, type should be either empty or 'object' @@ -823,7 +821,7 @@ class Policy(FidesModel): _sort_rules: classmethod = field_validator("rules")(sort_list_objects_by_name) -def validate_deprecated_cookies(values: Dict[str, Any] | Any) -> None: # type: ignore[misc] +def validate_deprecated_cookies(values: Dict[str, Any] | Any) -> None: """ Shared function to validate that the `cookies` field is deprecated and warn that it should not be used. """ @@ -916,7 +914,7 @@ class PrivacyDeclaration(BaseModel): @model_validator(mode="before") @classmethod - def validate_cookies(cls, values: Dict[str, Any] | Any) -> Dict[str, Any]: # type: ignore[misc] + def validate_cookies(cls, values: Dict[str, Any] | Any) -> Dict[str, Any]: """ Validate that the `cookies` field is deprecated and warn that it should not be used. """ @@ -1139,7 +1137,7 @@ class System(FidesModel): @model_validator(mode="before") @classmethod - def validate_cookies(cls, values: Dict[str, Any] | Any) -> Dict[str, Any]: # type: ignore[misc] + def validate_cookies(cls, values: Dict[str, Any] | Any) -> Dict[str, Any]: """ Validate that the `cookies` field is deprecated and warn that it should not be used. """ @@ -1193,11 +1191,11 @@ class Taxonomy(BaseModel): """ data_category: List[DataCategory] = Field(default_factory=list) - data_subject: Optional[List[DataSubject]] = Field(default_factory=list) # type: ignore[arg-type] - data_use: Optional[List[DataUse]] = Field(default_factory=list) # type: ignore[arg-type] + data_subject: List[DataSubject] = Field(default_factory=list) + data_use: List[DataUse] = Field(default_factory=list) - dataset: Optional[List[Dataset]] = Field(default_factory=list) # type: ignore[arg-type] - system: Optional[List[System]] = Field(default_factory=list) # type: ignore[arg-type] - policy: Optional[List[Policy]] = Field(default_factory=list) # type: ignore[arg-type] + dataset: List[Dataset] = Field(default_factory=list) + system: List[System] = Field(default_factory=list) + policy: List[Policy] = Field(default_factory=list) organization: List[Organization] = Field(default_factory=list) diff --git a/src/fideslang/validation.py b/src/fideslang/validation.py index 4b8a6c9..74c3c46 100644 --- a/src/fideslang/validation.py +++ b/src/fideslang/validation.py @@ -58,7 +58,7 @@ def unique_items_in_list(values: List) -> List: return values -def no_self_reference(value: FidesKey, values: ValidationInfo) -> FidesKey: # type: ignore[misc] +def no_self_reference(value: FidesKey, values: ValidationInfo) -> FidesKey: """ Check to make sure that the fides_key doesn't match other fides_key references within an object. @@ -71,7 +71,7 @@ def no_self_reference(value: FidesKey, values: ValidationInfo) -> FidesKey: # t return value -def deprecated_version_later_than_added( # type: ignore[misc] +def deprecated_version_later_than_added( version_deprecated: Optional[str], values: ValidationInfo ) -> Optional[str]: """ @@ -104,7 +104,7 @@ def deprecated_version_later_than_added( # type: ignore[misc] return version_deprecated -def has_versioning_if_default(is_default: bool, values: ValidationInfo) -> bool: # type: ignore[misc] +def has_versioning_if_default(is_default: bool, values: ValidationInfo) -> bool: """ Check to make sure that version fields are set for default items. """ @@ -129,7 +129,7 @@ def has_versioning_if_default(is_default: bool, values: ValidationInfo) -> bool: return is_default -def is_deprecated_if_replaced(replaced_by: str, values: ValidationInfo) -> str: # type: ignore[misc] +def is_deprecated_if_replaced(replaced_by: str, values: ValidationInfo) -> str: """ Check to make sure that the item has been deprecated if there is a replacement. """ @@ -140,7 +140,7 @@ def is_deprecated_if_replaced(replaced_by: str, values: ValidationInfo) -> str: return replaced_by -def matching_parent_key(parent_key: FidesKey, values: ValidationInfo) -> FidesKey: # type: ignore[misc] +def matching_parent_key(parent_key: FidesKey, values: ValidationInfo) -> FidesKey: """ Confirm that the parent_key matches the parent parsed from the FidesKey. """