Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ History
* **Breaking:** ``get_unfinished_runs`` returns its runs keyed by ``ConnectionId`` instead of connection name,
because two connections in different projects can share a name.
* ``list_connections``, ``list_table_references`` and ``list_files_of_type`` take an optional ``project`` filter.
* Added ``default_project`` to ``DataMasqueInstanceConfig``.
A create whose model has no ``project`` goes into ``default_project``,
and ``create_or_update_*`` looks for an existing resource there,
before falling back to the server's only active project.
Updating a connection leaves it in its project.
* ``DataMasqueFile`` gains a read-only ``project`` field, which is ``None`` for built-in seeds.
* ``get_generated_rulesets`` sets each ruleset's ``project`` to the connection's project.
* Added project models and management APIs.
Expand Down
19 changes: 17 additions & 2 deletions datamasque/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,15 @@ class BaseClient:
password: Optional[str]
verify_ssl: bool
token_source: Optional[Callable[[], str]]
default_project: Optional[str]

def __init__(self, connection_config: DataMasqueInstanceConfig) -> None:
self.base_url = connection_config.base_url
self.username = connection_config.username
self.password = connection_config.password
self.verify_ssl = connection_config.verify_ssl
self.token_source = connection_config.token_source
self.default_project = connection_config.default_project
self._session = _build_session(self.verify_ssl)
self._timeout = (connection_config.connect_timeout_seconds, connection_config.read_timeout_seconds)
if connection_config.spcs_pat:
Expand Down Expand Up @@ -318,15 +320,17 @@ def _delete_if_exists(self, path: str, *, params: Optional[dict] = None) -> None

def _resolve_project(self, project: Optional[Union[ProjectId, str]]) -> Union[ProjectId, str]:
"""
Returns `project`, or when it is `None`, the ID of the only active project.
Returns `project`, else the client's `default_project`, else the ID of the only active project.

That is the project the server puts a create into when it is sent no `project`.
The last is the project the server puts a create into when it is sent no `project`.
With no active project, or more than one, the server rejects such a create,
so this raises `DataMasqueException` instead.
"""

if project is not None:
return project
if self.default_project is not None:
return self.default_project

