datamasque-python 1.0.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.
- datamasque/client/__init__.py +204 -0
- datamasque/client/base.py +304 -0
- datamasque/client/connections.py +64 -0
- datamasque/client/discovery.py +286 -0
- datamasque/client/dmclient.py +49 -0
- datamasque/client/exceptions.py +75 -0
- datamasque/client/files.py +92 -0
- datamasque/client/ifm.py +301 -0
- datamasque/client/license.py +41 -0
- datamasque/client/models/__init__.py +0 -0
- datamasque/client/models/connection.py +429 -0
- datamasque/client/models/data_selection.py +62 -0
- datamasque/client/models/discovery.py +229 -0
- datamasque/client/models/dm_instance.py +39 -0
- datamasque/client/models/files.py +89 -0
- datamasque/client/models/ifm.py +177 -0
- datamasque/client/models/license.py +60 -0
- datamasque/client/models/pagination.py +29 -0
- datamasque/client/models/ruleset.py +45 -0
- datamasque/client/models/ruleset_library.py +22 -0
- datamasque/client/models/runs.py +165 -0
- datamasque/client/models/status.py +68 -0
- datamasque/client/models/user.py +69 -0
- datamasque/client/py.typed +0 -0
- datamasque/client/ruleset_libraries.py +164 -0
- datamasque/client/rulesets.py +57 -0
- datamasque/client/runs.py +189 -0
- datamasque/client/settings.py +76 -0
- datamasque/client/users.py +96 -0
- datamasque_python-1.0.0.dist-info/METADATA +113 -0
- datamasque_python-1.0.0.dist-info/RECORD +33 -0
- datamasque_python-1.0.0.dist-info/WHEEL +4 -0
- datamasque_python-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Typed request and response shapes for schema-discovery and ruleset-generation endpoints."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
6
|
+
|
|
7
|
+
from datamasque.client.models.connection import ConnectionConfig, ConnectionId, unwrap_connection_id
|
|
8
|
+
from datamasque.client.models.data_selection import HashColumnsTableConfig, Locator, UserSelection
|
|
9
|
+
from datamasque.client.models.pagination import Page
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InDataDiscoveryRule(BaseModel):
|
|
13
|
+
"""A single rule for in-data discovery."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(extra="forbid")
|
|
16
|
+
|
|
17
|
+
name: Optional[str] = None
|
|
18
|
+
pattern: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InDataDiscoveryConfig(BaseModel):
|
|
22
|
+
"""In-data discovery configuration nested under `SchemaDiscoveryRequest.in_data_discovery`."""
|
|
23
|
+
|
|
24
|
+
model_config = ConfigDict(extra="forbid")
|
|
25
|
+
|
|
26
|
+
enabled: Optional[bool] = None
|
|
27
|
+
row_sample_size: Optional[int] = None
|
|
28
|
+
custom_rules: Optional[list[InDataDiscoveryRule]] = None
|
|
29
|
+
non_sensitive_rules: Optional[list[InDataDiscoveryRule]] = None
|
|
30
|
+
force: Optional[bool] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SchemaDiscoveryRequest(BaseModel):
|
|
34
|
+
"""
|
|
35
|
+
Request body for `POST /api/schema-discovery/`.
|
|
36
|
+
|
|
37
|
+
`connection` accepts either a `ConnectionId` or a full `ConnectionConfig` returned by an earlier client call.
|
|
38
|
+
Every other field uses the server's default value when omitted.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
model_config = ConfigDict(extra="forbid")
|
|
42
|
+
|
|
43
|
+
connection: Union[ConnectionId, ConnectionConfig]
|
|
44
|
+
custom_keywords: list[str] = Field(default_factory=list)
|
|
45
|
+
ignored_keywords: list[str] = Field(default_factory=list)
|
|
46
|
+
schemas: list[str] = Field(default_factory=list)
|
|
47
|
+
in_data_discovery: Optional[InDataDiscoveryConfig] = None
|
|
48
|
+
disable_built_in_keywords: bool = False
|
|
49
|
+
disable_global_custom_keywords: bool = False
|
|
50
|
+
disable_global_ignored_keywords: bool = False
|
|
51
|
+
|
|
52
|
+
@field_validator("connection", mode="before")
|
|
53
|
+
@classmethod
|
|
54
|
+
def _unwrap_connection(cls, value: Any) -> Any:
|
|
55
|
+
return unwrap_connection_id(value)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RulesetGenerationRequest(BaseModel):
|
|
59
|
+
"""
|
|
60
|
+
Request body for `POST /api/generate-ruleset/v2/`.
|
|
61
|
+
|
|
62
|
+
`connection` accepts either a `ConnectionId` or a full `ConnectionConfig` returned by an earlier client call.
|
|
63
|
+
`selected_columns` is the same nested `schema -> table -> [column, ...]` mapping
|
|
64
|
+
used by `SelectedColumns.columns`,
|
|
65
|
+
and `hash_columns` follows the `HashColumnsTableConfig` shape.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
model_config = ConfigDict(extra="forbid")
|
|
69
|
+
|
|
70
|
+
connection: Union[ConnectionId, ConnectionConfig]
|
|
71
|
+
selected_columns: dict[str, dict[str, list[str]]]
|
|
72
|
+
hash_columns: Optional[dict[str, dict[str, HashColumnsTableConfig]]] = None
|
|
73
|
+
|
|
74
|
+
@field_validator("connection", mode="before")
|
|
75
|
+
@classmethod
|
|
76
|
+
def _unwrap_connection(cls, value: Any) -> Any:
|
|
77
|
+
return unwrap_connection_id(value)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class FileRulesetGenerationRequest(BaseModel):
|
|
81
|
+
"""
|
|
82
|
+
Request body for `POST /api/generate-file-ruleset/`.
|
|
83
|
+
|
|
84
|
+
`connection` accepts either a `ConnectionId` or a full `ConnectionConfig` returned by an earlier client call.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
model_config = ConfigDict(extra="forbid")
|
|
88
|
+
|
|
89
|
+
connection: Union[ConnectionId, ConnectionConfig]
|
|
90
|
+
selected_data: list[UserSelection]
|
|
91
|
+
|
|
92
|
+
@field_validator("connection", mode="before")
|
|
93
|
+
@classmethod
|
|
94
|
+
def _unwrap_connection(cls, value: Any) -> Any:
|
|
95
|
+
return unwrap_connection_id(value)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DiscoveryMatch(BaseModel):
|
|
99
|
+
"""A single match found by schema or file discovery."""
|
|
100
|
+
|
|
101
|
+
model_config = ConfigDict(extra="allow")
|
|
102
|
+
|
|
103
|
+
label: str
|
|
104
|
+
categories: list[str]
|
|
105
|
+
flagged_by: str
|
|
106
|
+
description: str
|
|
107
|
+
hit_ratio: Optional[int] = None # None for metadata matches, percentage 0-100 for IDD matches.
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ForeignKeyRef(BaseModel):
|
|
111
|
+
"""A foreign key declared on a column, pointing to another column it references."""
|
|
112
|
+
|
|
113
|
+
model_config = ConfigDict(extra="allow")
|
|
114
|
+
|
|
115
|
+
name: str
|
|
116
|
+
referenced_column: str # Dotted path: "schema.table.column".
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ReferencingForeignKey(BaseModel):
|
|
120
|
+
"""A foreign key declared on another column that points *at* this column."""
|
|
121
|
+
|
|
122
|
+
model_config = ConfigDict(extra="allow")
|
|
123
|
+
|
|
124
|
+
name: str
|
|
125
|
+
referencing_column: str # Dotted path: "schema.table.column".
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class SchemaDiscoveryColumn(BaseModel):
|
|
129
|
+
"""Column-level data in a schema discovery result."""
|
|
130
|
+
|
|
131
|
+
model_config = ConfigDict(extra="allow")
|
|
132
|
+
|
|
133
|
+
data_type: Optional[str] = None
|
|
134
|
+
max_length: Optional[int] = None
|
|
135
|
+
foreign_keys: list[ForeignKeyRef]
|
|
136
|
+
discovery_matches: list[DiscoveryMatch]
|
|
137
|
+
numeric_precision: Optional[int] = None
|
|
138
|
+
numeric_scale: Optional[int] = None
|
|
139
|
+
constraint_columns: list[str]
|
|
140
|
+
pk_constraint_name: Optional[str] = None
|
|
141
|
+
uk_constraint_name: Optional[str] = None
|
|
142
|
+
unique_index_names: list[str]
|
|
143
|
+
referencing_foreign_keys: list[ReferencingForeignKey]
|
|
144
|
+
constraint: str # Primary or Unique, or empty string if column does not participate in a PK/UK
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class SchemaDiscoveryResult(BaseModel):
|
|
148
|
+
"""A single row in the v2 schema discovery results."""
|
|
149
|
+
|
|
150
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
151
|
+
|
|
152
|
+
id: int
|
|
153
|
+
column: str
|
|
154
|
+
table: str
|
|
155
|
+
schema_name: Optional[str] = Field(default=None, alias="schema") # "schema" is a reserved word in Pydantic
|
|
156
|
+
data: SchemaDiscoveryColumn
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class ConstraintColumns(BaseModel):
|
|
160
|
+
"""A constraint's column list in table metadata."""
|
|
161
|
+
|
|
162
|
+
model_config = ConfigDict(extra="allow")
|
|
163
|
+
|
|
164
|
+
columns: list[str]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TableConstraints(BaseModel):
|
|
168
|
+
"""Constraint metadata for a single table."""
|
|
169
|
+
|
|
170
|
+
model_config = ConfigDict(extra="allow")
|
|
171
|
+
|
|
172
|
+
primary_keys: Optional[list[ConstraintColumns]] = None
|
|
173
|
+
unique_keys: Optional[list[ConstraintColumns]] = None
|
|
174
|
+
foreign_keys: Optional[list[ConstraintColumns]] = None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class SchemaDiscoveryPage(Page[SchemaDiscoveryResult]):
|
|
178
|
+
"""
|
|
179
|
+
Admin-server envelope for `GET /api/schema-discovery/v2/{run_id}/`.
|
|
180
|
+
|
|
181
|
+
Extends the standard `Page` with `table_metadata`.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
table_metadata: Optional[dict[str, dict[str, TableConstraints]]] = None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class FileDiscoveryMatch(BaseModel):
|
|
188
|
+
"""A single match in a file discovery locator."""
|
|
189
|
+
|
|
190
|
+
model_config = ConfigDict(extra="allow")
|
|
191
|
+
|
|
192
|
+
categories: Optional[list[str]] = None
|
|
193
|
+
flagged_by: Optional[str] = None
|
|
194
|
+
description: Optional[str] = None
|
|
195
|
+
label: Optional[str] = None
|
|
196
|
+
hit_ratio: Optional[int] = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class FileDiscoveryLocatorResult(BaseModel):
|
|
200
|
+
"""A locator (column/path) within a discovered file."""
|
|
201
|
+
|
|
202
|
+
model_config = ConfigDict(extra="allow")
|
|
203
|
+
|
|
204
|
+
locator: Optional[Locator] = None
|
|
205
|
+
matches: Optional[list[FileDiscoveryMatch]] = None
|
|
206
|
+
data_types: Optional[list[str]] = None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class FileDiscoveryFile(BaseModel):
|
|
210
|
+
"""A file entry in a file discovery result."""
|
|
211
|
+
|
|
212
|
+
model_config = ConfigDict(extra="allow")
|
|
213
|
+
|
|
214
|
+
path: Optional[str] = None
|
|
215
|
+
file_type: Optional[str] = None
|
|
216
|
+
delimiter: Optional[str] = None
|
|
217
|
+
encoding: Optional[str] = None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class FileDiscoveryResult(BaseModel):
|
|
221
|
+
"""A single record from `GET /api/runs/{run_id}/file-discovery-results/`."""
|
|
222
|
+
|
|
223
|
+
model_config = ConfigDict(extra="allow")
|
|
224
|
+
|
|
225
|
+
id: Optional[int] = None
|
|
226
|
+
connection: Optional[Any] = None
|
|
227
|
+
file_type: Optional[str] = None
|
|
228
|
+
files: Optional[list[FileDiscoveryFile]] = None
|
|
229
|
+
results: Optional[list[FileDiscoveryLocatorResult]] = None
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Callable, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
4
|
+
|
|
5
|
+
from datamasque.client.exceptions import DataMasqueUserError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DataMasqueInstanceConfig(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
Connection configuration for `DataMasqueClient`.
|
|
11
|
+
|
|
12
|
+
`base_url` is the root URL of the DataMasque admin server
|
|
13
|
+
(e.g. `https://datamasque.example.com/`).
|
|
14
|
+
Set `verify_ssl=False` to skip TLS certificate verification
|
|
15
|
+
(only use this with a self-signed certificate;
|
|
16
|
+
do not disable it otherwise).
|
|
17
|
+
Exactly one of `password` or `token_source` must be set.
|
|
18
|
+
`token_source` is a user-supplied callable that returns the bare API token string —
|
|
19
|
+
the hex value returned by `POST /api/auth/token/login/`;
|
|
20
|
+
the client prepends it with `Token ` when sending the `Authorization` header.
|
|
21
|
+
The client calls `token_source` on each authentication attempt,
|
|
22
|
+
so the callable is free to fetch and refresh tokens out-of-band (e.g. from a secrets manager).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
26
|
+
|
|
27
|
+
base_url: str
|
|
28
|
+
username: str
|
|
29
|
+
password: Optional[str] = None
|
|
30
|
+
verify_ssl: bool = True
|
|
31
|
+
token_source: Optional[Callable[[], str]] = None
|
|
32
|
+
|
|
33
|
+
@model_validator(mode="after")
|
|
34
|
+
def _validate_auth_source(self) -> "DataMasqueInstanceConfig":
|
|
35
|
+
if (self.password is None) == (self.token_source is None):
|
|
36
|
+
raise DataMasqueUserError(
|
|
37
|
+
"Exactly one of `password` or `token_source` must be provided to `DataMasqueInstanceConfig`."
|
|
38
|
+
)
|
|
39
|
+
return self
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import NewType, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
6
|
+
|
|
7
|
+
FileId = NewType("FileId", str)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DataMasqueFile(BaseModel):
|
|
11
|
+
"""Base class for the concrete file types (`SeedFile`, `OracleWalletFile`, `SslZipFile`, `SnowflakeKeyFile`)."""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(extra="allow")
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
created_date: datetime
|
|
17
|
+
modified_date: Optional[datetime] = None
|
|
18
|
+
id: Optional[FileId] = None
|
|
19
|
+
|
|
20
|
+
@model_validator(mode="before")
|
|
21
|
+
@classmethod
|
|
22
|
+
def _promote_filename(cls, data: dict) -> dict:
|
|
23
|
+
"""The API sometimes returns `filename` instead of `name`."""
|
|
24
|
+
if isinstance(data, dict):
|
|
25
|
+
if "filename" in data and "name" not in data:
|
|
26
|
+
data["name"] = data["filename"]
|
|
27
|
+
return data
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def get_url(cls) -> str:
|
|
32
|
+
"""Returns the API URL path for files of this type."""
|
|
33
|
+
|
|
34
|
+
raise NotImplementedError # pragma: no cover
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def get_content_param_name(cls) -> str:
|
|
39
|
+
"""Returns the multipart form field name used when uploading files of this type."""
|
|
40
|
+
|
|
41
|
+
raise NotImplementedError # pragma: no cover
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SeedFile(DataMasqueFile):
|
|
45
|
+
"""Represents a seed file (CSV file)."""
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def get_url(cls) -> str:
|
|
49
|
+
return "api/seeds/"
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def get_content_param_name(cls) -> str:
|
|
53
|
+
return "seed_file"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class OracleWalletFile(DataMasqueFile):
|
|
57
|
+
"""Represents an Oracle wallet file (ZIP file)."""
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def get_url(cls) -> str:
|
|
61
|
+
return "api/oracle-wallets/"
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def get_content_param_name(cls) -> str:
|
|
65
|
+
return "zip_archive"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SslZipFile(DataMasqueFile):
|
|
69
|
+
"""Represents a ZIP file of SSL certificates used to establish secure database connections."""
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def get_url(cls) -> str:
|
|
73
|
+
return "api/connection-filesets/"
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def get_content_param_name(cls) -> str:
|
|
77
|
+
return "zip_archive"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SnowflakeKeyFile(DataMasqueFile):
|
|
81
|
+
"""Represents a private SSH key file for Snowflake connections."""
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def get_url(cls) -> str:
|
|
85
|
+
return "api/files/snowflake-keys/"
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def get_content_param_name(cls) -> str:
|
|
89
|
+
return "key_file"
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Typed request and response shapes for the IFM (in-flight masking) HTTP API."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Callable, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
7
|
+
|
|
8
|
+
from datamasque.client.exceptions import DataMasqueUserError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RulesetPlanOptions(BaseModel):
|
|
12
|
+
"""
|
|
13
|
+
Server-defined defaults applied when a mask request omits the corresponding fields.
|
|
14
|
+
|
|
15
|
+
All keys are optional;
|
|
16
|
+
callers can supply any subset (or none) and the IFM server fills in remaining defaults.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(extra="forbid")
|
|
20
|
+
|
|
21
|
+
enabled: Optional[bool] = None
|
|
22
|
+
# NB: Encoding and charset are not currently implemented for IFM.
|
|
23
|
+
# These fields are here just to ensure we can round-trip a `RulesetPlan` object.
|
|
24
|
+
default_encoding: Optional[str] = None
|
|
25
|
+
default_charset: Optional[str] = None
|
|
26
|
+
default_log_level: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class IfmLog(BaseModel):
|
|
30
|
+
"""A single log entry produced by IFM during a mask call or a ruleset-plan validation."""
|
|
31
|
+
|
|
32
|
+
model_config = ConfigDict(extra="allow")
|
|
33
|
+
|
|
34
|
+
log_level: str
|
|
35
|
+
timestamp: str
|
|
36
|
+
message: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class IfmRulesetPlanRef(BaseModel):
|
|
40
|
+
"""Reference to a ruleset plan embedded in a mask response."""
|
|
41
|
+
|
|
42
|
+
model_config = ConfigDict(extra="allow")
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
serial: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RulesetPlan(BaseModel):
|
|
49
|
+
"""
|
|
50
|
+
Unified model for IFM ruleset plans.
|
|
51
|
+
|
|
52
|
+
Collapses the list/detail/create/update response shapes into one model
|
|
53
|
+
with optional fields for parts that differ by endpoint.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
model_config = ConfigDict(extra="allow")
|
|
57
|
+
|
|
58
|
+
name: str
|
|
59
|
+
serial: int
|
|
60
|
+
created_time: datetime
|
|
61
|
+
modified_time: datetime
|
|
62
|
+
options: RulesetPlanOptions
|
|
63
|
+
ruleset_yaml: Optional[str] = None
|
|
64
|
+
logs: Optional[list[IfmLog]] = None
|
|
65
|
+
url: Optional[str] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RulesetPlanCreateRequest(BaseModel):
|
|
69
|
+
"""Request body for `POST /ifm/ruleset-plans/`."""
|
|
70
|
+
|
|
71
|
+
model_config = ConfigDict(extra="forbid")
|
|
72
|
+
|
|
73
|
+
name: str
|
|
74
|
+
ruleset_yaml: str
|
|
75
|
+
options: Optional[RulesetPlanOptions] = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class RulesetPlanUpdateRequest(BaseModel):
|
|
79
|
+
"""Request body for `PUT /ifm/ruleset-plans/{name}/`."""
|
|
80
|
+
|
|
81
|
+
model_config = ConfigDict(extra="forbid")
|
|
82
|
+
|
|
83
|
+
ruleset_yaml: str
|
|
84
|
+
options: Optional[RulesetPlanOptions] = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RulesetPlanPartialUpdateRequest(BaseModel):
|
|
88
|
+
"""Request body for `PATCH /ifm/ruleset-plans/{name}/` — every field is optional."""
|
|
89
|
+
|
|
90
|
+
model_config = ConfigDict(extra="forbid")
|
|
91
|
+
|
|
92
|
+
ruleset_yaml: Optional[str] = None
|
|
93
|
+
options: Optional[RulesetPlanOptions] = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class IfmMaskRequest(BaseModel):
|
|
97
|
+
"""
|
|
98
|
+
Request body for `POST /ruleset-plans/{name}/mask/`.
|
|
99
|
+
|
|
100
|
+
`data` is the list of records to be masked;
|
|
101
|
+
every other field overrides server defaults configured on the plan.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
model_config = ConfigDict(extra="forbid")
|
|
105
|
+
|
|
106
|
+
data: list[Any]
|
|
107
|
+
disable_instance_secret: Optional[bool] = None
|
|
108
|
+
run_secret: Optional[str] = None
|
|
109
|
+
hash_values: Optional[Any] = None
|
|
110
|
+
log_level: Optional[str] = None
|
|
111
|
+
request_id: Optional[str] = None
|
|
112
|
+
ai_engine_url: Optional[str] = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class IfmMaskResult(BaseModel):
|
|
116
|
+
"""
|
|
117
|
+
Response shape for `POST /ruleset-plans/{name}/mask/`.
|
|
118
|
+
|
|
119
|
+
`success` is populated by the client based on the HTTP status the server returned:
|
|
120
|
+
|
|
121
|
+
- `True` — masking completed;
|
|
122
|
+
`data` carries the masked records (possibly an empty list if the request had no input).
|
|
123
|
+
- `False` — the server rejected the request with a soft failure
|
|
124
|
+
(e.g. a masking function received an unsupported value type);
|
|
125
|
+
`data` is omitted and details surface in `logs`.
|
|
126
|
+
|
|
127
|
+
Hard failures (plan not found, auth, transport) still raise rather than producing an `IfmMaskResult`.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
model_config = ConfigDict(extra="allow")
|
|
131
|
+
|
|
132
|
+
success: bool
|
|
133
|
+
request_id: Optional[str] = None
|
|
134
|
+
ruleset_plan: Optional[IfmRulesetPlanRef] = None
|
|
135
|
+
logs: Optional[list[IfmLog]] = None
|
|
136
|
+
data: Optional[list[Any]] = None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class IfmTokenInfo(BaseModel):
|
|
140
|
+
"""Response body for `GET /verify-token/` — the list of scopes granted to the current JWT."""
|
|
141
|
+
|
|
142
|
+
model_config = ConfigDict(extra="allow")
|
|
143
|
+
|
|
144
|
+
scopes: list[str]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class DataMasqueIfmInstanceConfig(BaseModel):
|
|
148
|
+
"""
|
|
149
|
+
Connection configuration for `DataMasqueIfmClient`.
|
|
150
|
+
|
|
151
|
+
`admin_server_base_url` is where JWTs are obtained and refreshed;
|
|
152
|
+
`ifm_base_url` is where the IFM API itself lives
|
|
153
|
+
(typically a separate hostname or the admin server with `/ifm` prefix).
|
|
154
|
+
Exactly one of `password` or `token_source` must be set.
|
|
155
|
+
`token_source` is a user-supplied callable that returns the bare JWT access token string —
|
|
156
|
+
the value issued by the admin server's `/api/auth/jwt/login/` endpoint;
|
|
157
|
+
the client prepends it with `Bearer ` when sending the `Authorization` header.
|
|
158
|
+
The client calls `token_source` on each authentication and refresh,
|
|
159
|
+
so the callable is free to fetch and refresh tokens out-of-band (e.g. from a secrets manager).
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
163
|
+
|
|
164
|
+
admin_server_base_url: str
|
|
165
|
+
ifm_base_url: str
|
|
166
|
+
username: str
|
|
167
|
+
password: Optional[str] = None
|
|
168
|
+
verify_ssl: bool = True
|
|
169
|
+
token_source: Optional[Callable[[], str]] = None
|
|
170
|
+
|
|
171
|
+
@model_validator(mode="after")
|
|
172
|
+
def _validate_auth_source(self) -> "DataMasqueIfmInstanceConfig":
|
|
173
|
+
if (self.password is None) == (self.token_source is None):
|
|
174
|
+
raise DataMasqueUserError(
|
|
175
|
+
"Exactly one of `password` or `token_source` must be provided to `DataMasqueIfmInstanceConfig`."
|
|
176
|
+
)
|
|
177
|
+
return self
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Typed response shape for the license endpoint."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SwitchableLicenseMetadata(BaseModel):
|
|
10
|
+
"""Metadata for switchable license management (AWS Marketplace, etc.)."""
|
|
11
|
+
|
|
12
|
+
model_config = ConfigDict(extra="allow")
|
|
13
|
+
|
|
14
|
+
can_switch_license_source: Optional[bool] = None
|
|
15
|
+
license_source: Optional[str] = None
|
|
16
|
+
license_select_time: Optional[datetime] = None
|
|
17
|
+
aws_account_number: Optional[str] = None
|
|
18
|
+
last_checkout_success_time: Optional[datetime] = None
|
|
19
|
+
last_checkout_success_type: Optional[str] = None
|
|
20
|
+
last_checkout_error: Optional[str] = None
|
|
21
|
+
last_checkout_license_arn: Optional[str] = None
|
|
22
|
+
last_checkout_product_name: Optional[str] = None
|
|
23
|
+
last_checkout_contract_expiry: Optional[datetime] = None
|
|
24
|
+
last_checkout_agreement_id: Optional[str] = None
|
|
25
|
+
last_checkout_agreement_url: Optional[str] = None
|
|
26
|
+
checkout_mode: Optional[str] = None
|
|
27
|
+
selected_product_sku: Optional[str] = None
|
|
28
|
+
allow_fallback: Optional[bool] = None
|
|
29
|
+
last_checkout_success_license_count: Optional[int] = None
|
|
30
|
+
iam_role_arn: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class LicenseInfo(BaseModel):
|
|
34
|
+
"""
|
|
35
|
+
License information returned by `GET /api/license/`.
|
|
36
|
+
|
|
37
|
+
Core fields (`uuid`, `name`, `type`, `is_expired`, `uploadable`)
|
|
38
|
+
are always present in the server response.
|
|
39
|
+
Other fields vary by license type and server version.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
model_config = ConfigDict(extra="allow")
|
|
43
|
+
|
|
44
|
+
uuid: str
|
|
45
|
+
name: str
|
|
46
|
+
type: str
|
|
47
|
+
is_expired: bool
|
|
48
|
+
uploadable: bool
|
|
49
|
+
version: Optional[str] = None
|
|
50
|
+
raw_type: Optional[str] = None
|
|
51
|
+
expiry_date: Optional[datetime] = None
|
|
52
|
+
quota_tb: Optional[float] = None
|
|
53
|
+
maximum_node_count: Optional[int] = None
|
|
54
|
+
row_limit: Optional[int] = None
|
|
55
|
+
platform_name: Optional[str] = None
|
|
56
|
+
platform_code: Optional[str] = None
|
|
57
|
+
days_until_expiry: Optional[int] = None
|
|
58
|
+
is_contract_product: Optional[bool] = None
|
|
59
|
+
contract_license_type: Optional[str] = None
|
|
60
|
+
switchable_license_metadata: Optional[SwitchableLicenseMetadata] = None
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Pagination envelope models matching the DataMasque admin-server and IFM list-endpoint response shapes."""
|
|
2
|
+
|
|
3
|
+
from typing import Generic, Optional, TypeVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Page(BaseModel, Generic[T]):
|
|
11
|
+
"""Admin-server paginated response envelope."""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(extra="allow")
|
|
14
|
+
|
|
15
|
+
count: int
|
|
16
|
+
next: Optional[str] = None
|
|
17
|
+
previous: Optional[str] = None
|
|
18
|
+
results: list[T]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IfmPage(BaseModel, Generic[T]):
|
|
22
|
+
"""IFM paginated response envelope."""
|
|
23
|
+
|
|
24
|
+
model_config = ConfigDict(extra="allow")
|
|
25
|
+
|
|
26
|
+
items: list[T]
|
|
27
|
+
total: int
|
|
28
|
+
limit: int
|
|
29
|
+
offset: int
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from typing import Any, NewType, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
|
+
|
|
6
|
+
from datamasque.client.models.status import ValidationStatus
|
|
7
|
+
|
|
8
|
+
RulesetId = NewType("RulesetId", str)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def unwrap_ruleset_id(value: Any) -> Any:
|
|
12
|
+
"""
|
|
13
|
+
Coerce a `Ruleset` to its `id`; pass other values through unchanged.
|
|
14
|
+
|
|
15
|
+
Used by request-model validators that accept either a `RulesetId`
|
|
16
|
+
or a full `Ruleset` for user convenience.
|
|
17
|
+
Raises `ValueError` if the ruleset has no `id`
|
|
18
|
+
(i.e. the caller hasn't yet created it on the server).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
if isinstance(value, Ruleset):
|
|
22
|
+
if value.id is None:
|
|
23
|
+
raise ValueError("Ruleset has not been created yet (id is None)")
|
|
24
|
+
return value.id
|
|
25
|
+
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RulesetType(enum.Enum):
|
|
30
|
+
"""Ruleset type (database masking or file masking)."""
|
|
31
|
+
|
|
32
|
+
file = "file"
|
|
33
|
+
database = "database"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Ruleset(BaseModel):
|
|
37
|
+
"""Represents a ruleset."""
|
|
38
|
+
|
|
39
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
yaml: str = Field(default="", alias="config_yaml")
|
|
43
|
+
ruleset_type: RulesetType = Field(default=RulesetType.database, alias="mask_type")
|
|
44
|
+
id: Optional[RulesetId] = None
|
|
45
|
+
is_valid: Optional[ValidationStatus] = None
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import NewType, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
|
+
|
|
6
|
+
from datamasque.client.models.status import ValidationStatus
|
|
7
|
+
|
|
8
|
+
RulesetLibraryId = NewType("RulesetLibraryId", str)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RulesetLibrary(BaseModel):
|
|
12
|
+
"""Represents a ruleset library."""
|
|
13
|
+
|
|
14
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
namespace: str = ""
|
|
18
|
+
yaml: Optional[str] = Field(default=None, alias="config_yaml")
|
|
19
|
+
id: Optional[RulesetLibraryId] = None
|
|
20
|
+
is_valid: Optional[ValidationStatus] = None
|
|
21
|
+
created: Optional[datetime] = None
|
|
22
|
+
modified: Optional[datetime] = None
|