datamasque-python 1.1.6__py3-none-any.whl → 1.1.8__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.
@@ -116,12 +116,51 @@ from datamasque.client.models.runs import (
116
116
  RunInfo,
117
117
  UnfinishedRun,
118
118
  )
119
+ from datamasque.client.models.safe_data_preview import (
120
+ BooleanPreview,
121
+ BooleanStatistics,
122
+ ColumnKind,
123
+ ColumnPreview,
124
+ CommonStatistics,
125
+ FirstCharsStatistics,
126
+ LengthEntry,
127
+ LengthsStatistics,
128
+ MaskedFormEntry,
129
+ NumericBin,
130
+ NumericHistograms,
131
+ NumericPreview,
132
+ NumericStatistics,
133
+ NumericSummaries,
134
+ NumericTemporalDisclosureLevel,
135
+ PatternComposition,
136
+ PatternEntry,
137
+ PatternsStatistics,
138
+ SafeDataPreview,
139
+ SafeDataPreviewOptions,
140
+ StringDisclosureLevel,
141
+ StringPreview,
142
+ StringStatistics,
143
+ TemporalBin,
144
+ TemporalHistograms,
145
+ TemporalPreview,
146
+ TemporalStatistics,
147
+ TemporalSummaries,
148
+ UnsupportedPreview,
149
+ UnsupportedStatistics,
150
+ )
119
151
  from datamasque.client.models.status import (
120
152
  AsyncRulesetGenerationTaskStatus,
121
153
  MaskingRunStatus,
154
+ ValidationErrorDetails,
122
155
  ValidationErrorType,
123
156
  ValidationStatus,
124
157
  )
158
+ from datamasque.client.models.table_reference import (
159
+ TableReference,
160
+ TableReferenceFormat,
161
+ TableReferenceId,
162
+ TableReferenceOptions,
163
+ )
125
164
  from datamasque.client.models.user import User, UserId, UserRole
126
165
 
127
166
  __version__ = version("datamasque-python")
