Skip to content

Commit 1832d70

Browse files
feat(discovery): DM-3801 add discovery config library CRUD client APIs
1 parent 65bd407 commit 1832d70

5 files changed

Lines changed: 660 additions & 0 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.0 (unreleased)
6+
------------------
7+
8+
* Added discovery-config-library management APIs (``list_discovery_config_libraries``, ``create_discovery_config_library``, and friends).
9+
510
1.0.4 (2026-06-09)
611
------------------
712

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import logging
2+
from typing import Iterator, Optional
3+
4+
from datamasque.client.base import BaseClient
5+
from datamasque.client.exceptions import DataMasqueApiError, DataMasqueException
6+
from datamasque.client.models.discovery_config_library import DiscoveryConfigLibrary, DiscoveryConfigLibraryId
7+
from datamasque.client.models.pagination import Page
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 iter_discovery_config_libraries(self) -> Iterator[DiscoveryConfigLibrary]:
16+
"""Lazily iterate all discovery config libraries via the paginated endpoint."""
17+
18+
return self._iter_paginated("/api/discovery/config-libraries/", model=DiscoveryConfigLibrary)
19+
20+
def list_discovery_config_libraries(self) -> list[DiscoveryConfigLibrary]:
21+
"""
22+
Lists all (non-archived) discovery config libraries.
23+
24+
Note: the YAML content is not included in the list response for performance.
25+
Use `get_discovery_config_library` to retrieve the full library with its YAML body.
26+
"""
27+
28+
return list(self.iter_discovery_config_libraries())
29+
30+
def get_discovery_config_library(self, library_id: DiscoveryConfigLibraryId) -> DiscoveryConfigLibrary:
31+
"""Retrieves a single discovery config library by ID, including its YAML content."""
32+
33+
response = self.make_request("GET", f"/api/discovery/config-libraries/{library_id}/")
34+
return DiscoveryConfigLibrary.model_validate(response.json())
35+
36+
def get_discovery_config_library_by_name(self, name: str, namespace: str = "") -> Optional[DiscoveryConfigLibrary]:
37+
"""
38+
Looks for a discovery config library matching the given name and namespace (case-sensitive, exact match).
39+
40+
Returns it (with full YAML content) if found, otherwise `None`.
41+
"""
42+
43+
response = self.make_request(
44+
"GET",
45+
"/api/discovery/config-libraries/",
46+
params={"name_exact": name, "namespace_exact": namespace, "limit": 1},
47+
)
48+
page = Page[DiscoveryConfigLibrary].model_validate(response.json())
49+
if not page.results:
50+
return None
51+
52+
library_id = page.results[0].id
53+
if library_id is None:
54+
raise DataMasqueApiError(
55+
"Server returned a discovery config library list entry without an `id`.",
56+
response=response,
57+
)
58+
59+
return self.get_discovery_config_library(library_id)
60+
61+
def create_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary:
62+
"""
63+
Creates a new discovery config library on the server.
64+
65+
Sets the library's server-assigned fields (`id`, `is_valid`, `created`, `modified`) and returns the library.
66+
"""
67+
68+
data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
69+
response = self.make_request("POST", "/api/discovery/config-libraries/", data=data)
70+
created = DiscoveryConfigLibrary.model_validate(response.json())
71+
library.id = created.id
72+
library.is_valid = created.is_valid
73+
library.created = created.created
74+
library.modified = created.modified
75+
logger.info('Creation of discovery config library "%s" successful', library.name)
76+
return library
77+
78+
def update_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary:
79+
"""
80+
Performs a full update of the discovery config library.
81+
82+
The library must have its `id` set (i.e., it must have been previously created or retrieved from the server).
83+
"""
84+
85+
if library.id is None:
86+
raise ValueError("Cannot update a discovery config library that has not been created yet (id is None)")
87+
88+
data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
89+
response = self.make_request("PUT", f"/api/discovery/config-libraries/{library.id}/", data=data)
90+
updated = DiscoveryConfigLibrary.model_validate(response.json())
91+
library.is_valid = updated.is_valid
92+
library.modified = updated.modified
93+
logger.debug('Update of discovery config library "%s" successful', library.name)
94+
return library
95+
96+
def create_or_update_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary:
97+
"""
98+
Creates the library if it doesn't exist, or updates it if one with the same name and namespace already exists.
99+
100+
Sets the library's `id` property.
101+
"""
102+
103+
existing = self.get_discovery_config_library_by_name(library.name, library.namespace)
104+
if existing is not None:
105+
library.id = existing.id
106+
return self.update_discovery_config_library(library)
107+
108+
return self.create_discovery_config_library(library)
109+
110+
def delete_discovery_config_library_by_id_if_exists(
111+
self, library_id: DiscoveryConfigLibraryId, *, force: bool = False
112+
) -> None:
113+
"""
114+
Deletes (archives) the discovery config library with the given ID.
115+
116+
No-op if the library does not exist.
117+
118+
If the library is imported by any discovery configs,
119+
the server will return 409 Conflict unless `force=True` is passed.
120+
"""
121+
122+
params = {"force": "true"} if force else None
123+
self._delete_if_exists(f"/api/discovery/config-libraries/{library_id}/", params=params)
124+
125+
def delete_discovery_config_library_by_name_if_exists(
126+
self, name: str, namespace: str = "", *, force: bool = False
127+
) -> None:
128+
"""
129+
Deletes the discovery config library with the given name and namespace.
130+
131+
No-op if the library does not exist.
132+
"""
133+
134+
all_libraries = self.list_discovery_config_libraries()
135+
matching = [lib for lib in all_libraries if lib.name == name and lib.namespace == namespace]
136+
for lib in matching:
137+
if lib.id is None:
138+
raise DataMasqueException(
139+
f'Server returned a discovery config library named "{lib.name}" without an `id`.'
140+
)
141+
142+
self.delete_discovery_config_library_by_id_if_exists(lib.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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
DiscoveryConfigLibraryId = NewType("DiscoveryConfigLibraryId", str)
9+
10+
11+
class DiscoveryConfigLibrary(BaseModel):
12+
"""Represents a named, namespaced, persisted YAML discovery config 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[DiscoveryConfigLibraryId] = None
20+
# Server-managed validation surface, populated by the DataMasque server.
21+
# `is_valid` may be `in_progress` immediately after creating a large library,
22+
# transitioning to `valid` or `invalid` once the server finishes validating.
23+
is_valid: Optional[ValidationStatus] = None
24+
validation_error: Optional[str] = None
25+
created: Optional[datetime] = None
26+
modified: Optional[datetime] = None

0 commit comments

Comments
 (0)