crosscontract 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crosscontract/__init__.py +14 -0
- crosscontract/contracts/__init__.py +9 -0
- crosscontract/contracts/contracts/__init__.py +4 -0
- crosscontract/contracts/contracts/base_contract.py +103 -0
- crosscontract/contracts/contracts/cross_contract.py +64 -0
- crosscontract/contracts/schema/__init__.py +29 -0
- crosscontract/contracts/schema/adapters/__init__.py +18 -0
- crosscontract/contracts/schema/adapters/abstract_adapter.py +30 -0
- crosscontract/contracts/schema/adapters/pandera_adapter.py +557 -0
- crosscontract/contracts/schema/adapters/pydantic_adapter.py +288 -0
- crosscontract/contracts/schema/adapters/sqlalchemy_adapter.py +168 -0
- crosscontract/contracts/schema/adapters/utils.py +30 -0
- crosscontract/contracts/schema/exceptions/__init__.py +1 -0
- crosscontract/contracts/schema/exceptions/validation_error.py +222 -0
- crosscontract/contracts/schema/field_descriptors/__init__.py +20 -0
- crosscontract/contracts/schema/field_descriptors/descriptors.py +66 -0
- crosscontract/contracts/schema/field_descriptors/field_descriptors.py +89 -0
- crosscontract/contracts/schema/fields/__init__.py +18 -0
- crosscontract/contracts/schema/fields/base.py +61 -0
- crosscontract/contracts/schema/fields/datetime_field.py +37 -0
- crosscontract/contracts/schema/fields/list_field.py +55 -0
- crosscontract/contracts/schema/fields/numeric_field.py +44 -0
- crosscontract/contracts/schema/fields/string_field.py +48 -0
- crosscontract/contracts/schema/reference/__init__.py +4 -0
- crosscontract/contracts/schema/reference/foreign_key.py +150 -0
- crosscontract/contracts/schema/reference/primary_key.py +52 -0
- crosscontract/contracts/schema/schema.py +253 -0
- crosscontract/contracts/schema/validation/__init__.py +3 -0
- crosscontract/contracts/schema/validation/validate_dataframe.py +81 -0
- crosscontract/contracts/utils.py +35 -0
- crosscontract/contracts/valid_items.py +14 -0
- crosscontract/crossclient/__init__.py +3 -0
- crosscontract/crossclient/crossclient.py +146 -0
- crosscontract/crossclient/exceptions/__init__.py +24 -0
- crosscontract/crossclient/exceptions/exception_factory.py +104 -0
- crosscontract/crossclient/exceptions/exceptions.py +104 -0
- crosscontract/crossclient/logger.py +4 -0
- crosscontract/crossclient/services/__init__.py +4 -0
- crosscontract/crossclient/services/contract_resource.py +303 -0
- crosscontract/crossclient/services/contract_service.py +267 -0
- crosscontract/py.typed +0 -0
- crosscontract-0.2.2.dist-info/METADATA +23 -0
- crosscontract-0.2.2.dist-info/RECORD +45 -0
- crosscontract-0.2.2.dist-info/WHEEL +4 -0
- crosscontract-0.2.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""crosscontract package for data contracts and related utilities."""
|
|
2
|
+
|
|
3
|
+
from .contracts import BaseContract, CrossContract, SchemaValidationError, TableSchema
|
|
4
|
+
from .crossclient import CrossClient
|
|
5
|
+
|
|
6
|
+
__version__ = "0.2.2"
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"CrossClient",
|
|
10
|
+
"CrossContract",
|
|
11
|
+
"TableSchema",
|
|
12
|
+
"BaseContract",
|
|
13
|
+
"SchemaValidationError",
|
|
14
|
+
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
5
|
+
|
|
6
|
+
from ..schema import TableSchema
|
|
7
|
+
from ..utils import read_yaml_or_json_file
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseMetaData(BaseModel):
|
|
11
|
+
"""
|
|
12
|
+
The BaseMetadata class encapsulates the essential metadata attributes
|
|
13
|
+
required for defining a data contract. Every data contract MUST include
|
|
14
|
+
these metadata fields to ensure proper identification and description.
|
|
15
|
+
To extend the metadata for specific use cases, inherit from this class
|
|
16
|
+
and add additional fields as necessary. Then use the extended metadata
|
|
17
|
+
class as a base for your custom contract together with BaseContract.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
name (str): A unique identifier for the data contract.
|
|
21
|
+
Must contain only alphanumeric characters, underscores, or hyphens.
|
|
22
|
+
Maximum length is 100 characters.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(extra="forbid")
|
|
26
|
+
|
|
27
|
+
name: str = Field(
|
|
28
|
+
pattern="^[a-zA-Z0-9_-]+$",
|
|
29
|
+
max_length=100,
|
|
30
|
+
description="A unique identifier for the data contract.",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BaseContract(BaseMetaData):
|
|
35
|
+
"""
|
|
36
|
+
The BaseContract class is the most basic representation of a data contract.
|
|
37
|
+
It combines the minimum required metadata with the contract structure given by
|
|
38
|
+
Schema.
|
|
39
|
+
|
|
40
|
+
It serves as the foundational blueprint for defining data contracts.
|
|
41
|
+
Any custom contract implementation MUST inherit from this class to ensure
|
|
42
|
+
structural consistency and compatibility with the system.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
name (str): A unique identifier for the data contract.
|
|
46
|
+
Must contain only alphanumeric characters, underscores, or hyphens.
|
|
47
|
+
Maximum length is 100 characters.
|
|
48
|
+
tableschema (TableSchema): The schema defining the structure of the contract
|
|
49
|
+
(fields, primary keys, foreign keys, field descriptors).
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
To implement a custom contract with additional metadata:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from pydantic import Field
|
|
56
|
+
from crosscontract.contracts import BaseContract
|
|
57
|
+
|
|
58
|
+
class MyCustomContract(BaseContract):
|
|
59
|
+
# Add custom metadata fields
|
|
60
|
+
owner: str = Field(description="The owner of this dataset")
|
|
61
|
+
version: str = Field(description="Semantic version of the contract")
|
|
62
|
+
|
|
63
|
+
# The 'schema' field is already inherited from BaseContract!
|
|
64
|
+
```
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
68
|
+
|
|
69
|
+
tableschema: TableSchema = Field(
|
|
70
|
+
description="The Frictionless Table Schema definition.",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_file(cls, file_path: str | Path) -> Self:
|
|
75
|
+
"""
|
|
76
|
+
Load a BaseContract from a YAML or JSON file.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
file_path (str | Path): The path to the YAML or JSON file.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Self: An instance of BaseContract loaded from the file.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
FileNotFoundError: If the specified file does not exist.
|
|
86
|
+
ValueError: If the file format is not supported (not .json, .yaml, or .yml).
|
|
87
|
+
"""
|
|
88
|
+
data = read_yaml_or_json_file(file_path)
|
|
89
|
+
return cls.model_validate(data)
|
|
90
|
+
|
|
91
|
+
@model_validator(mode="after")
|
|
92
|
+
def validate_self_reference(self) -> Self:
|
|
93
|
+
"""Validate that self-referencing foreign keys are given as None on the
|
|
94
|
+
resource field. Raise if a reference has the same name as the contract itself.
|
|
95
|
+
"""
|
|
96
|
+
for fk in self.tableschema.foreignKeys:
|
|
97
|
+
if fk.reference.resource == self.name:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Foreign key reference resource '{fk.reference.resource}' "
|
|
100
|
+
"cannot be the same as the contract name. Self-references must "
|
|
101
|
+
"use None for the resource field."
|
|
102
|
+
)
|
|
103
|
+
return self
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from pydantic import ConfigDict, Field
|
|
2
|
+
|
|
3
|
+
from .base_contract import BaseContract, BaseMetaData
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CrossMetaData(BaseMetaData):
|
|
7
|
+
"""
|
|
8
|
+
Metadata specific to the CrossContract system,
|
|
9
|
+
extending the base metadata requirements
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
title (str): A human-readable title for the data.
|
|
13
|
+
description (str): A human-readable description of the data.
|
|
14
|
+
tags (list[str] | None): A list of tags for categorization and filtering.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
model_config = ConfigDict(str_strip_whitespace=True)
|
|
18
|
+
title: str = Field(
|
|
19
|
+
description=(
|
|
20
|
+
"A human-readable title for the data."
|
|
21
|
+
"Think of this as the label that will be used in graphs and tables."
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
description: str = Field(
|
|
26
|
+
description=(
|
|
27
|
+
"A human-readable description of the data. This should explain what "
|
|
28
|
+
" the data is about."
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
tags: list[str] = Field(
|
|
33
|
+
default_factory=list,
|
|
34
|
+
description=(
|
|
35
|
+
"A list of tags that can be used to categorize the table. "
|
|
36
|
+
"This can be used to filter tables in the UI."
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CrossContract(BaseContract, CrossMetaData):
|
|
42
|
+
"""
|
|
43
|
+
A concrete implementation of a data contract for the CrossContract system.
|
|
44
|
+
|
|
45
|
+
This class extends `BaseContract` by adding tagging capabilities.
|
|
46
|
+
It serves as the standard contract definition for resources within the
|
|
47
|
+
CrossContract ecosystem.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
name (str): A unique identifier for the data contract.
|
|
51
|
+
Must contain only alphanumeric characters, underscores, or hyphens.
|
|
52
|
+
Inherited from BaseContract.
|
|
53
|
+
title (str): A human-readable title for the data.
|
|
54
|
+
description (str): A human-readable description of the data.
|
|
55
|
+
tags (list[str] | None): A list of tags used for categorization and filtering.
|
|
56
|
+
schema (Schema): The Frictionless Table Schema definition.
|
|
57
|
+
Accessible via the `schema` property as well.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
model_config = ConfigDict(
|
|
61
|
+
populate_by_name=True,
|
|
62
|
+
extra="forbid",
|
|
63
|
+
serialize_by_alias=True,
|
|
64
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The schema is at the core of a contract, defining the structure and types of
|
|
2
|
+
data it holds. Schemas ensure data integrity and consistency across different systems
|
|
3
|
+
and applications. Schemas are based on Fritionless Schema
|
|
4
|
+
standard (https://frictionlessdata.io/docs/specs/schema/).
|
|
5
|
+
|
|
6
|
+
A schema consists of a collection of fields, each representing a specific data
|
|
7
|
+
type and its associated constraints. The schema defines how data should be
|
|
8
|
+
validated in the context of a contract.
|
|
9
|
+
|
|
10
|
+
To make schemas operational, there are methods to convert schema definitions into
|
|
11
|
+
Pydantic or Pandera models for data validation and manipulation, as well as into
|
|
12
|
+
SQLAlchemy columns, enabling seamless integration with databases.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .adapters import (
|
|
16
|
+
convert_schema_to_pandera,
|
|
17
|
+
convert_schema_to_pydantic,
|
|
18
|
+
convert_schema_to_sqlalchemy,
|
|
19
|
+
)
|
|
20
|
+
from .exceptions import SchemaValidationError
|
|
21
|
+
from .schema import TableSchema
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"TableSchema",
|
|
25
|
+
"convert_schema_to_pydantic",
|
|
26
|
+
"convert_schema_to_pandera",
|
|
27
|
+
"convert_schema_to_sqlalchemy",
|
|
28
|
+
"SchemaValidationError",
|
|
29
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Adapters take a schema and convert it to a different format.
|
|
2
|
+
For example, the PydanticAdapter converts a schema into a corresponding pydantic
|
|
3
|
+
model that allows to validate a single row of data against the schema. Likewise,
|
|
4
|
+
the PanderaAdapter converts a schema into a corresponding pandera schema that allows
|
|
5
|
+
to validate a dataframe against the schema."""
|
|
6
|
+
|
|
7
|
+
from .pandera_adapter import PanderaPandasAdapter, convert_schema_to_pandera
|
|
8
|
+
from .pydantic_adapter import PydanticAdapter, convert_schema_to_pydantic
|
|
9
|
+
from .sqlalchemy_adapter import SQLAlchemyPostgresAdapter, convert_schema_to_sqlalchemy
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"PydanticAdapter",
|
|
13
|
+
"convert_schema_to_pydantic",
|
|
14
|
+
"SQLAlchemyPostgresAdapter",
|
|
15
|
+
"convert_schema_to_sqlalchemy",
|
|
16
|
+
"PanderaPandasAdapter",
|
|
17
|
+
"convert_schema_to_pandera",
|
|
18
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
5
|
+
from crosscontract.contracts.schema import TableSchema
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AbstractAdapter(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract base class for schema adapters. Adapters are responsible for converting
|
|
11
|
+
a TableSchema into a specific format, such as a Pydantic model, a Pandera DataFrame,
|
|
12
|
+
or SQLAlchemy columns.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, schema: "TableSchema", *args, **kwargs: Any):
|
|
16
|
+
self.schema = schema
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def convert(self, *args, **kwargs) -> Any:
|
|
20
|
+
"""Convert the given TableSchema into the target format."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def convert_schema(cls, schema: "TableSchema", *args, **kwargs) -> Any:
|
|
26
|
+
"""Convenience method to convert a TableSchema without needing to instantiate
|
|
27
|
+
the adapter. Needs to be implemented by each adapter class to force
|
|
28
|
+
documentation of the expected arguments. But can simply call the instance
|
|
29
|
+
method by default."""
|
|
30
|
+
return cls(schema).convert(*args, **kwargs)
|