@@ -130,6 +169,11 @@ __all__ = [
130
169
  "AsyncRulesetGenerationInProgressError",
131
170
  "AsyncRulesetGenerationTaskStatus",
132
171
  "AzureConnectionConfig",
172
+ "BooleanPreview",
173
+ "BooleanStatistics",
174
+ "ColumnKind",
175
+ "ColumnPreview",
176
+ "CommonStatistics",
133
177
  "ConnectionConfig",
134
178
  "ConnectionId",
135
179
  "ConstraintColumns",
@@ -170,6 +214,7 @@ __all__ = [
170
214
  "FileId",
171
215
  "FileOrContent",
172
216
  "FileRulesetGenerationRequest",
217
+ "FirstCharsStatistics",
173
218
  "ForeignKeyRef",
174
219
  "GitSnapshot",
175
220
  "HashColumnsTableConfig",
@@ -185,16 +230,28 @@ __all__ = [
185
230
  "InvalidLibraryError",
186
231
  "InvalidRulesetError",
187
232
  "JsonPath",
233
+ "LengthEntry",
234
+ "LengthsStatistics",
188
235
  "LicenseInfo",
189
236
  "Locator",
190
237
  "MaskType",
238
+ "MaskedFormEntry",
191
239
  "MaskingRunOptions",
192
240
  "MaskingRunRequest",
193
241
  "MaskingRunStatus",
194
242
  "MongoConnectionConfig",
195
243
  "MountedShareConnectionConfig",
196
244
  "MssqlLinkedServerConnectionConfig",
245
+ "NumericBin",
246
+ "NumericHistograms",
247
+ "NumericPreview",
248
+ "NumericStatistics",
249
+ "NumericSummaries",
250
+ "NumericTemporalDisclosureLevel",
197
251
  "OracleWalletFile",
252
+ "PatternComposition",
253
+ "PatternEntry",
254
+ "PatternsStatistics",
198
255
  "ReferencingForeignKey",
199
256
  "Ruleset",
200
257
  "RulesetGenerationRequest",
@@ -212,6 +269,8 @@ __all__ = [
212
269
  "RunInfo",
213
270
  "RunNotCancellableError",
214
271
  "S3ConnectionConfig",
272
+ "SafeDataPreview",
273
+ "SafeDataPreviewOptions",
215
274
  "SchemaDiscoveryColumn",
216
275
  "SchemaDiscoveryFromConfigRequest",
217
276
  "SchemaDiscoveryPage",
@@ -227,13 +286,28 @@ __all__ = [
227
286
  "SseConfig",
228
287
  "SseSelection",
229
288
  "SslZipFile",
289
+ "StringDisclosureLevel",
290
+ "StringPreview",
291
+ "StringStatistics",
230
292
  "SwitchableLicenseMetadata",
231
293
  "TableConstraints",
294
+ "TableReference",
295
+ "TableReferenceFormat",
296
+ "TableReferenceId",
297
+ "TableReferenceOptions",
298
+ "TemporalBin",
299
+ "TemporalHistograms",
300
+ "TemporalPreview",
301
+ "TemporalStatistics",
302
+ "TemporalSummaries",
232
303
  "UnfinishedRun",
304
+ "UnsupportedPreview",
305
+ "UnsupportedStatistics",
233
306
  "User",
234
307
  "UserId",
235
308
  "UserRole",
236
309
  "UserSelection",
310
+ "ValidationErrorDetails",
237
311
  "ValidationErrorType",
238
312
  "ValidationStatus",
239
313
  ]
@@ -9,6 +9,7 @@ from datamasque.client.ruleset_libraries import RulesetLibraryClient
9
9
  from datamasque.client.rulesets import RulesetClient
10
10
  from datamasque.client.runs import RunClient
11
11
  from datamasque.client.settings import SettingsClient
12
+ from datamasque.client.table_references import TableReferenceClient
12
13
  from datamasque.client.users import UserClient
13
14
 
14
15
  __all__ = ["DataMasqueClient", "FileOrContent", "UploadFile"]
@@ -24,6 +25,7 @@ class DataMasqueClient(
24
25
  DiscoveryClient,
25
26
  DiscoveryConfigClient,
26
27
  DiscoveryConfigLibraryClient,
28
+ TableReferenceClient,
27
29
  UserClient,
28
30
  SettingsClient,
29
31
  ):
@@ -166,6 +166,7 @@ class MongoConnectionConfig(ConnectionConfig):
166
166
  tls_ca_file: str = ""
167
167
  tls_allow_invalid_certificates: bool = False
168
168
  direct_connection: bool = False
169
+ retry_writes: bool = True
169
170
  replica_set: str = ""
170
171
  is_read_only: bool = False
171
172
 
@@ -194,6 +195,8 @@ class MongoConnectionConfig(ConnectionConfig):
194
195
  d.pop("tls_allow_invalid_certificates", None)
195
196
  if not d.get("direct_connection"):
196
197
  d.pop("direct_connection", None)
198
+ if d.get("retry_writes", True):
199
+ d.pop("retry_writes", None)
197
200
  if not d.get("replica_set"):
198
201
  d.pop("replica_set", None)
199
202
  if not d.get("user"):
@@ -10,6 +10,10 @@ from datamasque.client.models.data_selection import HashColumnsTableConfig, Loca
10
10
  from datamasque.client.models.discovery_config import DiscoveryConfig, DiscoveryConfigId, unwrap_discovery_config_id
11
11
  from datamasque.client.models.pagination import Page
12
12
  from datamasque.client.models.runs import RunConnectionRef
13
+ from datamasque.client.models.safe_data_preview import (
14
+ SafeDataPreview,
15
+ SafeDataPreviewOptions,
16
+ )
13
17
 
14
18
 
15
19
  class InDataDiscoveryRule(BaseModel):
@@ -32,6 +36,7 @@ class InDataDiscoveryConfig(BaseModel):
32
36
  non_sensitive_rules: Optional[list[InDataDiscoveryRule]] = None
33
37
  ignore_rules: Optional[list[InDataDiscoveryRule]] = None
34
38
  force: Optional[bool] = None
39
+ safe_data_preview: Optional[SafeDataPreviewOptions] = None
35
40
 
36
41
 
37
42
  class SchemaDiscoveryRequest(BaseModel):
@@ -294,6 +299,7 @@ class SchemaDiscoveryColumn(BaseModel):
294
299
  unique_index_names: list[str]
295
300
  referencing_foreign_keys: list[ReferencingForeignKey]
296
301
  constraint: str # Primary or Unique, or empty string if column does not participate in a PK/UK
302
+ safe_data_preview: Optional[SafeDataPreview] = None
297
303
 
298
304
 
299
305
  class SchemaDiscoveryResult(BaseModel):
@@ -324,6 +330,7 @@ class TableConstraints(BaseModel):
324
330
  primary_keys: Optional[list[ConstraintColumns]] = None
325
331
  unique_keys: Optional[list[ConstraintColumns]] = None
326
332
  foreign_keys: Optional[list[ConstraintColumns]] = None
333
+ row_count: Optional[int] = None
327
334
 
328
335
 
329
336
  class SchemaDiscoveryPage(Page[SchemaDiscoveryResult]):
@@ -356,6 +363,7 @@ class FileDiscoveryLocatorResult(BaseModel):
356
363
  locator: Locator
357
364
  matches: list[FileDiscoveryMatch]
358
365
  data_types: list[str]
366
+ safe_data_preview: Optional[SafeDataPreview] = None
359
367
 
360
368
 
361
369
  class FileDiscoveryFile(BaseModel):
@@ -4,7 +4,7 @@ from typing import Any, NewType, Optional
4
4
  from pydantic import Field
5
5
 
6
6
  from datamasque.client.models.git import GitTrackedEntity
7
- from datamasque.client.models.status import ValidationErrorType, ValidationStatus
7
+ from datamasque.client.models.status import ValidationErrorDetails, ValidationStatus
8
8
 
9
9
  RulesetId = NewType("RulesetId", str)
10
10
 
@@ -44,7 +44,5 @@ class Ruleset(GitTrackedEntity):
44
44
  # Server-populated read-only fields, excluded from request bodies.
45
45
  id: Optional[RulesetId] = Field(default=None, exclude=True)
46
46
  is_valid: Optional[ValidationStatus] = Field(default=None, exclude=True)
47
- validation_error: Optional[str] = Field(default=None, exclude=True)
48
- """Human-readable validation error, or `None` when valid."""
49
- validation_error_type: Optional[ValidationErrorType] = Field(default=None, exclude=True)
50
- """Category of the validation failure, or `None` when valid."""
47
+ validation_errors: list[ValidationErrorDetails] = Field(default_factory=list, exclude=True)
48
+ """Validation errors surfaced by the server; empty when valid."""
@@ -4,7 +4,7 @@ from typing import NewType, Optional
4
4
  from pydantic import Field
5
5
 
6
6
  from datamasque.client.models.git import GitTrackedEntity
7
- from datamasque.client.models.status import ValidationStatus
7
+ from datamasque.client.models.status import ValidationErrorDetails, ValidationStatus
8
8
 
9
9
  RulesetLibraryId = NewType("RulesetLibraryId", str)
10
10
 
@@ -19,7 +19,7 @@ class RulesetLibrary(GitTrackedEntity):
19
19
  # Server-populated read-only fields, excluded from request bodies.
20
20
  id: Optional[RulesetLibraryId] = Field(default=None, exclude=True)
21
21
  is_valid: Optional[ValidationStatus] = Field(default=None, exclude=True)
22
- validation_error: Optional[str] = Field(default=None, exclude=True)
23
- """Human-readable validation error, or `None` when valid."""
22
+ validation_errors: list[ValidationErrorDetails] = Field(default_factory=list, exclude=True)
23
+ """Validation errors surfaced by the server; empty when valid."""
24
24
  created: Optional[datetime] = Field(default=None, exclude=True)
25
25
  modified: Optional[datetime] = Field(default=None, exclude=True)
@@ -0,0 +1,282 @@
1
+ """Typed request and response shapes for Safe Data Preview, part of In-Data Discovery."""
2
+
3
+ from enum import Enum
4
+ from typing import Annotated, Literal, Optional, Union
5
+
6
+ from pydantic import (
7
+ BaseModel,
8
+ ConfigDict,
9
+ Field,
10
+ )
11
+
12
+
13
+ class StringDisclosureLevel(Enum):
14
+ """How much detail a string Safe Data Preview reveals."""
15
+
16
+ lengths = "lengths"
17
+ patterns = "patterns"
18
+ first_chars = "first_chars"
19
+
20
+
21
+ class NumericTemporalDisclosureLevel(Enum):
22
+ """How much detail a numeric or temporal Safe Data Preview reveals."""
23
+
24
+ summaries = "summaries"
25
+ histograms = "histograms"
26
+
27
+
28
+ class SafeDataPreviewOptions(BaseModel):
29
+ """Configures whether Safe Data Preview runs and how much detail each column reveals."""
30
+
31
+ model_config = ConfigDict(extra="forbid")
32
+
33
+ enabled: Optional[bool] = None
34
+ min_group_size: Optional[int] = None
35
+ string_level: Optional[StringDisclosureLevel] = None
36
+ numeric_level: Optional[NumericTemporalDisclosureLevel] = None
37
+ temporal_level: Optional[NumericTemporalDisclosureLevel] = None
38
+
39
+
40
+ class ColumnKind(str, Enum):
41
+ """Which shape a column's Safe Data Preview takes."""
42
+
43
+ boolean = "boolean"
44
+ numeric = "numeric"
45
+ string = "string"
46
+ temporal = "temporal"
47
+ unsupported = "unsupported"
48
+
49
+
50
+ class CommonStatistics(BaseModel):
51
+ """Row counts shared by every preview kind."""
52
+
53
+ model_config = ConfigDict(extra="allow")
54
+
55
+ count_row: int
56
+ count_null: int
57
+ count_distinct: Optional[int] = None
58
+
59
+
60
+ class LengthEntry(BaseModel):
61
+ """A value length and how many sampled rows had it."""
62
+
63
+ model_config = ConfigDict(extra="allow")
64
+
65
+ length: int
66
+ count: int
67
+
68
+
69
+ class LengthsStatistics(BaseModel):
70
+ """Distribution of value lengths."""
71
+
72
+ model_config = ConfigDict(extra="allow")
73
+
74
+ min: int
75
+ max: int
76
+ mean: float
77
+ median: float
78
+ most_common: list[LengthEntry]
79
+
80
+
81
+ class PatternEntry(BaseModel):
82
+ """A masked character pattern and how many sampled rows matched it."""
83
+
84
+ model_config = ConfigDict(extra="allow")
85
+
86
+ pattern: str
87
+ count: int
88
+
89
+
90
+ class PatternComposition(BaseModel):
91
+ """The proportion of letter, digit, and other characters across sampled values."""
92
+
93
+ model_config = ConfigDict(extra="allow")
94
+
95
+ letter: float
96
+ digit: float
97
+ other: float
98
+
99
+
100
+ class PatternsStatistics(BaseModel):
101
+ """The most common character patterns and the overall character composition."""
102
+
103
+ model_config = ConfigDict(extra="allow")
104
+
105
+ top: list[PatternEntry]
106
+ composition: PatternComposition
107
+
108
+
109
+ class MaskedFormEntry(BaseModel):
110
+ """A masked leading form of a value and how many sampled rows had it."""
111
+
112
+ model_config = ConfigDict(extra="allow")
113
+
114
+ masked: str
115
+ count: int
116
+
117
+
118
+ class FirstCharsStatistics(BaseModel):
119
+ """The most common masked leading forms."""
120
+
121
+ model_config = ConfigDict(extra="allow")
122
+
123
+ top: list[MaskedFormEntry]
124
+
125
+
126
+ class StringStatistics(BaseModel):
127
+ """String preview statistics; `patterns`/`first_chars` appear at or above certain disclosure levels."""
128
+
129
+ model_config = ConfigDict(extra="allow")
130
+
131
+ lengths: LengthsStatistics
132
+ patterns: Optional[PatternsStatistics] = None
133
+ first_chars: Optional[FirstCharsStatistics] = None
134
+
135
+
136
+ class NumericSummaries(BaseModel):
137
+ """Numeric mean and percentile summary."""
138
+
139
+ model_config = ConfigDict(extra="allow")
140
+
141
+ mean: float
142
+ q1: float
143
+ q2: float
144
+ q3: float
145
+ p5: float
146
+ p95: float
147
+
148
+
149
+ class NumericBin(BaseModel):
150
+ """One half-open interval `[lower_bound, upper_bound)` of a numeric histogram and its row count."""
151
+
152
+ model_config = ConfigDict(extra="allow")
153
+
154
+ lower_bound: float
155
+ upper_bound: float
156
+ count: int
157
+
158
+
159
+ class NumericHistograms(BaseModel):
160
+ """The bins of a numeric histogram."""
161
+
162
+ model_config = ConfigDict(extra="allow")
163
+
164
+ bins: list[NumericBin]
165
+
166
+
167
+ class NumericStatistics(BaseModel):
168
+ """Numeric preview statistics; `histograms` is present only at the histogram disclosure level."""
169
+
170
+ model_config = ConfigDict(extra="allow")
171
+
172
+ summaries: NumericSummaries
173
+ histograms: Optional[NumericHistograms] = None
174
+
175
+
176
+ class TemporalSummaries(BaseModel):
177
+ """Temporal mean and percentile summary, each an ISO-formatted string."""
178
+
179
+ model_config = ConfigDict(extra="allow")
180
+
181
+ mean: str
182
+ q1: str
183
+ q2: str
184
+ q3: str
185
+ p5: str
186
+ p95: str
187
+
188
+
189
+ class TemporalBin(BaseModel):
190
+ """One interval of a temporal histogram, with ISO-formatted bounds and a human-readable label."""
191
+
192
+ model_config = ConfigDict(extra="allow")
193
+
194
+ lower_bound: str
195
+ upper_bound: str
196
+ label: str
197
+ count: int
198
+
199
+
200
+ class TemporalHistograms(BaseModel):
201
+ """The bins of a temporal histogram."""
202
+
203
+ model_config = ConfigDict(extra="allow")
204
+
205
+ bins: list[TemporalBin]
206
+
207
+
208
+ class TemporalStatistics(BaseModel):
209
+ """Temporal preview statistics; `histograms` is present only at the histogram disclosure level."""
210
+
211
+ model_config = ConfigDict(extra="allow")
212
+
213
+ summaries: TemporalSummaries
214
+ histograms: Optional[TemporalHistograms] = None
215
+
216
+
217
+ class BooleanStatistics(BaseModel):
218
+ """Counts of true and false values."""
219
+
220
+ model_config = ConfigDict(extra="allow")
221
+
222
+ count_true: int
223
+ count_false: int
224
+
225
+
226
+ class UnsupportedStatistics(BaseModel):
227
+ """Why a column could not be previewed."""
228
+
229
+ model_config = ConfigDict(extra="allow")
230
+
231
+ reason: str
232
+
233
+
234
+ class ColumnPreview(BaseModel):
235
+ """Fields present on every Safe Data Preview."""
236
+
237
+ model_config = ConfigDict(extra="allow")
238
+
239
+ # For grouped file discovery, the file whose sample produced this preview; None for database columns.
240
+ sampled_from: Optional[str] = None
241
+ statistics_common: CommonStatistics
242
+
243
+
244
+ class StringPreview(ColumnPreview):
245
+ """A preview of a string column."""
246
+
247
+ kind: Literal[ColumnKind.string] = ColumnKind.string
248
+ statistics_kind: StringStatistics
249
+
250
+
251
+ class NumericPreview(ColumnPreview):
252
+ """A preview of a numeric column."""
253
+
254
+ kind: Literal[ColumnKind.numeric] = ColumnKind.numeric
255
+ statistics_kind: NumericStatistics
256
+
257
+
258
+ class TemporalPreview(ColumnPreview):
259
+ """A preview of a date/time column."""
260
+
261
+ kind: Literal[ColumnKind.temporal] = ColumnKind.temporal
262
+ statistics_kind: TemporalStatistics
263
+
264
+
265
+ class BooleanPreview(ColumnPreview):
266
+ """A preview of a boolean column."""
267
+
268
+ kind: Literal[ColumnKind.boolean] = ColumnKind.boolean
269
+ statistics_kind: BooleanStatistics
270
+
271
+
272
+ class UnsupportedPreview(ColumnPreview):
273
+ """A column the preview ran against but declined; `statistics_kind.reason` says why."""
274
+
275
+ kind: Literal[ColumnKind.unsupported] = ColumnKind.unsupported
276
+ statistics_kind: UnsupportedStatistics
277
+
278
+
279
+ SafeDataPreview = Annotated[
280
+ Union[StringPreview, NumericPreview, TemporalPreview, BooleanPreview, UnsupportedPreview],
281
+ Field(discriminator="kind"),
282
+ ]
@@ -1,4 +1,7 @@
1
1
  import enum
2
+ from typing import Optional
3
+
4
+ from pydantic import BaseModel
2
5
 
3
6
 
4
7
  class ValidationStatus(enum.Enum):
@@ -11,7 +14,7 @@ class ValidationStatus(enum.Enum):
11
14
 
12
15
 
13
16
  class ValidationErrorType(enum.Enum):
14
- """Categorises why a ruleset failed validation (see `Ruleset.validation_error_type`)."""
17
+ """Categorises why validation failed (see `ValidationErrorDetails.validation_error_type`)."""
15
18
 
16
19
  ruleset = "ruleset"
17
20
  library_missing = "library_missing"
@@ -19,6 +22,15 @@ class ValidationErrorType(enum.Enum):
19
22
  expansion = "expansion" # The ruleset is not valid once its library references are expanded.
20
23
 
21
24
 
25
+ class ValidationErrorDetails(BaseModel):
26
+ """A single validation error, with its position in the YAML when available."""
27
+
28
+ message: str
29
+ validation_error_type: Optional[ValidationErrorType] = None
30
+ line_number: Optional[int] = None
31
+ column_number: Optional[int] = None
32
+
33
+
22
34
  class MaskingRunStatus(enum.Enum):
23
35
  """List of valid masking run statuses."""
24
36
 
@@ -0,0 +1,101 @@
1
+ """Table reference models for the DataMasque API."""
2
+
3
+ import enum
4
+ from datetime import datetime
5
+ from typing import NewType, Optional, Union
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
8
+
9
+ from datamasque.client.models.connection import ConnectionConfig, ConnectionId, unwrap_connection_id
10
+
11
+ TableReferenceId = NewType("TableReferenceId", str)
12
+
13
+
14
+ class TableReferenceFormat(enum.Enum):
15
+ """
16
+ File format of the data a table reference points at.
17
+
18
+ Only meaningful for a table reference on a file connection,
19
+ and an explicit choice: the source path's suffix carries no meaning.
20
+ """
21
+
22
+ csv = "csv"
23
+ parquet = "parquet"
24
+
25
+
26
+ class TableReferenceOptions(BaseModel):
27
+ """
28
+ Format and CSV parsing options for a table reference.
29
+
30
+ These apply to file connections only —
31
+ they are ignored for a table reference on a database connection,
32
+ and the CSV options are ignored when the `format` is `parquet`.
33
+
34
+ There is deliberately no header toggle:
35
+ a CSV identity map's columns must be addressable by name,
36
+ so the first row is always read as the header.
37
+
38
+ Every option has a default, and the server fills in any option omitted from a request,
39
+ so the values here are the same defaults the server applies.
40
+ """
41
+
42
+ model_config = ConfigDict(extra="allow", populate_by_name=True)
43
+
44
+ format: TableReferenceFormat = TableReferenceFormat.csv
45
+ delimiter: str = ","
46
+ encoding: str = "utf-8"
47
+ quotechar: str = '"'
48
+ null_string: Optional[str] = None
49
+ """The string in the source data to read as a null value, or `None` to read no value as null."""
50
+
51
+
52
+ class TableReference(BaseModel):
53
+ """
54
+ Represents a named, persisted reference to a table of identity data held outside DataMasque.
55
+
56
+ The data lives in the referenced connection —
57
+ a CSV or Parquet file in a file connection, or a table in a database connection —
58
+ so a ruleset can reference the identity map once, by name,
59
+ instead of re-declaring the source each time.
60
+
61
+ A table reference stores no credentials of its own; it borrows the referenced connection's.
62
+
63
+ `source` is interpreted according to that connection's own type:
64
+
65
+ * file connection — a path to the file within the connection's fileset.
66
+ The format comes from `options.format`, never from the path's suffix.
67
+ * database connection — a dotted, schema-qualified `schema.table` reference.
68
+
69
+ Names are unique across live table references (a deleted reference frees its name for reuse),
70
+ and the referenced connection must not be archived.
71
+ """
72
+
73
+ model_config = ConfigDict(extra="allow", populate_by_name=True)
74
+
75
+ name: str
76
+ connection: Union[ConnectionId, ConnectionConfig]
77
+ """The connection the data lives in. Accepts a `ConnectionId` or a `ConnectionConfig` (its `id` is extracted)."""
78
+ source: str
79
+ options: Optional[TableReferenceOptions] = None
80
+ """Format and CSV parsing options; when `None`, the server applies its defaults on create."""
81
+
82
+ # Server-populated read-only fields, excluded from request bodies.
83
+ id: Optional[TableReferenceId] = Field(default=None, exclude=True)
84
+ created: Optional[datetime] = Field(default=None, exclude=True)
85
+ modified: Optional[datetime] = Field(default=None, exclude=True)
86
+
87
+ @field_validator("connection", mode="before")
88
+ @classmethod
89
+ def _unwrap_connection(cls, value: object) -> object:
90
+ return unwrap_connection_id(value)
91
+
92
+ @property
93
+ def connection_id(self) -> ConnectionId:
94
+ """
95
+ The ID of the referenced connection.
96
+
97
+ `connection` accepts a whole `ConnectionConfig` for convenience but always holds an ID
98
+ once validated, so this narrows the declared type back down for callers reading it.
99
+ """
100
+
101
+ return ConnectionId(unwrap_connection_id(self.connection))
@@ -64,7 +64,7 @@ class RulesetLibraryClient(BaseClient):
64
64
  Creates a new ruleset library on the server.
65
65
 
66
66
  Sets the library's server-assigned fields
67
- (`id`, `is_valid`, `validation_error`, `git`, `created`, `modified`) and returns the library.
67
+ (`id`, `is_valid`, `validation_errors`, `git`, `created`, `modified`) and returns the library.
68
68
  """
69
69
 
70
70
  data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
@@ -72,7 +72,7 @@ class RulesetLibraryClient(BaseClient):
72
72
  created_library = RulesetLibrary.model_validate(response.json())
73
73
  library.id = created_library.id
74
74
  library.is_valid = created_library.is_valid
75
- library.validation_error = created_library.validation_error
75
+ library.validation_errors = created_library.validation_errors
76
76
  library.git = created_library.git
77
77
  library.created = created_library.created
78
78
  library.modified = created_library.modified
@@ -93,7 +93,7 @@ class RulesetLibraryClient(BaseClient):
93
93
  response = self.make_request("PUT", f"/api/ruleset-libraries/{library.id}/", data=data)
94
94
  updated_library = RulesetLibrary.model_validate(response.json())
95
95
  library.is_valid = updated_library.is_valid
96
- library.validation_error = updated_library.validation_error
96
+ library.validation_errors = updated_library.validation_errors
97
97
  library.git = updated_library.git
98
98
  library.modified = updated_library.modified
99
99
  logger.debug('Update of ruleset library "%s" successful', library.name)
@@ -20,7 +20,7 @@ class RulesetClient(BaseClient):
20
20
  """
21
21
  Creates or updates a ruleset.
22
22
 
23
- Populates the given ruleset's `id`, `is_valid`, `validation_error`, `validation_error_type`,
23
+ Populates the given ruleset's `id`, `is_valid`, `validation_errors`,
24
24
  and `git` fields from the server response, and returns the same ruleset instance for convenience.
25
25
  """
26
26
 
@@ -29,8 +29,7 @@ class RulesetClient(BaseClient):
29
29
  created = Ruleset.model_validate(response.json())
30
30
  ruleset.id = created.id
31
31
  ruleset.is_valid = created.is_valid
32
- ruleset.validation_error = created.validation_error
33
- ruleset.validation_error_type = created.validation_error_type
32
+ ruleset.validation_errors = created.validation_errors
34
33
  ruleset.git = created.git
35
34
 
36
35
  if response.status_code == 201:
@@ -0,0 +1,144 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from datamasque.client.base import BaseClient
5
+ from datamasque.client.exceptions import DataMasqueApiError
6
+ from datamasque.client.models.table_reference import TableReference, TableReferenceId
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class TableReferenceClient(BaseClient):
12
+ """Table reference CRUD API methods. Mixed into `DataMasqueClient`."""
13
+
14
+ def list_table_references(self) -> list[TableReference]:
15
+ """
16
+ Lists all table references.
17
+
18
+ Unlike most list endpoints, this one is not paginated;
19
+ the server returns every table reference in a single response.
20
+ Deleted table references are archived rather than removed, and are not listed.
21
+ """
22
+
23
+ response = self.make_request("GET", "/api/table-references/")
24
+ return [TableReference.model_validate(item) for item in response.json()]
25
+
26
+ def get_table_reference(self, table_reference_id: TableReferenceId) -> TableReference:
27
+ """Retrieves a single table reference by ID."""
28
+
29
+ response = self.make_request("GET", f"/api/table-references/{table_reference_id}/")
30
+ return TableReference.model_validate(response.json())
31
+
32
+ def get_table_reference_by_name(self, name: str) -> Optional[TableReference]:
33
+ """
34
+ Looks for a table reference with the given name (case-sensitive, exact match).
35
+
36
+ Names are unique across live table references, so at most one can match.
37
+ Returns it if found, otherwise `None`.
38
+
39
+ The endpoint takes no name filter, so the match is made client-side over the full listing.
40
+ """
41
+
42
+ matches = [reference for reference in self.list_table_references() if reference.name == name]
43
+ if not matches:
44
+ return None
45
+
46
+ return matches[0]
47
+
48
+ def _get_table_reference_id_by_name(self, name: str) -> Optional[TableReferenceId]:
49
+ """Return the ID of the table reference with the given name, or `None` if there is none."""
50
+
51
+ response = self.make_request("GET", "/api/table-references/")
52
+ matches = [item for item in response.json() if item.get("name") == name]
53
+ if not matches:
54
+ return None
55
+
56
+ existing_id = matches[0].get("id")
57
+ if existing_id is None:
58
+ raise DataMasqueApiError(
59
+ f'Server returned a table reference named "{name}" without an `id`.',
60
+ response=response,
61
+ )
62
+
63
+ return TableReferenceId(existing_id)
64
+
65
+ def create_table_reference(self, table_reference: TableReference) -> TableReference:
66
+ """
67
+ Creates a new table reference on the server.
68
+
69
+ Sets the table reference's server-assigned fields (`id`, `created`, `modified`)
70
+ and its `options` as stored by the server (with any omitted option filled in with its default),
71
+ then returns the table reference.
72
+ """
73
+
74
+ data = table_reference.model_dump(by_alias=True, mode="json")
75
+ if data.get("options") is None:
76
+ data.pop("options", None)
77
+ response = self.make_request("POST", "/api/table-references/", data=data)
78
+ created = TableReference.model_validate(response.json())
79
+ table_reference.id = created.id
80
+ table_reference.options = created.options
81
+ table_reference.created = created.created
82
+ table_reference.modified = created.modified
83
+ logger.info('Creation of table reference "%s" successful', table_reference.name)
84
+ return table_reference
85
+
86
+ def update_table_reference(self, table_reference: TableReference) -> TableReference:
87
+ """
88
+ Performs a full update of the table reference.
89
+
90
+ The table reference must have its `id` set
91
+ (i.e., it must have been previously created or retrieved from the server).
92
+
93
+ `options` are replaced wholesale when set, so an option left at its model default
94
+ is sent as that default rather than keeping whatever the server had stored.
95
+ Leaving `options` as `None` sends none at all, which leaves the stored options untouched.
96
+ """
97
+
98
+ if table_reference.id is None:
99
+ raise ValueError("Cannot update a table reference that has not been created yet (id is None)")
100
+
101
+ data = table_reference.model_dump(by_alias=True, mode="json")
102
+ if data.get("options") is None:
103
+ data.pop("options", None)
104
+ response = self.make_request("PUT", f"/api/table-references/{table_reference.id}/", data=data)
105
+ updated = TableReference.model_validate(response.json())
106
+ table_reference.options = updated.options
107
+ table_reference.modified = updated.modified
108
+ logger.debug('Update of table reference "%s" successful', table_reference.name)
109
+ return table_reference
110
+
111
+ def create_or_update_table_reference(self, table_reference: TableReference) -> TableReference:
112
+ """
113
+ Creates the table reference, or updates the existing one with the same name.
114
+
115
+ Sets the table reference's `id` property.
116
+ """
117
+
118
+ existing_id = self._get_table_reference_id_by_name(table_reference.name)
119
+ if existing_id is not None:
120
+ table_reference.id = existing_id
121
+ return self.update_table_reference(table_reference)
122
+
123
+ return self.create_table_reference(table_reference)
124
+
125
+ def delete_table_reference_by_id_if_exists(self, table_reference_id: TableReferenceId) -> None:
126
+ """
127
+ Deletes the table reference with the given ID.
128
+
129
+ The server archives rather than removes it, which frees its name for reuse.
130
+ No-op if the table reference does not exist.
131
+ """
132
+
133
+ self._delete_if_exists(f"/api/table-references/{table_reference_id}/")
134
+
135
+ def delete_table_reference_by_name_if_exists(self, name: str) -> None:
136
+ """
137
+ Deletes the table reference with the given name.
138
+
139
+ No-op if no such table reference exists.
140
+ """
141
+
142
+ table_reference_id = self._get_table_reference_id_by_name(name)
143
+ if table_reference_id is not None:
144
+ self.delete_table_reference_by_id_if_exists(table_reference_id)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datamasque-python
3
- Version: 1.1.6
3
+ Version: 1.1.8
4
4
  Summary: Official Python client for the DataMasque data-masking API.
5
5
  Project-URL: Homepage, https://datamasque.com/
6
6
  Project-URL: Documentation, https://datamasque-python.readthedocs.io/
@@ -1,25 +1,26 @@
1
- datamasque/client/__init__.py,sha256=8GtJwv76dHgVDFGpE5YEMC0B2KIj6W8D3M3StwjvyXo,6551
1
+ datamasque/client/__init__.py,sha256=Tq3xY-x3yQj5E05q5YJFxXzpYYEu_eOiKSGmX-IINHI,8323
2
2
  datamasque/client/base.py,sha256=cXU4dluL9e1N1F2pV48iIvebv6OHt7Kh4Ka2y9vkOPo,14296
3
3
  datamasque/client/connections.py,sha256=EFinx8fJRme0mTxuWY3d29UnmUFbsQhMaUQT0Ma2PK4,2885
4
4
  datamasque/client/discovery.py,sha256=MTDiif6YWkv64hHXZ8YJxtRhoeKIvQVfjNYvjCIZCXE,21261
5
5
  datamasque/client/discovery_config_libraries.py,sha256=Ne8vzY9NDoY11O2HCSA8le9KiTlJPSoznKy3PL05lqY,7628
6
6
  datamasque/client/discovery_configs.py,sha256=G-W-X_KizjjQLa-Rb6ixLjlkxbkCe4WGCOfYZrGtxIk,6420
7
- datamasque/client/dmclient.py,sha256=rsQyc-awujpaWKr_CEYLeDDo7Y8zrb4pHxxs-AqV-Z4,1770
7
+ datamasque/client/dmclient.py,sha256=w8aloPHL8YGKfESUzn62G9XrVovDoGlqrktX_KuDL7U,1864
8
8
  datamasque/client/exceptions.py,sha256=YpM_vhnl31_02KQmx-NPP2AxR3gKYcmcxyb_35G9HVw,3755
9
9
  datamasque/client/files.py,sha256=vc2tZgT8RdwuCObIQ7H-Qjb80H1PBFqD0nG5Y1tOA_o,3503
10
10
  datamasque/client/ifm.py,sha256=uIMxpLIPvDiDO1m4bxezixNIUFIFY0MXWRMQNvTbpTA,11858
11
11
  datamasque/client/license.py,sha256=pluYaSU168OC6_laB9bM9H3Vuuxs8wa9gujCJvtoJCk,1392
12
12
  datamasque/client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- datamasque/client/ruleset_libraries.py,sha256=bMUMBHdxjDyFOziw9LMRy5xChPvHvCjpjAUoVeTm1e0,7086
14
- datamasque/client/rulesets.py,sha256=Fsnsv-uAhF1DBnLI-pRpOrWJ7aCSZspcuU1r0RYTzUc,2655
13
+ datamasque/client/ruleset_libraries.py,sha256=s--iZKB8EAC7bNYrU3ZI0olSWU-mXJoyUBOgJPfQ0xQ,7091
14
+ datamasque/client/rulesets.py,sha256=gzxn9mR7PhCySRUUB1cyl4aLmEfuDKj8BvQUq8OgLH8,2563
15
15
  datamasque/client/runs.py,sha256=l_JpYgelqsDKW_O6LtsHC60IMXHG9ak_RwVnuvJKE1I,7703
16
16
  datamasque/client/settings.py,sha256=Ui8AyR2XdoW8MZ9FIrGn2jm8DLzUGWIL8i2DOTvu7hc,2898
17
17
  datamasque/client/spcs.py,sha256=geHrBQ6WLWsMGpSMV8hmK5SFAwftrOU0DXkrR2orOmo,7532
18
+ datamasque/client/table_references.py,sha256=dPyB59Sz4iLuF1lc7xffgf9qeNTFA5srT55E8TnFLvk,6092
18
19
  datamasque/client/users.py,sha256=VCUo2CJyOw4-aO_3mp_w_0BcSoa6-h1HcReMcAMyumw,3701
19
20
  datamasque/client/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- datamasque/client/models/connection.py,sha256=CPT2wF1dVMs-N8Jca7hUhegtRk_mxPKU8XhWV43C0xE,17419
21
+ datamasque/client/models/connection.py,sha256=loLy4RCiIfY9kvBFspSqJSRhHqhcJxm_mQzXwMRS4VU,17529
21
22
  datamasque/client/models/data_selection.py,sha256=406yyUZ5NmLBSql2lYM1gsTWY5GWmnutmF-DjznAoLc,1904
22
- datamasque/client/models/discovery.py,sha256=WHSIkFebQ5m47K_BdxY1lvCeeF23q_BbZan7Ryuh5vY,13198
23
+ datamasque/client/models/discovery.py,sha256=w3DXPux15BDMVs5urTpU2DM8pnWUzS3LFzCr6ctymxk,13517
23
24
  datamasque/client/models/discovery_config.py,sha256=1CHMgSxMXuarPGfRcz9wX7jeZLn1SDw0UEe-L5naE54,1917
24
25
  datamasque/client/models/discovery_config_library.py,sha256=5MLIe3SCQ6IbhWbZ_avDL9H65fW4O5h3HtjlgEfcFGg,1378
25
26
  datamasque/client/models/dm_instance.py,sha256=6h0WNlI1qt6jkrYFapIvuSJFkhRCcwbpx0PLG5NsrGw,2219
@@ -28,12 +29,14 @@ datamasque/client/models/git.py,sha256=F0KdMFe2vMyL-9tp3SGKGeOlbD1gbcjQn0jpOzNzy
28
29
  datamasque/client/models/ifm.py,sha256=j0Ef2BZYTk6MNZO6HS6mW0qWs1_wWD9PlUP_Y6WyBQI,5563
29
30
  datamasque/client/models/license.py,sha256=OqIn4Sx3ATBStgt5KNiCOBChTTFYLBsyGj7inAl8oB4,2113
30
31
  datamasque/client/models/pagination.py,sha256=egg9aO2cf6KUDwDANPu5RxpJbdRKdwUdCQAtusnuf1c,651
31
- datamasque/client/models/ruleset.py,sha256=ZYv6SmcrONV5rLK0tKjhlKCwZiCU1dMYtH7olxI4l5A,1678
32
- datamasque/client/models/ruleset_library.py,sha256=5UbL_M47mgPedo8xt4q5crUh4aeOVoEMAEKbGO8CfiY,950
32
+ datamasque/client/models/ruleset.py,sha256=FDnvewJQvbeH73LpZiwYg3BG9Ya4yhmKKGmO1BKZQPk,1549
33
+ datamasque/client/models/ruleset_library.py,sha256=Pw6Udr7pMgPt1AcG2ZhUXOPBS7tTvpw13UZNLILhZbQ,1003
33
34
  datamasque/client/models/runs.py,sha256=UtPMGCJFLFP1f2nyVGAbl3A5_wAONrRNJ63y43Hbpi0,6213
34
- datamasque/client/models/status.py,sha256=UUEDHd3CQZA1Mf-1LyhAiE4COuCWvAz5oJx5ECVIwp4,2868
35
+ datamasque/client/models/safe_data_preview.py,sha256=BiI9d13KdQrTin2JfbjiRiODjns_FipWmfcEyfQzN1E,6778
36
+ datamasque/client/models/status.py,sha256=WiSMEy_YxdcIA46ioYpAEu8OK8UNDoXpV1W7IUhhyVY,3219
37
+ datamasque/client/models/table_reference.py,sha256=55fKdHjC2TQnUmbp0XiWqfeFGXqcq4wXjbWgMaN3p3c,3835
35
38
  datamasque/client/models/user.py,sha256=UGAUzgJkf78m24_zFXXoA99zdut48BXkX_ivV8yq1Vc,2043
36
- datamasque_python-1.1.6.dist-info/METADATA,sha256=5xb8jZEaGhZfMu2NOImE3tuTI2LBlV8O6JScm_Q-pBw,4497
37
- datamasque_python-1.1.6.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
38
- datamasque_python-1.1.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
39
- datamasque_python-1.1.6.dist-info/RECORD,,
39
+ datamasque_python-1.1.8.dist-info/METADATA,sha256=_uqOykCTG6M-qHCHbFxWOIzY5hNpxh2L2GOYfrH4Zr0,4497
40
+ datamasque_python-1.1.8.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
41
+ datamasque_python-1.1.8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
42
+ datamasque_python-1.1.8.dist-info/RECORD,,