cmem-client 0.5.0__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.
Files changed (52) hide show
  1. cmem_client/__init__.py +13 -0
  2. cmem_client/auth_provider/__init__.py +14 -0
  3. cmem_client/auth_provider/abc.py +124 -0
  4. cmem_client/auth_provider/client_credentials.py +207 -0
  5. cmem_client/auth_provider/password.py +252 -0
  6. cmem_client/auth_provider/prefetched_token.py +153 -0
  7. cmem_client/client.py +485 -0
  8. cmem_client/components/__init__.py +10 -0
  9. cmem_client/components/graph_store.py +316 -0
  10. cmem_client/components/marketplace.py +179 -0
  11. cmem_client/components/sparql_wrapper.py +53 -0
  12. cmem_client/components/workspace.py +194 -0
  13. cmem_client/config.py +364 -0
  14. cmem_client/exceptions.py +82 -0
  15. cmem_client/logging_utils.py +49 -0
  16. cmem_client/models/__init__.py +16 -0
  17. cmem_client/models/access_condition.py +147 -0
  18. cmem_client/models/base.py +30 -0
  19. cmem_client/models/dataset.py +32 -0
  20. cmem_client/models/error.py +67 -0
  21. cmem_client/models/graph.py +26 -0
  22. cmem_client/models/item.py +143 -0
  23. cmem_client/models/logging_config.py +51 -0
  24. cmem_client/models/package.py +35 -0
  25. cmem_client/models/project.py +46 -0
  26. cmem_client/models/python_package.py +26 -0
  27. cmem_client/models/token.py +40 -0
  28. cmem_client/models/url.py +34 -0
  29. cmem_client/models/workflow.py +80 -0
  30. cmem_client/repositories/__init__.py +15 -0
  31. cmem_client/repositories/access_conditions.py +62 -0
  32. cmem_client/repositories/base/__init__.py +12 -0
  33. cmem_client/repositories/base/abc.py +138 -0
  34. cmem_client/repositories/base/paged_list.py +63 -0
  35. cmem_client/repositories/base/plain_list.py +39 -0
  36. cmem_client/repositories/base/task_search.py +70 -0
  37. cmem_client/repositories/datasets.py +36 -0
  38. cmem_client/repositories/graph_imports.py +93 -0
  39. cmem_client/repositories/graphs.py +458 -0
  40. cmem_client/repositories/marketplace_packages.py +486 -0
  41. cmem_client/repositories/projects.py +214 -0
  42. cmem_client/repositories/protocols/__init__.py +15 -0
  43. cmem_client/repositories/protocols/create_item.py +125 -0
  44. cmem_client/repositories/protocols/delete_item.py +95 -0
  45. cmem_client/repositories/protocols/export_item.py +114 -0
  46. cmem_client/repositories/protocols/import_item.py +141 -0
  47. cmem_client/repositories/python_packages.py +58 -0
  48. cmem_client/repositories/workflows.py +143 -0
  49. cmem_client-0.5.0.dist-info/METADATA +64 -0
  50. cmem_client-0.5.0.dist-info/RECORD +52 -0
  51. cmem_client-0.5.0.dist-info/WHEEL +4 -0
  52. cmem_client-0.5.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,16 @@
