Skip to content

Commit 02f1294

Browse files
Merge pull request #33 from datamasque/DM-3801-config-library-python-client
Dm 3801 config library python client
2 parents d734074 + 4f06c46 commit 02f1294

9 files changed

Lines changed: 843 additions & 3 deletions

File tree

HISTORY.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
History
33
=======
44

5+
1.1.6 (unreleased)
6+
------------------
7+
8+
* Added discovery-config-library management APIs (``list_discovery_config_libraries``, ``create_discovery_config_library``, and friends).
9+
510
1.1.5 (2026-06-29)
611
------------------
712

datamasque/client/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
TableConstraints,
8181
)
8282
from datamasque.client.models.discovery_config import DiscoveryConfig, DiscoveryConfigId, DiscoveryConfigType
83+
from datamasque.client.models.discovery_config_library import DiscoveryConfigLibrary, DiscoveryConfigLibraryId
8384
from datamasque.client.models.dm_instance import DataMasqueInstanceConfig
8485
from datamasque.client.models.files import (
8586
DataMasqueFile,
@@ -148,6 +149,8 @@
148149
"DatabricksConnectionConfig",
149150
"DiscoveryConfig",
150151
"DiscoveryConfigId",
152+
"DiscoveryConfigLibrary",
153+
"DiscoveryConfigLibraryId",
151154
"DiscoveryConfigNotFoundError",
152155
"DiscoveryConfigType",
153156
"DiscoveryMatch",
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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.discovery_config import DiscoveryConfigType
7+
from datamasque.client.models.discovery_config_library import DiscoveryConfigLibrary, DiscoveryConfigLibraryId
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class DiscoveryConfigLibraryClient(BaseClient):
13+
"""Discovery config library CRUD API methods. Mixed into `DataMasqueClient`."""
14+
15+
def list_discovery_config_libraries(self) -> list[DiscoveryConfigLibrary]:
16+
"""
17+
Lists all discovery config libraries.
18+
19+
Unlike most list endpoints, this one is not paginated;
20+
the server returns every library in a single response.
21+
Note: the YAML content is not included in the list response for performance.
22+
Use `get_discovery_config_library` to retrieve the full library with its YAML body.
23+
"""
24+
25+
response = self.make_request("GET", "/api/discovery/config-libraries/")
26+
return [DiscoveryConfigLibrary.model_validate(item) for item in response.json()]
27+
28+
def get_discovery_config_library(self, library_id: DiscoveryConfigLibraryId) -> DiscoveryConfigLibrary:
29+
"""Retrieves a single discovery config library by ID, including its YAML content."""
30+
31+
response = self.make_request("GET", f"/api/discovery/config-libraries/{library_id}/")
32+
return DiscoveryConfigLibrary.model_validate(response.json())
33+
34+
def _get_discovery_config_library_id_by_name(
35+
self, name: str, config_type: DiscoveryConfigType, namespace: str
36+
) -> Optional[DiscoveryConfigLibraryId]:
37+
"""
38+
Returns the ID of the library with the given name, type, and namespace, or `None` if there is none.
39+
40+
The listing is filtered by name to keep the response small;
41+
type and namespace are matched client-side,
42+
since the API's namespace filter cannot select the default (empty) namespace.
43+
"""
44+
45+
response = self.make_request(
46+
"GET",
47+
"/api/discovery/config-libraries/",
48+
params={"name_exact": name},
49+
)
50+
entries = [DiscoveryConfigLibrary.model_validate(item) for item in response.json()]
51+
matches = [
52+
entry
53+
for entry in entries
54+
if entry.name == name and entry.namespace == namespace and entry.config_type is config_type
55+
]
56+
if not matches:
57+
return None
58+
59+
if matches[0].id is None:
60+
raise DataMasqueApiError(
61+
"Server returned a discovery config library list entry without an `id`.",
62+
response=response,
63+
)
64+
65+
return matches[0].id
66+
67+
def get_discovery_config_library_by_name(
68+
self, name: str, config_type: DiscoveryConfigType, namespace: str = ""
69+
) -> Optional[DiscoveryConfigLibrary]:
70+
"""
71+
Looks for a discovery config library matching the given name, type, and namespace (case-sensitive, exact match).
72+
73+
Library names are unique per type within a namespace,
74+
so a type is required to identify a single library.
75+
Returns it (with full YAML content) if found, otherwise `None`.
76+
"""
77+
78+
library_id = self._get_discovery_config_library_id_by_name(name, config_type, namespace)
79+
if library_id is None:
80+
return None
81+
82+
return self.get_discovery_config_library(library_id)
83+
84+
def create_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary:
85+
"""
86+
Creates a new discovery config library on the server.
87+
88+
Sets the library's server-assigned fields
89+
(`id`, `is_valid`, `validation_error`, `created`, `modified`) and returns the library.
90+
"""
91+
92+
data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
93+
response = self.make_request("POST", "/api/discovery/config-libraries/", data=data)
94+
created = DiscoveryConfigLibrary.model_validate(response.json())
95+
library.id = created.id
96+
library.is_valid = created.is_valid
97+
library.validation_error = created.validation_error
98+
library.created = created.created
99+
library.modified = created.modified
100+
logger.info('Creation of discovery config library "%s" successful', library.name)
101+
return library
102+
103+
def update_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary:
104+
"""
105+
Performs a full update of the discovery config library.
106+
107+
The library must have its `id` set (i.e., it must have been previously created or retrieved from the server)
108+
and its `yaml` content present.
109+
A library's `config_type` is fixed at creation and cannot be changed by an update.
110+
"""
111+
112+
if library.id is None:
113+
raise ValueError("Cannot update a discovery config library that has not been created yet (id is None)")
114+
115+
if library.yaml is None:
116+
raise ValueError(
117+
"Cannot update a discovery config library without YAML content (yaml is None); "
118+
"list results omit YAML, so fetch the full library with `get_discovery_config_library` first"
119+
)
120+
121+
data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
122+
response = self.make_request("PUT", f"/api/discovery/config-libraries/{library.id}/", data=data)
123+
updated = DiscoveryConfigLibrary.model_validate(response.json())
124+
library.is_valid = updated.is_valid
125+
library.validation_error = updated.validation_error
126+
library.modified = updated.modified
127+
logger.debug('Update of discovery config library "%s" successful', library.name)
128+
return library
129+
130+
def create_or_update_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary:
131+
"""
132+
Creates the library, or updates the existing one with the same name, namespace, and config type.
133+
134+
Sets the library's `id` property.
135+
"""
136+
137+
library_id = self._get_discovery_config_library_id_by_name(library.name, library.config_type, library.namespace)
138+
if library_id is not None:
139+
library.id = library_id
140+
return self.update_discovery_config_library(library)
141+
142+
return self.create_discovery_config_library(library)
143+
144+
def delete_discovery_config_library_by_id_if_exists(
145+
self, library_id: DiscoveryConfigLibraryId, *, force: bool = False
146+
) -> None:
147+
"""
148+
Deletes the discovery config library with the given ID.
149+
150+
No-op if the library does not exist.
151+
152+
If the library is imported by any discovery configs,
153+
the server will return 409 Conflict unless `force=True` is passed.
154+
"""
155+
156+
params = {"force": "true"} if force else None
157+
self._delete_if_exists(f"/api/discovery/config-libraries/{library_id}/", params=params)
158+
159+
def delete_discovery_config_library_by_name_if_exists(
160+
self, name: str, config_type: DiscoveryConfigType, namespace: str = "", *, force: bool = False
161+
) -> None:
162+
"""
163+
Deletes the discovery config library with the given name, type, and namespace.
164+
165+
Library names are unique per type within a namespace,
166+
so a type is required to identify a single library.
167+
No-op if no such library exists.
168+
"""
169+
170+
library_id = self._get_discovery_config_library_id_by_name(name, config_type, namespace)
171+
if library_id is not None:
172+
self.delete_discovery_config_library_by_id_if_exists(library_id, force=force)

datamasque/client/dmclient.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datamasque.client.base import FileOrContent, UploadFile
22
from datamasque.client.connections import ConnectionClient
33
from datamasque.client.discovery import DiscoveryClient
4+
from datamasque.client.discovery_config_libraries import DiscoveryConfigLibraryClient
45
from datamasque.client.discovery_configs import DiscoveryConfigClient
56
from datamasque.client.files import FileClient
67
from datamasque.client.license import LicenseClient
@@ -22,6 +23,7 @@ class DataMasqueClient(
2223
RunClient,
2324
DiscoveryClient,
2425
DiscoveryConfigClient,
26+
DiscoveryConfigLibraryClient,
2527
UserClient,
2628
SettingsClient,
2729
):
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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.discovery_config import DiscoveryConfigType
7+
from datamasque.client.models.status import ValidationStatus
8+
9+
DiscoveryConfigLibraryId = NewType("DiscoveryConfigLibraryId", str)
10+
11+
12+
class DiscoveryConfigLibrary(BaseModel):
13+
"""
14+
Represents a named, namespaced, persisted YAML discovery config library.
15+
16+
Library names are unique per config type within a namespace,
17+
so a database and a file library may share a name.
18+
"""
19+
20+
model_config = ConfigDict(extra="allow", populate_by_name=True)
21+
22+
name: str
23+
config_type: DiscoveryConfigType
24+
namespace: str = ""
25+
yaml: Optional[str] = Field(default=None, alias="config_yaml")
26+
# Server-populated read-only fields, excluded from request bodies.
27+
id: Optional[DiscoveryConfigLibraryId] = Field(default=None, exclude=True)
28+
is_valid: Optional[ValidationStatus] = Field(default=None, exclude=True)
29+
"""Validation status; libraries are validated synchronously on create/update."""
30+
validation_error: Optional[str] = Field(default=None, exclude=True)
31+
"""Human-readable validation error, or `None` when valid."""
32+
created: Optional[datetime] = Field(default=None, exclude=True)
33+
modified: Optional[datetime] = Field(default=None, exclude=True)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "datamasque-python"
3-
version = "1.1.5"
3+
version = "1.1.6"
44
description = "Official Python client for the DataMasque data-masking API."
55
authors = [
66
{ name = "DataMasque Ltd" },

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 1.1.3
2+
current_version = 1.1.6
33
commit = True
44
tag = True
55

0 commit comments

Comments
 (0)