Skip to content

Commit ff668ab

Browse files
fix: add discover-conf-library to init, handle non pagination
1 parent 1832d70 commit ff668ab

4 files changed

Lines changed: 448 additions & 206 deletions

File tree

datamasque/client/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@
7676
TableConstraints,
7777
)
7878
from datamasque.client.models.discovery_config import DiscoveryConfig, DiscoveryConfigId
79+
from datamasque.client.models.discovery_config_library import (
80+
DiscoveryConfigLibrary,
81+
DiscoveryConfigLibraryId,
82+
DiscoveryConfigType,
83+
)
7984
from datamasque.client.models.dm_instance import DataMasqueInstanceConfig
8085
from datamasque.client.models.files import (
8186
DataMasqueFile,
@@ -138,6 +143,9 @@
138143
"DatabricksConnectionConfig",
139144
"DiscoveryConfig",
140145
"DiscoveryConfigId",
146+
"DiscoveryConfigLibrary",
147+
"DiscoveryConfigLibraryId",
148+
"DiscoveryConfigType",
141149
"DiscoveryMatch",
142150
"DynamoConnectionConfig",
143151
"FailedToStartError",
Lines changed: 88 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,114 @@
11
import logging
2-
from typing import Iterator, Optional
2+
from typing import Optional
33

44
from datamasque.client.base import BaseClient
55
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
6+
from datamasque.client.models.discovery_config_library import (
7+
DiscoveryConfigLibrary,
8+
DiscoveryConfigLibraryId,
9+
DiscoveryConfigType,
10+
)
811

912
logger = logging.getLogger(__name__)
1013

1114

1215
class DiscoveryConfigLibraryClient(BaseClient):
1316
"""Discovery config library CRUD API methods. Mixed into `DataMasqueClient`."""
1417

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-
2018
def list_discovery_config_libraries(self) -> list[DiscoveryConfigLibrary]:
2119
"""
2220
Lists all (non-archived) discovery config libraries.
2321
22+
Unlike most list endpoints, this one is not paginated;
23+
the server returns every library in a single response.
2424
Note: the YAML content is not included in the list response for performance.
2525
Use `get_discovery_config_library` to retrieve the full library with its YAML body.
2626
"""
2727

28-
return list(self.iter_discovery_config_libraries())
28+
response = self.make_request("GET", "/api/discovery/config-libraries/")
29+
return [DiscoveryConfigLibrary.model_validate(item) for item in response.json()]
2930

3031
def get_discovery_config_library(self, library_id: DiscoveryConfigLibraryId) -> DiscoveryConfigLibrary:
3132
"""Retrieves a single discovery config library by ID, including its YAML content."""
3233

3334
response = self.make_request("GET", f"/api/discovery/config-libraries/{library_id}/")
3435
return DiscoveryConfigLibrary.model_validate(response.json())
3536

36-
def get_discovery_config_library_by_name(self, name: str, namespace: str = "") -> Optional[DiscoveryConfigLibrary]:
37+
def find_discovery_config_library_ids(
38+
self, name: str, namespace: str, config_type: Optional[DiscoveryConfigType]
39+
) -> list[DiscoveryConfigLibraryId]:
3740
"""
38-
Looks for a discovery config library matching the given name and namespace (case-sensitive, exact match).
41+
Returns the IDs of libraries matching the given name, namespace, and (optionally) config type.
3942
40-
Returns it (with full YAML content) if found, otherwise `None`.
43+
The name is filtered server-side via `name_exact` to keep the response small,
44+
but all three fields are matched client-side:
45+
the server's `namespace_exact` filter ignores empty strings (the default namespace),
46+
and delete-by-name must not trust a filter param the server may ignore.
4147
"""
4248

4349
response = self.make_request(
4450
"GET",
4551
"/api/discovery/config-libraries/",
46-
params={"name_exact": name, "namespace_exact": namespace, "limit": 1},
52+
params={"name_exact": name},
4753
)
48-
page = Page[DiscoveryConfigLibrary].model_validate(response.json())
49-
if not page.results:
54+
entries = [DiscoveryConfigLibrary.model_validate(item) for item in response.json()]
55+
matches = [
56+
entry
57+
for entry in entries
58+
if entry.name == name
59+
and entry.namespace == namespace
60+
and (config_type is None or entry.config_type is config_type)
61+
]
62+
63+
library_ids = []
64+
for entry in matches:
65+
if entry.id is None:
66+
raise DataMasqueApiError(
67+
"Server returned a discovery config library list entry without an `id`.",
68+
response=response,
69+
)
70+
library_ids.append(entry.id)
71+
72+
return library_ids
73+
74+
def get_discovery_config_library_by_name(
75+
self, name: str, namespace: str = "", config_type: Optional[DiscoveryConfigType] = None
76+
) -> Optional[DiscoveryConfigLibrary]:
77+
"""
78+
Looks for a discovery config library matching the given name and namespace (case-sensitive, exact match).
79+
80+
Returns it (with full YAML content) if found, otherwise `None`.
81+
82+
A database and a file library may share a name;
83+
pass `config_type` to disambiguate.
84+
Raises `DataMasqueException` if `config_type` was not given and both exist.
85+
"""
86+
87+
library_ids = self.find_discovery_config_library_ids(name, namespace, config_type)
88+
if not library_ids:
5089
return None
5190

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,
91+
if len(library_ids) > 1:
92+
raise DataMasqueException(
93+
f'Multiple discovery config libraries are named "{name}"; pass `config_type` to disambiguate.'
5794
)
5895

59-
return self.get_discovery_config_library(library_id)
96+
return self.get_discovery_config_library(library_ids[0])
6097

6198
def create_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary:
6299
"""
63100
Creates a new discovery config library on the server.
64101
65-
Sets the library's server-assigned fields (`id`, `is_valid`, `created`, `modified`) and returns the library.
102+
Sets the library's server-assigned fields
103+
(`id`, `is_valid`, `validation_error`, `created`, `modified`) and returns the library.
66104
"""
67105

68106
data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
69107
response = self.make_request("POST", "/api/discovery/config-libraries/", data=data)
70108
created = DiscoveryConfigLibrary.model_validate(response.json())
71109
library.id = created.id
72110
library.is_valid = created.is_valid
111+
library.validation_error = created.validation_error
73112
library.created = created.created
74113
library.modified = created.modified
75114
logger.info('Creation of discovery config library "%s" successful', library.name)
@@ -79,30 +118,39 @@ def update_discovery_config_library(self, library: DiscoveryConfigLibrary) -> Di
79118
"""
80119
Performs a full update of the discovery config library.
81120
82-
The library must have its `id` set (i.e., it must have been previously created or retrieved from the server).
121+
The library must have its `id` set (i.e., it must have been previously created or retrieved from the server)
122+
and its `yaml` content present.
123+
A library's `config_type` is fixed at creation and cannot be changed by an update.
83124
"""
84125

85126
if library.id is None:
86127
raise ValueError("Cannot update a discovery config library that has not been created yet (id is None)")
87128

129+
if library.yaml is None:
130+
raise ValueError(
131+
"Cannot update a discovery config library without YAML content (yaml is None); "
132+
"list results omit YAML, so fetch the full library with `get_discovery_config_library` first"
133+
)
134+
88135
data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
89136
response = self.make_request("PUT", f"/api/discovery/config-libraries/{library.id}/", data=data)
90137
updated = DiscoveryConfigLibrary.model_validate(response.json())
91138
library.is_valid = updated.is_valid
139+
library.validation_error = updated.validation_error
92140
library.modified = updated.modified
93141
logger.debug('Update of discovery config library "%s" successful', library.name)
94142
return library
95143

96144
def create_or_update_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary:
97145
"""
98-
Creates the library if it doesn't exist, or updates it if one with the same name and namespace already exists.
146+
Creates the library, or updates the existing one with the same name, namespace, and config type.
99147
100148
Sets the library's `id` property.
101149
"""
102150

103-
existing = self.get_discovery_config_library_by_name(library.name, library.namespace)
104-
if existing is not None:
105-
library.id = existing.id
151+
library_ids = self.find_discovery_config_library_ids(library.name, library.namespace, library.config_type)
152+
if library_ids:
153+
library.id = library_ids[0]
106154
return self.update_discovery_config_library(library)
107155

108156
return self.create_discovery_config_library(library)
@@ -123,20 +171,21 @@ def delete_discovery_config_library_by_id_if_exists(
123171
self._delete_if_exists(f"/api/discovery/config-libraries/{library_id}/", params=params)
124172

125173
def delete_discovery_config_library_by_name_if_exists(
126-
self, name: str, namespace: str = "", *, force: bool = False
174+
self,
175+
name: str,
176+
namespace: str = "",
177+
*,
178+
config_type: Optional[DiscoveryConfigType] = None,
179+
force: bool = False,
127180
) -> None:
128181
"""
129-
Deletes the discovery config library with the given name and namespace.
182+
Deletes the discovery config library(s) with the given name and namespace.
130183
131-
No-op if the library does not exist.
132-
"""
184+
No-op if no library matches.
133185
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-
)
186+
When `config_type` is not given and both a database and a file library share the name,
187+
both are deleted.
188+
"""
141189

142-
self.delete_discovery_config_library_by_id_if_exists(lib.id, force=force)
190+
for library_id in self.find_discovery_config_library_ids(name, namespace, config_type):
191+
self.delete_discovery_config_library_by_id_if_exists(library_id, force=force)

datamasque/client/models/discovery_config_library.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import enum
12
from datetime import datetime
23
from typing import NewType, Optional
34

@@ -8,12 +9,25 @@
89
DiscoveryConfigLibraryId = NewType("DiscoveryConfigLibraryId", str)
910

1011

12+
class DiscoveryConfigType(enum.Enum):
13+
"""The flavour of discovery config a library belongs to; fixed at creation."""
14+
15+
database = "database"
16+
file = "file"
17+
18+
1119
class DiscoveryConfigLibrary(BaseModel):
12-
"""Represents a named, namespaced, persisted YAML discovery config library."""
20+
"""
21+
Represents a named, namespaced, persisted YAML discovery config library.
22+
23+
A database and a file library may share a name;
24+
uniqueness on the server is scoped to (`namespace`, `name`, `config_type`).
25+
"""
1326

1427
model_config = ConfigDict(extra="allow", populate_by_name=True)
1528

1629
name: str
30+
config_type: DiscoveryConfigType
1731
namespace: str = ""
1832
yaml: Optional[str] = Field(default=None, alias="config_yaml")
1933
id: Optional[DiscoveryConfigLibraryId] = None

0 commit comments

Comments
 (0)