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.
- cmem_client/__init__.py +13 -0
- cmem_client/auth_provider/__init__.py +14 -0
- cmem_client/auth_provider/abc.py +124 -0
- cmem_client/auth_provider/client_credentials.py +207 -0
- cmem_client/auth_provider/password.py +252 -0
- cmem_client/auth_provider/prefetched_token.py +153 -0
- cmem_client/client.py +485 -0
- cmem_client/components/__init__.py +10 -0
- cmem_client/components/graph_store.py +316 -0
- cmem_client/components/marketplace.py +179 -0
- cmem_client/components/sparql_wrapper.py +53 -0
- cmem_client/components/workspace.py +194 -0
- cmem_client/config.py +364 -0
- cmem_client/exceptions.py +82 -0
- cmem_client/logging_utils.py +49 -0
- cmem_client/models/__init__.py +16 -0
- cmem_client/models/access_condition.py +147 -0
- cmem_client/models/base.py +30 -0
- cmem_client/models/dataset.py +32 -0
- cmem_client/models/error.py +67 -0
- cmem_client/models/graph.py +26 -0
- cmem_client/models/item.py +143 -0
- cmem_client/models/logging_config.py +51 -0
- cmem_client/models/package.py +35 -0
- cmem_client/models/project.py +46 -0
- cmem_client/models/python_package.py +26 -0
- cmem_client/models/token.py +40 -0
- cmem_client/models/url.py +34 -0
- cmem_client/models/workflow.py +80 -0
- cmem_client/repositories/__init__.py +15 -0
- cmem_client/repositories/access_conditions.py +62 -0
- cmem_client/repositories/base/__init__.py +12 -0
- cmem_client/repositories/base/abc.py +138 -0
- cmem_client/repositories/base/paged_list.py +63 -0
- cmem_client/repositories/base/plain_list.py +39 -0
- cmem_client/repositories/base/task_search.py +70 -0
- cmem_client/repositories/datasets.py +36 -0
- cmem_client/repositories/graph_imports.py +93 -0
- cmem_client/repositories/graphs.py +458 -0
- cmem_client/repositories/marketplace_packages.py +486 -0
- cmem_client/repositories/projects.py +214 -0
- cmem_client/repositories/protocols/__init__.py +15 -0
- cmem_client/repositories/protocols/create_item.py +125 -0
- cmem_client/repositories/protocols/delete_item.py +95 -0
- cmem_client/repositories/protocols/export_item.py +114 -0
- cmem_client/repositories/protocols/import_item.py +141 -0
- cmem_client/repositories/python_packages.py +58 -0
- cmem_client/repositories/workflows.py +143 -0
- cmem_client-0.5.0.dist-info/METADATA +64 -0
- cmem_client-0.5.0.dist-info/RECORD +52 -0
- cmem_client-0.5.0.dist-info/WHEEL +4 -0
- 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
|