1
+ """Pydantic data models for Corporate Memory entities.
2
+
3
+ This package contains all data models used throughout the cmem_client library,
4
+ implementing structured data validation and serialization using Pydantic v2.
5
+ These models represent various Corporate Memory entities and API responses.
6
+
7
+ Key model categories:
8
+ - Base models: Common base classes and interfaces
9
+ - Business entities: Projects, datasets, graphs, access conditions
10
+ - Authentication: Token models for OAuth flows
11
+ - API responses: Error models and result sets
12
+ - Utilities: URL handling and validation
13
+
14
+ All models inherit from either Model (for general data) or ReadRepositoryItem
15
+ (for entities that can be retrieved from repositories).
16
+ """
@@ -0,0 +1,147 @@
1
+ """Access control and authorization models for Corporate Memory.
2
+
3
+ This module defines models for managing access conditions in Corporate Memory,
4
+ which control user and group permissions for graphs, actions, and other resources.
5
+ Access conditions form the foundation of Corporate Memory's authorization system.
6
+
7
+ The AccessCondition model supports both static permissions (defined at creation)
8
+ and dynamic permissions (computed via SPARQL queries), providing flexible
9
+ access control patterns for different organizational needs.
10
+
11
+ Access conditions can grant various permissions including graph read/write access,
12
+ action execution rights, and management permissions for other access conditions.
13
+ """
14
+
15
+ from datetime import datetime
16
+
17
+ from pydantic import Field
18
+
19
+ from cmem_client.models.base import Model, ReadRepositoryItem
20
+ from cmem_client.repositories.base.paged_list import PageDescription
21
+
22
+ NS_AC = "http://eccenca.com/ac/"
23
+ NS_ACTION = "https://vocab.eccenca.com/auth/Action/"
24
+
25
+
26
+ class AccessCondition(Model, ReadRepositoryItem):
27
+ """An access condition"""
28
+
29
+ iri: str = Field(description="The IRI of the access condition.", examples=[f"{NS_AC}my-condition"])
30
+ name: str = Field(description="A short name to identify the access condition.", examples=["My Access Condition"])
31
+ comment: str | None = Field(
32
+ default=None,
33
+ description="An optional description to provide more context information of the access condition.",
34
+ examples=["This condition is of me ..."],
35
+ )
36
+ requires_account: str | None = Field(
37
+ alias="requiresAccount",
38
+ default=None,
39
+ description="A specific account IRI required by the access condition.",
40
+ examples=["http://eccenca.com/admin"],
41
+ )
42
+ requires_group: list[str] = Field(
43
+ alias="requiresGroup",
44
+ default=[],
45
+ description="The groups (IRI) the account must be member of to meet the access condition.",
46
+ examples=[["http://eccenca.com/elds-admins"]],
47
+ )
48
+ readable_graphs: list[str] = Field(
49
+ alias="readableGraphs",
50
+ default=[],
51
+ description="Grants read access to a graph - list of Graph IRIs.",
52
+ examples=[["https://vocab.eccenca.com/shacl/", "https://vocab.eccenca.com/auth/AllGraphs"]],
53
+ )
54
+ writable_graphs: list[str] = Field(
55
+ alias="writableGraphs",
56
+ default=[],
57
+ description="Grants read/write access to a graph - list of Graph IRIs.",
58
+ examples=[["https://vocab.eccenca.com/shacl/", "https://vocab.eccenca.com/auth/AllGraphs"]],
59
+ )
60
+ allowed_actions: list[str] = Field(
61
+ alias="allowedActions",
62
+ default=[],
63
+ description="Grants permission to execute an action - list of Action IRIs.",
64
+ examples=[[f"{NS_ACTION}Build", f"{NS_ACTION}AllActions"]],
65
+ )
66
+ grant_allowed_actions: list[str] = Field(
67
+ alias="grantAllowedActions",
68
+ default=[],
69
+ description="Grants management of conditions granting action allowance for actions matching"
70
+ " the defined pattern.",
71
+ examples=[[f"{NS_ACTION}Build*", "*"]],
72
+ )
73
+ grant_read_patterns: list[str] = Field(
74
+ alias="grantReadPatterns",
75
+ default=[],
76
+ description="Grants management of conditions granting read access on graphs matching the defined pattern.",
77
+ examples=[["https://example.org/*", "*"]],
78
+ )
79
+ grant_write_patterns: list[str] = Field(
80
+ alias="grantWritePatterns",
81
+ default=[],
82
+ description="Grants management of conditions granting write access on graphs matching the defined pattern.",
83
+ examples=[["https://example.org/*", "*"]],
84
+ )
85
+ query: str | None = Field(
86
+ alias="dynamicAccessConditionQuery",
87
+ default=None,
88
+ description="A SPARQL SELECT query which returns the following projection variables: user, group,"
89
+ " readGraph, and writeGraph.",
90
+ examples=[
91
+ """
92
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
93
+ PREFIX dct: <http://purl.org/dc/terms/>
94
+ PREFIX void: <http://rdfs.org/ns/void#>
95
+
96
+ SELECT ?user ?group ?readGraph ?writeGraph
97
+ WHERE
98
+ {
99
+ GRAPH ?writeGraph {
100
+ ?writeGraph rdf:type void:Dataset .
101
+ ?writeGraph dct:creator ?user .
102
+ }
103
+ }
104
+ """
105
+ ],
106
+ )
107
+ creator: str | None = Field(
108
+ default=None,
109
+ description="The IRI of the account which created the access condition.",
110
+ examples=["http://eccenca.com/admin"],
111
+ )
112
+ created: datetime | None = Field(
113
+ default=None, description="The time when the access condition was created.", examples=["2025-09-12T09:09:48Z"]
114
+ )
115
+
116
+ def get_id(self) -> str:
117
+ """Get the IRI of the access condition"""
118
+ return self.iri
119
+
120
+ def set_iri(self, local_name: str) -> None:
121
+ """Set the IRI of the access condition based on a new local name
122
+
123
+ this just adds the namespace prefix
124
+ """
125
+ self.iri = f"{NS_AC}{local_name}"
126
+
127
+ def get_create_request(self) -> dict:
128
+ """Create a CreateAccessConditionRequest dict
129
+
130
+ This object is used to create new access condition.
131
+ """
132
+ if not self.get_id().startswith(NS_AC):
133
+ raise ValueError(f"Access condition ID must start with '{NS_AC}'")
134
+ data = self.model_dump(by_alias=True)
135
+ data["staticId"] = self.get_id().replace(NS_AC, "")
136
+ for key in ["iri", "creator", "created"]:
137
+ # remove not needed keys if present
138
+ if key in data:
139
+ del data[key]
140
+ return data
141
+
142
+
143
+ class AccessConditionResultSet(Model):
144
+ """An access condition result set"""
145
+
146
+ content: list[AccessCondition]
147
+ page: PageDescription
@@ -0,0 +1,30 @@
1
+ """Base model classes for all cmem_client data models.
2
+
3
+ This module provides the foundational model classes that all other models
4
+ inherit from, establishing common patterns for data validation, serialization,
5
+ and repository interactions.
6
+
7
+ The Model class serves as the base for all Pydantic models in the library,
8
+ while ReadRepositoryItem provides an additional interface for entities that
9
+ can be retrieved from repositories and have identifiable IDs.
10
+ """
11
+
12
+ from abc import ABC, abstractmethod
13
+
14
+ from pydantic import BaseModel, ConfigDict
15
+
16
+
17
+ class Model(BaseModel):
18
+ """Base model for all cmem-client models."""
19
+
20
+ model_config = ConfigDict(extra="allow")
21
+
22
+
23
+ class ReadRepositoryItem(BaseModel, ABC):
24
+ """Abstract base class for items of a read repository"""
25
+
26
+ model_config = ConfigDict(extra="allow")
27
+
28
+ @abstractmethod
29
+ def get_id(self) -> str:
30
+ """Get the id of the item."""
@@ -0,0 +1,32 @@
1
+ """Corporate Memory dataset models for data integration.
2
+
3
+ This module defines models for representing datasets within Corporate Memory
4
+ projects. Datasets are data sources or sinks used in data integration workflows,
5
+ connecting to various external systems through plugins.
6
+
7
+ The Dataset model represents the configuration and metadata of datasets within
8
+ the DataIntegration environment, including their association with projects
9
+ and the plugins that handle their data access.
10
+ """
11
+
12
+ from pydantic import Field
13
+
14
+ from cmem_client.models.base import Model, ReadRepositoryItem
15
+
16
+
17
+ class Dataset(ReadRepositoryItem):
18
+ """A Dataset Description (Build)"""
19
+
20
+ id: str
21
+ project_id: str = Field(alias="projectId")
22
+ plugin_id: str = Field(alias="pluginId")
23
+
24
+ def get_id(self) -> str:
25
+ """Get the ID of the dataset"""
26
+ return f"{self.project_id}:{self.id}"
27
+
28
+
29
+ class DatasetSearchResultSet(Model):
30
+ """A dataset search result set"""
31
+
32
+ results: list[Dataset]
@@ -0,0 +1,67 @@
1
+ """Error response models for Corporate Memory API error handling.
2
+
3
+ This module defines models for parsing and handling error responses from
4
+ both the DataIntegration (build) and DataPlatform (explore) APIs. Different
5
+ API endpoints return different error response formats, and these models
6
+ provide a unified way to handle them.
7
+
8
+ The Problem model handles DataPlatform API errors, while ErrorResult handles
9
+ DataIntegration API errors. Both include methods for generating human-readable
10
+ error messages for debugging and user feedback.
11
+ """
12
+
13
+ from typing import Literal
14
+
15
+ from pydantic import Field
16
+
17
+ from cmem_client.models.base import Model
18
+
19
+
20
+ class Violation(Model):
21
+ """A data violation, communicated with a problem"""
22
+
23
+ field: str
24
+ message: str
25
+
26
+
27
+ class Problem(Model):
28
+ """A problem, communicated by the server
29
+
30
+ This type of response is returned by the explore APIs (DataPlatform)
31
+ """
32
+
33
+ type: str
34
+ title: str
35
+ status: int
36
+ details: str = Field(default="")
37
+ violations: list[Violation] = Field(default=[])
38
+
39
+ def get_exception_message(self) -> str:
40
+ """Get error message"""
41
+ text = f"{self.title} ({self.status}) - "
42
+ if self.details:
43
+ text += f" {self.details}"
44
+ if len(self.violations) > 0:
45
+ text += f" with {len(self.violations)} violation(s) -"
46
+ for violation in self.violations:
47
+ text += f" {violation.message} ({violation.field})"
48
+ return text
49
+
50
+
51
+ class ErrorResultIssue(Model):
52
+ """An issue listed with an ErrorResult"""
53
+
54
+ type: Literal["Error", "Warning", "Info"]
55
+ message: str
56
+ id: str
57
+
58
+
59
+ class ErrorResult(Model):
60
+ """An error result, communicated by the server
61
+
62
+ returned by the build APIs (DataIntegration)
63
+ """
64
+
65
+ title: str
66
+ detail: str
67
+ issues: list[ErrorResultIssue] | None
@@ -0,0 +1,26 @@
1
+ """RDF graph models for Corporate Memory knowledge graphs.
2
+
3
+ This module defines models for representing RDF graphs in Corporate Memory's
4
+ DataPlatform (explore) environment. Graphs contain semantic data and are
5
+ the primary storage units for knowledge graphs.
6
+
7
+ The Graph model includes metadata about graph permissions, assigned semantic
8
+ classes, and access control, providing the foundation for graph-based
9
+ operations in the explore APIs.
10
+ """
11
+
12
+ from pydantic import Field
13
+
14
+ from cmem_client.models.base import Model, ReadRepositoryItem
15
+
16
+
17
+ class Graph(Model, ReadRepositoryItem):
18
+ """A graph"""
19
+
20
+ iri: str
21
+ writeable: bool
22
+ assigned_classes: list[str] = Field(alias="assignedClasses")
23
+
24
+ def get_id(self) -> str:
25
+ """Get the IRI of the graph"""
26
+ return self.iri
@@ -0,0 +1,143 @@
1
+ """ImportItem base class and inherited classes"""
2
+
3
+ import tempfile
4
+ import zipfile
5
+ from abc import ABC, abstractmethod
6
+ from pathlib import Path
7
+ from types import TracebackType
8
+ from typing import ClassVar
9
+
10
+ from cmem_client.models.base import Model
11
+
12
+
13
+ class ImportItem(ABC, Model):
14
+ """Abstract base class for different import source types.
15
+
16
+ Each concrete implementation represents a different source type
17
+ (file, directory, zip, etc.) and knows how to prepare itself
18
+ for import by providing a Path to a directory or file.
19
+ """
20
+
21
+ import_type: ClassVar[str]
22
+
23
+ def __init__(self, source: Path | str) -> None:
24
+ """Initialize the import item with its source.
25
+
26
+ Args:
27
+ source: Source path or identifier for the import
28
+ """
29
+ super().__init__()
30
+ self.source = Path(source) if isinstance(source, str) else source
31
+ self._prepared_path: Path | None = None
32
+ self._cleanup_needed: bool = False
33
+
34
+ @abstractmethod
35
+ def prepare(self) -> Path:
36
+ """Prepare the import source and return a path to import from.
37
+
38
+ This method transforms the source into a format suitable for import.
39
+ For example:
40
+ - Zip files are extracted to a temp directory
41
+ - Directories are returned as-is
42
+
43
+ Returns:
44
+ Path to directory or file ready for import
45
+ """
46
+
47
+ @abstractmethod
48
+ def cleanup(self) -> None:
49
+ """Clean up any temporary resources created during preparation."""
50
+
51
+ def __enter__(self) -> Path:
52
+ """Context manager entry. Prepare the import source."""
53
+ self._prepared_path = self.prepare()
54
+ return self._prepared_path
55
+
56
+ def __exit__(
57
+ self,
58
+ exc_type: type[BaseException] | None,
59
+ exc_val: BaseException | None,
60
+ exc_tb: TracebackType | None,
61
+ ) -> None:
62
+ """Context manager exit. Cleanup resources."""
63
+ self.cleanup()
64
+
65
+ @classmethod
66
+ def detect(cls, source: Path) -> type["ImportItem"]:
67
+ """Detect the appropriate ImportItem type for the given source.
68
+
69
+ Args:
70
+ source: Path to analyze
71
+
72
+ Returns:
73
+ The appropriate ImportItem subclass
74
+ """
75
+ if source.is_dir():
76
+ return DirectoryImportItem
77
+ if zipfile.is_zipfile(source):
78
+ return ZipImportItem
79
+ return FileImportItem
80
+
81
+
82
+ class DirectoryImportItem(ImportItem):
83
+ """Import from a directory - no transformation needed."""
84
+
85
+ import_type = "directory"
86
+
87
+ def prepare(self) -> Path:
88
+ """Return directory path as-is."""
89
+ return self.source
90
+
91
+ def cleanup(self) -> None:
92
+ """No cleanup needed for directories."""
93
+
94
+
95
+ class FileImportItem(ImportItem):
96
+ """Import from a single file, copy to temp directory."""
97
+
98
+ import_type = "file"
99
+
100
+ def prepare(self) -> Path:
101
+ """Copy file to a temporary directory."""
102
+ return self.source
103
+
104
+ def cleanup(self) -> None:
105
+ """Remove temporary directory."""
106
+
107
+
108
+ class ZipImportItem(ImportItem):
109
+ """Import from a zip archive - extract to temp directory."""
110
+
111
+ import_type = "zip"
112
+
113
+ def __init__(self, source: Path | str) -> None:
114
+ super().__init__(source)
115
+ self._temp_dir: tempfile.TemporaryDirectory[str] | None = None
116
+
117
+ def prepare(self) -> Path:
118
+ """Extract zip to temporary directory."""
119
+ self._temp_dir = tempfile.TemporaryDirectory()
120
+ temp_path = Path(self._temp_dir.name)
121
+ with zipfile.ZipFile(self.source, "r") as zipf:
122
+ zipf.extractall(temp_path)
123
+ self._cleanup_needed = True
124
+ return temp_path
125
+
126
+ def cleanup(self) -> None:
127
+ """Remove temporary directory."""
128
+ if self._temp_dir:
129
+ self._temp_dir.cleanup()
130
+ self._temp_dir = None
131
+
132
+
133
+ def create_import_item(source: Path) -> ImportItem:
134
+ """Factory function to create appropriate ImportItem instance.
135
+
136
+ Args:
137
+ source: Path to the import source
138
+
139
+ Returns:
140
+ Appropriate ImportItem instance based on source type
141
+ """
142
+ item_class = ImportItem.detect(source)
143
+ return item_class(source)
@@ -0,0 +1,51 @@
1
+ """Models for the configuration of the logging module"""
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, Field, field_validator
6
+
7
+ LogLevel = Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
8
+
9
+
10
+ class FormatterConfig(BaseModel):
11
+ """Formatter configuration."""
12
+
13
+ format: str
14
+ datefmt: str | None = None
15
+
16
+
17
+ class HandlerConfig(BaseModel):
18
+ """Handler configuration."""
19
+
20
+ class_: str = Field(..., alias="class")
21
+ level: LogLevel | None
22
+ formatter: str | None
23
+ filename: str | None
24
+
25
+
26
+ class LoggerConfig(BaseModel):
27
+ """Logger configuration."""
28
+
29
+ level: LogLevel | None
30
+ handlers: list[str] | None
31
+
32
+
33
+ class LoggingConfig(BaseModel):
34
+ """Logging configuration. Allows for extra fields but validates the most common fields."""
35
+
36
+ version: int = 1
37
+ disable_existing_loggers: bool
38
+ formatters: dict[str, FormatterConfig] | None
39
+ handlers: dict[str, HandlerConfig] | None
40
+ loggers: dict[str, LoggerConfig] | None
41
+ root: LoggerConfig | None
42
+
43
+ model_config = {"extra": "allow"}
44
+
45
+ @classmethod
46
+ @field_validator("version")
47
+ def check_version(cls, v: int) -> int:
48
+ """Ensure version is always 1."""
49
+ if v != 1:
50
+ raise ValueError("Logging config version must be 1")
51
+ return v
@@ -0,0 +1,35 @@
1
+ """Marketplace package models."""
2
+
3
+ from eccenca_marketplace_client.package_version import PackageVersion
4
+ from pydantic import ConfigDict
5
+
6
+ from cmem_client.models.base import Model, ReadRepositoryItem
7
+
8
+
9
+ class PackageMetadata(Model):
10
+ """Package metadata."""
11
+
12
+ name: str
13
+ description: str
14
+ comment: str | None = None
15
+
16
+
17
+ class Package(ReadRepositoryItem):
18
+ """Installed marketplace package.
19
+
20
+ Represents a package installed in Corporate Memory with all its
21
+ metadata, file specifications, and version information as stored
22
+ in the marketplace catalog graph.
23
+ """
24
+
25
+ package_version: PackageVersion
26
+
27
+ model_config = ConfigDict(arbitrary_types_allowed=True, str_strip_whitespace=True, extra="forbid")
28
+
29
+ def get_id(self) -> str:
30
+ """Get the package identifier.
31
+
32
+ Returns:
33
+ The package_id which uniquely identifies this package.
34
+ """
35
+ return str(self.package_version.manifest.package_id)
@@ -0,0 +1,46 @@
1
+ """Corporate Memory project models and metadata.
2
+
3
+ This module defines models for representing Corporate Memory DataIntegration
4
+ projects, including their metadata such as labels, descriptions, and tags.
5
+
6
+ Projects are the primary organizational unit in Corporate Memory's build
7
+ environment, containing datasets, transformations, and other integration
8
+ components. The Project model provides validation and serialization for
9
+ project data exchanged with the DataIntegration API.
10
+ """
11
+
12
+ from typing import Any
13
+
14
+ from pydantic import Field
15
+
16
+ from cmem_client.models.base import Model, ReadRepositoryItem
17
+
18
+
19
+ class ProjectMetaData(Model):
20
+ """Project Meta Data"""
21
+
22
+ label: str | None = None
23
+ description: str | None = None
24
+ tags: list[str] | None = None
25
+
26
+
27
+ def default_metadata() -> ProjectMetaData:
28
+ """Get the current UTC datetime"""
29
+ # empty string is not allowed by DI, so model_post_init will change this to the ID
30
+ return ProjectMetaData(label="")
31
+
32
+
33
+ class Project(ReadRepositoryItem):
34
+ """A Build (DataIntegration) Project"""
35
+
36
+ name: str
37
+ meta_data: ProjectMetaData = Field(alias="metaData", default_factory=default_metadata)
38
+
39
+ def model_post_init(self, context: Any, /) -> None: # noqa: ANN401, ARG002
40
+ """Set the label to the name if needed"""
41
+ if self.meta_data.label == "":
42
+ self.meta_data.label = self.name
43
+
44
+ def get_id(self) -> str:
45
+ """Get the ID of the project"""
46
+ return self.name
@@ -0,0 +1,26 @@
1
+ """Python package models."""
2
+
3
+ from eccenca_marketplace_client.fields import PyPiIdentifier
4
+ from pydantic import ConfigDict
5
+
6
+ from cmem_client.models.base import ReadRepositoryItem
7
+
8
+
9
+ class PythonPackage(ReadRepositoryItem):
10
+ """Installed python package.
11
+
12
+ Represents a python package installed in Corporate Memory
13
+ """
14
+
15
+ name: PyPiIdentifier
16
+ version: str | None = None
17
+
18
+ model_config = ConfigDict(arbitrary_types_allowed=True, str_strip_whitespace=True, extra="forbid")
19
+
20
+ def get_id(self) -> str:
21
+ """Get the package identifier.
22
+
23
+ Returns:
24
+ The python pypi name which uniquely identifies this package.
25
+ """
26
+ return str(self.name)
@@ -0,0 +1,40 @@
1
+ """Authentication token models for OAuth 2.0 flows.
2
+
3
+ This module provides models for handling OAuth 2.0 tokens, particularly
4
+ Keycloak tokens used in Corporate Memory authentication. It includes
5
+ automatic JWT parsing and expiration checking functionality.
6
+
7
+ The KeycloakToken model handles token lifecycle management, including
8
+ automatically parsing JWT contents and providing expiration checking
9
+ to support token refresh logic in authentication providers.
10
+ """
11
+
12
+ from datetime import UTC, datetime
13
+
14
+ import jwt
15
+ from pydantic import Field
16
+
17
+ from cmem_client.models.base import Model
18
+
19
+
20
+ def default_factory_now() -> datetime:
21
+ """Get the current UTC datetime"""
22
+ return datetime.now(tz=UTC)
23
+
24
+
25
+ class KeycloakToken(Model):
26
+ """A Keycloak token"""
27
+
28
+ access_token: str
29
+ expires_in: int
30
+ expires: datetime = Field(default_factory=default_factory_now) # will be overwritten
31
+ jwt: dict = Field(default_factory=dict)
32
+
33
+ def model_post_init(self, context, /) -> None: # noqa: ANN001, ARG002
34
+ """Do the post init"""
35
+ self.jwt = jwt.decode(self.access_token, options={"verify_signature": False})
36
+ self.expires = datetime.fromtimestamp(self.jwt["exp"], tz=UTC)
37
+
38
+ def is_expired(self) -> bool:
39
+ """Check if token is expired"""
40
+ return datetime.now(tz=UTC) >= self.expires