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
16 changes: 16 additions & 0 deletions src/datamaker/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from .routes.export_and_validation import ExportClient, ValidationClient
from .routes.scenario_files import ScenarioFilesClient
from .routes.sets import SetsClient
from .routes.plans import PlansClient
from .routes.masking_policies import MaskingPoliciesClient
from .routes.keymaps import KeyMapsClient

load_dotenv()
Expand Down Expand Up @@ -72,6 +74,10 @@ def __init__(
)
self._sets = SetsClient(api_key, default_headers, base_url, verify)
self._keymaps = KeyMapsClient(api_key, default_headers, base_url, verify)
self._plans = PlansClient(api_key, default_headers, base_url, verify)
self._masking_policies = MaskingPoliciesClient(
api_key, default_headers, base_url, verify
)

# Maintain backward compatibility
self.api_key = self._generation.api_key
Expand Down Expand Up @@ -1026,3 +1032,13 @@ def sets(self):
def keymaps(self):
"""Access to key maps client."""
return self._keymaps

@property
def plans(self):
"""Access to plans client."""
return self._plans

@property
def masking_policies(self):
"""Access to masking policies client."""
return self._masking_policies
130 changes: 130 additions & 0 deletions src/datamaker/routes/masking_policies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""Client for masking policy operations.

A masking policy is the set of field rules a masked generation or export
applies. Two flags decide how it behaves, and they are easy to confuse:

``consistent``
The same input masks to the same output within the policy, so joins across
masked tables still line up.

``reversible``
The mapping is recorded so it can be reversed later. A reversible policy
mints its mappings into the KeyMap named by ``key_map_name``, which is why
that argument only means anything alongside it.
"""

import os
from typing import Any, Dict, List, Optional

from .base import BaseClient


class MaskingPoliciesClient(BaseClient):
"""Client for masking policy operations."""

def get_masking_policies(
self, project_id: Optional[str] = None
) -> List[Dict[str, Any]]:
"""Fetch the masking policies in the caller's project/team scope.

Args:
project_id: Optional project ID to scope the listing to. Falls back
to the DATAMAKER_PROJECT_ID env var.

Returns:
A list of masking policy dictionaries.
"""
project_id = project_id or os.environ.get("DATAMAKER_PROJECT_ID")

endpoint = "/masking-policies"
if project_id:
endpoint += f"?projectId={project_id}"

response = self._make_request("GET", endpoint)
return response.json()

def get_masking_policy(self, policy_id: str) -> Dict[str, Any]:
"""Get a single masking policy by ID.

Args:
policy_id: The policy's ID.

Returns:
The masking policy dictionary.
"""
response = self._make_request("GET", f"/masking-policies/{policy_id}")
return response.json()

def create_masking_policy(
self,
name: str,
fields: Any,
description: Optional[str] = None,
consistent: Optional[bool] = None,
reversible: Optional[bool] = None,
key_map_name: Optional[str] = None,
project_id: Optional[str] = None,
) -> Dict[str, Any]:
"""Create a masking policy.

Args:
name: The policy name.
fields: The field rule list. Shape varies by rule type.
description: Optional description.
consistent: Same input masks to the same output. Defaults to the
API's own default (True) when omitted.
reversible: Record the mapping so masking can be reversed. Requires
``key_map_name`` to be useful.
key_map_name: The KeyMap a reversible policy mints mappings into.
project_id: Project to create it in. Falls back to
DATAMAKER_PROJECT_ID.

Returns:
The created masking policy dictionary.
"""
project_id = project_id or os.environ.get("DATAMAKER_PROJECT_ID")

# Only send what the caller set: omitting a key lets the API apply its
# own default, whereas sending null would override it.
payload: Dict[str, Any] = {"name": name, "fields": fields}
if description is not None:
payload["description"] = description
if consistent is not None:
payload["consistent"] = consistent
if reversible is not None:
payload["reversible"] = reversible
if key_map_name is not None:
payload["keyMapName"] = key_map_name
if project_id:
payload["projectId"] = project_id

response = self._make_request("POST", "/masking-policies", json=payload)
return response.json()

def update_masking_policy(self, policy_id: str, **fields: Any) -> Dict[str, Any]:
"""Update a masking policy.

Args:
policy_id: The policy's ID.
**fields: Fields to change. Use API spellings for multi-word keys,
e.g. ``keyMapName``.

Returns:
The updated masking policy dictionary.
"""
response = self._make_request(
"PATCH", f"/masking-policies/{policy_id}", json=fields
)
return response.json()

def delete_masking_policy(self, policy_id: str) -> Dict[str, Any]:
"""Delete a masking policy.

Args:
policy_id: The policy's ID.

Returns:
``{"message": "Masking policy deleted"}``.
"""
response = self._make_request("DELETE", f"/masking-policies/{policy_id}")
return response.json()
72 changes: 72 additions & 0 deletions src/datamaker/routes/plans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Client for Plan operations.

A Plan is a reviewable description of work the agent (or a person) intends to
carry out: which entities, which targets, how many rows, and against which
environment. Plans are the governed path - they are authored, reviewed and only
then run - which is why this client covers authoring and inspection rather than
execution.

The plan RUN endpoints (`/plans/{id}/run`, `/runs`, `/signoffs`) are not wrapped
yet: they carry artifacts, logs and multi-step state, and their response shapes
are not described in the API's OpenAPI document at the time of writing. Use
``client.plans._make_request`` or add them here once the API types them.
"""

from typing import Any, Dict, List

from .base import BaseClient


class PlansClient(BaseClient):
"""Client for plan operations."""

def get_plans(self) -> List[Dict[str, Any]]:
"""List the plans in the caller's active project.

Returns:
A list of plan dictionaries, newest first.
"""
response = self._make_request("GET", "/plans")
return response.json()

def get_plan(self, plan_id: str) -> Dict[str, Any]:
"""Get a single plan by ID, including its full ``spec`` and ``history``.

Args:
plan_id: The plan's ID.

Returns:
The plan dictionary.
"""
response = self._make_request("GET", f"/plans/{plan_id}")
return response.json()

def update_plan(self, plan_id: str, **fields: Any) -> Dict[str, Any]:
"""Update a plan.

Args:
plan_id: The plan's ID.
**fields: Fields to change, e.g. ``title``, ``summary``, ``status``.

Returns:
The updated plan dictionary.
"""
response = self._make_request("PATCH", f"/plans/{plan_id}", json=fields)
return response.json()

def delete_plan(self, plan_id: str) -> Dict[str, Any]:
"""Delete a plan.

Note:
This endpoint answers ``{"success": true}``, unlike the other
resources, which answer ``{"message": ...}``. Returned as-is rather
than normalised, so the SDK reports what the API actually sent.

Args:
plan_id: The plan's ID.

Returns:
``{"success": True}``.
"""
response = self._make_request("DELETE", f"/plans/{plan_id}")
return response.json()
Loading
Loading