response = self.make_request("GET", "/api/projects/", params={"limit": 1})
page = Page[Project].model_validate(response.json())
Expand All @@ -337,6 +341,17 @@ def _resolve_project(self, project: Optional[Union[ProjectId, str]]) -> Union[Pr

return page.results[0].id

def _default_project_field(self, project: Optional[Union[ProjectId, str]]) -> dict[str, str]:
"""
Returns the `project` field a create sends when the resource names no `project`: the client's `default_project`.

Empty when the resource names one, which its own fields carry, or when there is no `default_project`.
"""

if project is None and self.default_project is not None:
return {"project": self.default_project}
return {}

def _iter_paginated(
self,
path: str,
Expand Down
3 changes: 2 additions & 1 deletion datamasque/client/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def create_or_update_connection(self, connection_config: ConnectionConfig) -> Co
Creates or updates the connection in DM, and sets the `id` field on the given `connection_config`.

An existing connection with the same name in the same project is updated.
With no `project` set, the server's only active project is used,
With no `project` set, the client's `default_project` is used, else the server's only active project,
and `DataMasqueException` is raised when it has no active project or more than one.
"""

Expand All @@ -53,6 +53,7 @@ def create_or_update_connection(self, connection_config: ConnectionConfig) -> Co
"version": "1.0",
} | connection_config.model_dump(exclude_none=True, by_alias=True, mode="json")
if connection_id is None:
data |= self._default_project_field(connection_config.project)
response = self.make_request("POST", "/api/connections/", data=data)
else:
response = self.make_request("PUT", f"/api/connections/{connection_id}/", data=data)
Expand Down
6 changes: 4 additions & 2 deletions datamasque/client/discovery_config_libraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ def create_discovery_config_library(self, library: DiscoveryConfigLibrary) -> Di
if not library.yaml:
raise ValueError("Cannot create a discovery config library without YAML content (yaml is empty)")

data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
data = library.model_dump(exclude_none=True, by_alias=True, mode="json") | self._default_project_field(
library.project
)
response = self.make_request("POST", "/api/discovery/config-libraries/", data=data)
created = DiscoveryConfigLibrary.model_validate(response.json())
library.id = created.id
Expand Down Expand Up @@ -125,7 +127,7 @@ def create_or_update_discovery_config_library(self, library: DiscoveryConfigLibr
"""
Creates the library, or updates the existing one with the same name in the same project.

With no `project` set, the server's only active project is used,
With no `project` set, the client's `default_project` is used, else the server's only active project,
and `DataMasqueException` is raised when it has no active project or more than one.
Sets the library's `id` property.
"""
Expand Down
6 changes: 4 additions & 2 deletions datamasque/client/discovery_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ def create_discovery_config(self, config: DiscoveryConfig) -> DiscoveryConfig:
if not config.yaml:
raise ValueError("Cannot create a discovery config without YAML content (yaml is empty)")

data = config.model_dump(exclude_none=True, by_alias=True, mode="json")
data = config.model_dump(exclude_none=True, by_alias=True, mode="json") | self._default_project_field(
config.project
)
response = self.make_request("POST", "/api/discovery/configs/", data=data)
created = DiscoveryConfig.model_validate(response.json())
config.id = created.id
Expand Down Expand Up @@ -130,7 +132,7 @@ def create_or_update_discovery_config(self, config: DiscoveryConfig) -> Discover
"""
Creates the config, or updates the existing one with the same name and type in the same project.

With no `project` set, the server's only active project is used,
With no `project` set, the client's `default_project` is used, else the server's only active project,
and `DataMasqueException` is raised when it has no active project or more than one.
Sets the config's `id` property.
"""
Expand Down
2 changes: 1 addition & 1 deletion datamasque/client/models/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class ConnectionConfig(BaseModel):
"""
The project it belongs to: the `<id>-<slug>` reference a GET returns, such as `Project.reference`,
or the project's ID as a string.
Left as `None`, a create goes into the server's default project.
Left as `None`, a create goes into the client's `default_project`, else the server's default project.
"""
id: Optional[ConnectionId] = None
# Server-populated and read-only, so it is excluded when serializing a config back to the API.
Expand Down
2 changes: 1 addition & 1 deletion datamasque/client/models/discovery_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class DiscoveryConfig(BaseModel):
"""
The project it belongs to: the `<id>-<slug>` reference a GET returns, such as `Project.reference`,
or the project's ID as a string.
Left as `None`, a create goes into the server's default project.
Left as `None`, a create goes into the client's `default_project`, else the server's default project.
"""
yaml: Optional[str] = Field(default=None, alias="config_yaml")
config_type: DiscoveryConfigType
Expand Down
2 changes: 1 addition & 1 deletion datamasque/client/models/discovery_config_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class DiscoveryConfigLibrary(BaseModel):
"""
The project it belongs to: the `<id>-<slug>` reference a GET returns, such as `Project.reference`,
or the project's ID as a string.
Left as `None`, a create goes into the server's default project.
Left as `None`, a create goes into the client's `default_project`, else the server's default project.
"""
yaml: Optional[str] = Field(default=None, alias="config_yaml")
# Server-populated read-only fields, excluded from request bodies.
Expand Down
6 changes: 6 additions & 0 deletions datamasque/client/models/dm_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ class DataMasqueInstanceConfig(BaseModel):
"""How long a request may take to connect, including the TLS handshake."""
read_timeout_seconds: float = DEFAULT_READ_TIMEOUT_SECONDS
"""How long a request may wait for the server between bytes of its response."""
default_project: Optional[str] = None
"""The project a create goes into when the resource names none, as a `<id>-<slug>` reference (e.g. `1-default`)
or a bare project id.

Applies to connections, rulesets, discovery configs, RG configs, table references and libraries.
Leave unset to use the server's only active project."""

@model_validator(mode="after")
def _validate_auth_source(self) -> "DataMasqueInstanceConfig":
Expand Down
2 changes: 1 addition & 1 deletion datamasque/client/models/rg_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class RGConfig(BaseModel):
"""
The project it belongs to: the `<id>-<slug>` reference a GET returns, such as `Project.reference`,
or the project's ID as a string.
Left as `None`, a create goes into the server's default project.
Left as `None`, a create goes into the client's `default_project`, else the server's default project.
"""
yaml: Optional[str] = Field(default=None, alias="config_yaml")

Expand Down
2 changes: 1 addition & 1 deletion datamasque/client/models/ruleset.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class Ruleset(GitTrackedEntity):
"""
The project it belongs to: the `<id>-<slug>` reference a GET returns, such as `Project.reference`,
or the project's ID as a string.
Left as `None`, a create goes into the server's default project.
Left as `None`, a create goes into the client's `default_project`, else the server's default project.
"""
yaml: str = Field(default="", alias="config_yaml")
ruleset_type: RulesetType = Field(default=RulesetType.database, alias="mask_type")
Expand Down
2 changes: 1 addition & 1 deletion datamasque/client/models/ruleset_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class RulesetLibrary(GitTrackedEntity):
"""
The project it belongs to: the `<id>-<slug>` reference a GET returns, such as `Project.reference`,
or the project's ID as a string.
Left as `None`, a create goes into the server's default project.
Left as `None`, a create goes into the client's `default_project`, else the server's default project.
"""
yaml: Optional[str] = Field(default=None, alias="config_yaml")

Expand Down
2 changes: 1 addition & 1 deletion datamasque/client/models/table_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class TableReference(BaseModel):
"""
The project it belongs to: the `<id>-<slug>` reference a GET returns, such as `Project.reference`,
or the project's ID as a string.
Left as `None`, a create goes into the server's default project.
Left as `None`, a create goes into the client's `default_project`, else the server's default project.
"""
connection: Union[ConnectionId, ConnectionConfig]
"""The connection the data lives in. Accepts a `ConnectionId` or a `ConnectionConfig` (its `id` is extracted)."""
Expand Down
6 changes: 4 additions & 2 deletions datamasque/client/rg_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ def create_rg_config(self, config: RGConfig) -> RGConfig:
(`id`, `is_valid`, `validation_errors`, `created`, `modified`) and returns the config.
"""

data = config.model_dump(exclude_none=True, by_alias=True, mode="json")
data = config.model_dump(exclude_none=True, by_alias=True, mode="json") | self._default_project_field(
config.project
)
response = self.make_request("POST", "/api/ruleset-generation-configs/", data=data)
created = RGConfig.model_validate(response.json())
config.id = created.id
Expand Down Expand Up @@ -121,7 +123,7 @@ def create_or_update_rg_config(self, config: RGConfig) -> RGConfig:
"""
Creates the config, or updates the existing one with the same name in the same project.

With no `project` set, the server's only active project is used,
With no `project` set, the client's `default_project` is used, else the server's only active project,
and `DataMasqueException` is raised when it has no active project or more than one.
Sets the config's `id` property.
"""
Expand Down
6 changes: 4 additions & 2 deletions datamasque/client/ruleset_libraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ def create_ruleset_library(self, library: RulesetLibrary) -> RulesetLibrary:
and returns the library.
"""

data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
data = library.model_dump(exclude_none=True, by_alias=True, mode="json") | self._default_project_field(
library.project
)
response = self.make_request("POST", "/api/ruleset-libraries/", data=data)
created_library = RulesetLibrary.model_validate(response.json())
library.id = created_library.id
Expand Down Expand Up @@ -118,7 +120,7 @@ def create_or_update_ruleset_library(self, library: RulesetLibrary) -> RulesetLi
"""
Creates the library, or updates the existing one with the same name in the same project.

With no `project` set, the server's only active project is used,
With no `project` set, the client's `default_project` is used, else the server's only active project,
and `DataMasqueException` is raised when it has no active project or more than one.
Sets the library's `id` property.
"""
Expand Down
6 changes: 4 additions & 2 deletions datamasque/client/rulesets.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,17 @@ def create_or_update_ruleset(self, ruleset: Ruleset) -> Ruleset:
"""
Creates the ruleset, or updates the existing one with the same name and type in the same project.

With no `project` set, the server's only active project is used,
With no `project` set, the client's `default_project` is used, else the server's only active project,
and `DataMasqueException` is raised when it has no active project or more than one.
Populates the given ruleset's `id`, `is_valid`, `validation_errors`, `unlicensed_feature_warnings`,
and `git` fields from the server response, and returns the same ruleset instance for convenience.
"""

# The server finds the existing ruleset itself, so this only checks that an unset project resolves.
self._resolve_project(ruleset.project)
data = ruleset.model_dump(exclude_none=True, by_alias=True, mode="json")
data = ruleset.model_dump(exclude_none=True, by_alias=True, mode="json") | self._default_project_field(
ruleset.project
)
response = self.make_request("POST", "/api/rulesets/", data=data, params={"upsert": "true"})
created = Ruleset.model_validate(response.json())
ruleset.id = created.id
Expand Down
3 changes: 2 additions & 1 deletion datamasque/client/table_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def create_table_reference(self, table_reference: TableReference) -> TableRefere
data.pop("options", None)
if data.get("project") is None:
data.pop("project", None)
data |= self._default_project_field(table_reference.project)
response = self.make_request("POST", "/api/table-references/", data=data)
created = TableReference.model_validate(response.json())
table_reference.id = created.id
Expand Down Expand Up @@ -122,7 +123,7 @@ def create_or_update_table_reference(self, table_reference: TableReference) -> T
"""
Creates the table reference, or updates the existing one with the same name in the same project.

With no `project` set, the server's only active project is used,
With no `project` set, the client's `default_project` is used, else the server's only active project,
and `DataMasqueException` is raised when it has no active project or more than one.
Sets the table reference's `id` property.
"""
Expand Down
Loading