-
Notifications
You must be signed in to change notification settings - Fork 3
Dm 3801 config library python client #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
andrewchester-dm
wants to merge
4
commits into
main
Choose a base branch
from
DM-3801-config-library-python-client
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
074879e
feat(discovery): DM-3801 add discovery config library CRUD client APIs
andrewchester-dm e7aed54
fix: add discover-conf-library to init, handle non pagination
andrewchester-dm 5f2bbee
chore: sync uv.lock to version 1.1.5
andrewchester-dm 4f06c46
fix(discovery): DM-3801 address review (require config_type, exclude …
andrewchester-dm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| import logging | ||
| from typing import Optional | ||
|
|
||
| from datamasque.client.base import BaseClient | ||
| from datamasque.client.exceptions import DataMasqueApiError, DataMasqueException | ||
| from datamasque.client.models.discovery_config import DiscoveryConfigType | ||
| from datamasque.client.models.discovery_config_library import DiscoveryConfigLibrary, DiscoveryConfigLibraryId | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class DiscoveryConfigLibraryClient(BaseClient): | ||
| """Discovery config library CRUD API methods. Mixed into `DataMasqueClient`.""" | ||
|
|
||
| def list_discovery_config_libraries(self) -> list[DiscoveryConfigLibrary]: | ||
| """ | ||
| Lists all (non-archived) discovery config libraries. | ||
|
cph-datamasque marked this conversation as resolved.
Outdated
|
||
|
|
||
| Unlike most list endpoints, this one is not paginated; | ||
| the server returns every library in a single response. | ||
| Note: the YAML content is not included in the list response for performance. | ||
| Use `get_discovery_config_library` to retrieve the full library with its YAML body. | ||
| """ | ||
|
|
||
| response = self.make_request("GET", "/api/discovery/config-libraries/") | ||
| return [DiscoveryConfigLibrary.model_validate(item) for item in response.json()] | ||
|
|
||
| def get_discovery_config_library(self, library_id: DiscoveryConfigLibraryId) -> DiscoveryConfigLibrary: | ||
| """Retrieves a single discovery config library by ID, including its YAML content.""" | ||
|
|
||
| response = self.make_request("GET", f"/api/discovery/config-libraries/{library_id}/") | ||
| return DiscoveryConfigLibrary.model_validate(response.json()) | ||
|
|
||
| def find_discovery_config_library_ids( | ||
| self, name: str, namespace: str, config_type: Optional[DiscoveryConfigType] | ||
| ) -> list[DiscoveryConfigLibraryId]: | ||
| """ | ||
| Returns the IDs of libraries matching the given name, namespace, and (optionally) config type. | ||
|
|
||
| The name is filtered server-side via `name_exact` to keep the response small, | ||
| but all three fields are matched client-side: | ||
| the server's `namespace_exact` filter ignores empty strings (the default namespace), | ||
| and delete-by-name must not trust a filter param the server may ignore. | ||
|
cph-datamasque marked this conversation as resolved.
Outdated
|
||
| """ | ||
|
|
||
| response = self.make_request( | ||
| "GET", | ||
| "/api/discovery/config-libraries/", | ||
| params={"name_exact": name}, | ||
| ) | ||
| entries = [DiscoveryConfigLibrary.model_validate(item) for item in response.json()] | ||
| matches = [ | ||
| entry | ||
| for entry in entries | ||
| if entry.name == name | ||
| and entry.namespace == namespace | ||
| and (config_type is None or entry.config_type is config_type) | ||
| ] | ||
|
|
||
| library_ids = [] | ||
| for entry in matches: | ||
| if entry.id is None: | ||
| raise DataMasqueApiError( | ||
| "Server returned a discovery config library list entry without an `id`.", | ||
| response=response, | ||
| ) | ||
| library_ids.append(entry.id) | ||
|
|
||
| return library_ids | ||
|
|
||
| def get_discovery_config_library_by_name( | ||
| self, name: str, namespace: str = "", config_type: Optional[DiscoveryConfigType] = None | ||
|
cph-datamasque marked this conversation as resolved.
Outdated
|
||
| ) -> Optional[DiscoveryConfigLibrary]: | ||
| """ | ||
| Looks for a discovery config library matching the given name and namespace (case-sensitive, exact match). | ||
|
|
||
| Returns it (with full YAML content) if found, otherwise `None`. | ||
|
|
||
| A database and a file library may share a name; | ||
| pass `config_type` to disambiguate. | ||
| Raises `DataMasqueException` if `config_type` was not given and both exist. | ||
| """ | ||
|
|
||
| library_ids = self.find_discovery_config_library_ids(name, namespace, config_type) | ||
| if not library_ids: | ||
| return None | ||
|
|
||
| if len(library_ids) > 1: | ||
| raise DataMasqueException( | ||
| f'Multiple discovery config libraries are named "{name}"; pass `config_type` to disambiguate.' | ||
| ) | ||
|
|
||
| return self.get_discovery_config_library(library_ids[0]) | ||
|
|
||
| def create_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary: | ||
| """ | ||
| Creates a new discovery config library on the server. | ||
|
|
||
| Sets the library's server-assigned fields | ||
| (`id`, `is_valid`, `validation_error`, `created`, `modified`) and returns the library. | ||
| """ | ||
|
|
||
| data = library.model_dump(exclude_none=True, by_alias=True, mode="json") | ||
| response = self.make_request("POST", "/api/discovery/config-libraries/", data=data) | ||
| created = DiscoveryConfigLibrary.model_validate(response.json()) | ||
| library.id = created.id | ||
| library.is_valid = created.is_valid | ||
| library.validation_error = created.validation_error | ||
| library.created = created.created | ||
| library.modified = created.modified | ||
| logger.info('Creation of discovery config library "%s" successful', library.name) | ||
| return library | ||
|
|
||
| def update_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary: | ||
| """ | ||
| Performs a full update of the discovery config library. | ||
|
|
||
| The library must have its `id` set (i.e., it must have been previously created or retrieved from the server) | ||
| and its `yaml` content present. | ||
| A library's `config_type` is fixed at creation and cannot be changed by an update. | ||
| """ | ||
|
|
||
| if library.id is None: | ||
| raise ValueError("Cannot update a discovery config library that has not been created yet (id is None)") | ||
|
|
||
| if library.yaml is None: | ||
| raise ValueError( | ||
| "Cannot update a discovery config library without YAML content (yaml is None); " | ||
| "list results omit YAML, so fetch the full library with `get_discovery_config_library` first" | ||
| ) | ||
|
|
||
| data = library.model_dump(exclude_none=True, by_alias=True, mode="json") | ||
| response = self.make_request("PUT", f"/api/discovery/config-libraries/{library.id}/", data=data) | ||
| updated = DiscoveryConfigLibrary.model_validate(response.json()) | ||
| library.is_valid = updated.is_valid | ||
| library.validation_error = updated.validation_error | ||
| library.modified = updated.modified | ||
| logger.debug('Update of discovery config library "%s" successful', library.name) | ||
| return library | ||
|
|
||
| def create_or_update_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary: | ||
| """ | ||
| Creates the library, or updates the existing one with the same name, namespace, and config type. | ||
|
|
||
| Sets the library's `id` property. | ||
| """ | ||
|
|
||
| library_ids = self.find_discovery_config_library_ids(library.name, library.namespace, library.config_type) | ||
| if library_ids: | ||
| library.id = library_ids[0] | ||
| return self.update_discovery_config_library(library) | ||
|
|
||
| return self.create_discovery_config_library(library) | ||
|
|
||
| def delete_discovery_config_library_by_id_if_exists( | ||
| self, library_id: DiscoveryConfigLibraryId, *, force: bool = False | ||
| ) -> None: | ||
| """ | ||
| Deletes (archives) the discovery config library with the given ID. | ||
|
cph-datamasque marked this conversation as resolved.
Outdated
|
||
|
|
||
| No-op if the library does not exist. | ||
|
|
||
| If the library is imported by any discovery configs, | ||
| the server will return 409 Conflict unless `force=True` is passed. | ||
| """ | ||
|
|
||
| params = {"force": "true"} if force else None | ||
| self._delete_if_exists(f"/api/discovery/config-libraries/{library_id}/", params=params) | ||
|
|
||
| def delete_discovery_config_library_by_name_if_exists( | ||
| self, | ||
| name: str, | ||
| namespace: str = "", | ||
| *, | ||
| config_type: Optional[DiscoveryConfigType] = None, | ||
|
cph-datamasque marked this conversation as resolved.
Outdated
|
||
| force: bool = False, | ||
| ) -> None: | ||
| """ | ||
| Deletes the discovery config library(s) with the given name and namespace. | ||
|
|
||
| No-op if no library matches. | ||
|
|
||
| When `config_type` is not given and both a database and a file library share the name, | ||
| both are deleted. | ||
| """ | ||
|
|
||
| for library_id in self.find_discovery_config_library_ids(name, namespace, config_type): | ||
| self.delete_discovery_config_library_by_id_if_exists(library_id, force=force) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| from datetime import datetime | ||
| from typing import NewType, Optional | ||
|
|
||
| from pydantic import BaseModel, ConfigDict, Field | ||
|
|
||
| from datamasque.client.models.discovery_config import DiscoveryConfigType | ||
| from datamasque.client.models.status import ValidationStatus | ||
|
|
||
| DiscoveryConfigLibraryId = NewType("DiscoveryConfigLibraryId", str) | ||
|
|
||
|
|
||
| class DiscoveryConfigLibrary(BaseModel): | ||
| """ | ||
| Represents a named, namespaced, persisted YAML discovery config library. | ||
|
|
||
| A database and a file library may share a name; | ||
| uniqueness on the server is scoped to (`namespace`, `name`, `config_type`). | ||
| """ | ||
|
|
||
| model_config = ConfigDict(extra="allow", populate_by_name=True) | ||
|
|
||
| name: str | ||
| config_type: DiscoveryConfigType | ||
| namespace: str = "" | ||
| yaml: Optional[str] = Field(default=None, alias="config_yaml") | ||
| id: Optional[DiscoveryConfigLibraryId] = None | ||
| # Server-managed validation surface, populated by the DataMasque server. | ||
| # `is_valid` may be `in_progress` immediately after creating a large library, | ||
| # transitioning to `valid` or `invalid` once the server finishes validating. | ||
|
cph-datamasque marked this conversation as resolved.
Outdated
|
||
| is_valid: Optional[ValidationStatus] = None | ||
| validation_error: Optional[str] = None | ||
| created: Optional[datetime] = None | ||
| modified: Optional[datetime] = None | ||
|
cph-datamasque marked this conversation as resolved.
Outdated
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.