From d5b7993fd2a195758bb9b293cac7ea09c43d945e Mon Sep 17 00:00:00 2001 From: Thomas Brier <46268349+tombch@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:58:28 +0000 Subject: [PATCH] Added `identify` endpoint support in CLI and API (#59) * Added identifier endpoint support in CLI and API, and reshuffled API endpoint order to be consistent * Updated README.md --- README.md | 19 +-- onyx/api.py | 313 +++++++++++++++++++++++++++++------------------- onyx/cli.py | 39 ++++++ onyx/version.py | 2 +- 4 files changed, 237 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index 22cdc49..ead78c7 100644 --- a/README.md +++ b/README.md @@ -42,15 +42,16 @@ $ onyx │ --help -h Show this message and exit. │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ────────────────────────────────────────────────────────────────────────────────╮ -│ projects View available projects. │ -│ fields View the field specification for a project. │ -│ get Get a record from a project. │ -│ filter Filter multiple records from a project. │ -│ choices View options for a choice field. │ -│ profile View profile information. │ -│ siteusers View users from the same site. │ -│ auth Authentication commands. │ -│ admin Admin commands. │ +│ projects View available projects. │ +│ fields View the field specification for a project. │ +│ choices View options for a choice field. │ +│ get Get a record from a project. │ +│ filter Filter multiple records from a project. │ +│ identify Get the anonymised identifier for a value on a field. │ +│ profile View profile information. │ +│ siteusers View users from the same site. │ +│ auth Authentication commands. │ +│ admin Admin commands. │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ``` diff --git a/onyx/api.py b/onyx/api.py index 2eaa22f..2f7b772 100644 --- a/onyx/api.py +++ b/onyx/api.py @@ -17,95 +17,74 @@ class OnyxClientBase: __slots__ = "config", "_request_handler", "_session" ENDPOINTS = { - "register": lambda domain: OnyxClient._handle_endpoint( - lambda: os.path.join( - str(domain), - "accounts/register/", - ), - domain=domain, - ), - "login": lambda domain: OnyxClient._handle_endpoint( - lambda: os.path.join( - str(domain), - "accounts/login/", - ), - domain=domain, - ), - "logout": lambda domain: OnyxClient._handle_endpoint( - lambda: os.path.join( - str(domain), - "accounts/logout/", - ), - domain=domain, - ), - "logoutall": lambda domain: OnyxClient._handle_endpoint( - lambda: os.path.join( - str(domain), - "accounts/logoutall/", - ), - domain=domain, - ), - "profile": lambda domain: OnyxClient._handle_endpoint( + "projects": lambda domain: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), - "accounts/profile/", + "projects/", ), domain=domain, ), - "waiting": lambda domain: OnyxClient._handle_endpoint( + "fields": lambda domain, project: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), - "accounts/waiting/", + "projects", + str(project), + "fields/", ), domain=domain, + project=project, ), - "approve": lambda domain, username: OnyxClient._handle_endpoint( + "choices": lambda domain, project, field: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), - "accounts/approve", - str(username), + "projects", + str(project), + "choices", + str(field), "", ), domain=domain, - username=username, - ), - "siteusers": lambda domain: OnyxClient._handle_endpoint( - lambda: os.path.join( - str(domain), - "accounts/site/", - ), - domain=domain, + project=project, + field=field, ), - "allusers": lambda domain: OnyxClient._handle_endpoint( + "get": lambda domain, project, climb_id: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), - "accounts/all/", + "projects", + str(project), + str(climb_id), + "", ), domain=domain, + project=project, + climb_id=climb_id, ), - "projects": lambda domain: OnyxClient._handle_endpoint( + "filter": lambda domain, project: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), - "projects/", + "projects", + str(project), + "", ), domain=domain, + project=project, ), - "fields": lambda domain, project: OnyxClient._handle_endpoint( + "query": lambda domain, project: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), "projects", str(project), - "fields/", + "query/", ), domain=domain, project=project, ), - "choices": lambda domain, project, field: OnyxClient._handle_endpoint( + "identify": lambda domain, project, field: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), "projects", str(project), - "choices", + "identify", str(field), "", ), @@ -123,17 +102,17 @@ class OnyxClientBase: domain=domain, project=project, ), - "filter": lambda domain, project: OnyxClient._handle_endpoint( + "testcreate": lambda domain, project: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), "projects", str(project), - "", + "test/", ), domain=domain, project=project, ), - "get": lambda domain, project, climb_id: OnyxClient._handle_endpoint( + "update": lambda domain, project, climb_id: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), "projects", @@ -145,11 +124,12 @@ class OnyxClientBase: project=project, climb_id=climb_id, ), - "update": lambda domain, project, climb_id: OnyxClient._handle_endpoint( + "testupdate": lambda domain, project, climb_id: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), "projects", str(project), + "test", str(climb_id), "", ), @@ -169,38 +149,71 @@ class OnyxClientBase: project=project, climb_id=climb_id, ), - "query": lambda domain, project: OnyxClient._handle_endpoint( + "register": lambda domain: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), - "projects", - str(project), - "query/", + "accounts/register/", ), domain=domain, - project=project, ), - "testcreate": lambda domain, project: OnyxClient._handle_endpoint( + "login": lambda domain: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), - "projects", - str(project), - "test/", + "accounts/login/", ), domain=domain, - project=project, ), - "testupdate": lambda domain, project, climb_id: OnyxClient._handle_endpoint( + "logout": lambda domain: OnyxClient._handle_endpoint( lambda: os.path.join( str(domain), - "projects", - str(project), - "test", - str(climb_id), + "accounts/logout/", + ), + domain=domain, + ), + "logoutall": lambda domain: OnyxClient._handle_endpoint( + lambda: os.path.join( + str(domain), + "accounts/logoutall/", + ), + domain=domain, + ), + "profile": lambda domain: OnyxClient._handle_endpoint( + lambda: os.path.join( + str(domain), + "accounts/profile/", + ), + domain=domain, + ), + "waiting": lambda domain: OnyxClient._handle_endpoint( + lambda: os.path.join( + str(domain), + "accounts/waiting/", + ), + domain=domain, + ), + "approve": lambda domain, username: OnyxClient._handle_endpoint( + lambda: os.path.join( + str(domain), + "accounts/approve", + str(username), "", ), domain=domain, - project=project, - climb_id=climb_id, + username=username, + ), + "siteusers": lambda domain: OnyxClient._handle_endpoint( + lambda: os.path.join( + str(domain), + "accounts/site/", + ), + domain=domain, + ), + "allusers": lambda domain: OnyxClient._handle_endpoint( + lambda: os.path.join( + str(domain), + "accounts/all/", + ), + domain=domain, ), } @@ -275,42 +288,6 @@ def _request(self, method: str, retries: int = 3, **kwargs) -> requests.Response return method_response - @classmethod - def to_csv( - cls, - csv_file: TextIO, - data: Union[List[Dict[str, Any]], Generator[Dict[str, Any], Any, None]], - delimiter: Optional[str] = None, - ): - # Ensure data is an iterator - if inspect.isgenerator(data): - data_iterator = data - else: - data_iterator = iter(data) - - row = next(data_iterator, None) - if row: - fields = row.keys() - - # Create CSV writer - if delimiter is None: - writer = csv.DictWriter( - csv_file, - fieldnames=fields, - ) - else: - writer = csv.DictWriter( - csv_file, - fieldnames=fields, - delimiter=delimiter, - ) - - # Write data - writer.writeheader() - writer.writerow(row) - for row in data_iterator: - writer.writerow(row) - def _csv_upload( self, method: str, @@ -431,24 +408,6 @@ def choices(self, project: str, field: str) -> requests.Response: ) return response - def create( - self, - project: str, - fields: Dict[str, Any], - test: bool = False, - ) -> requests.Response: - if test: - endpoint = "testcreate" - else: - endpoint = "create" - - response = self._request( - method="post", - url=OnyxClient.ENDPOINTS[endpoint](self.config.domain, project), - json=fields, - ) - return response - def get( self, project: str, @@ -540,6 +499,68 @@ def query( else: _next = None + @classmethod + def to_csv( + cls, + csv_file: TextIO, + data: Union[List[Dict[str, Any]], Generator[Dict[str, Any], Any, None]], + delimiter: Optional[str] = None, + ): + # Ensure data is an iterator + if inspect.isgenerator(data): + data_iterator = data + else: + data_iterator = iter(data) + + row = next(data_iterator, None) + if row: + fields = row.keys() + + # Create CSV writer + if delimiter is None: + writer = csv.DictWriter( + csv_file, + fieldnames=fields, + ) + else: + writer = csv.DictWriter( + csv_file, + fieldnames=fields, + delimiter=delimiter, + ) + + # Write data + writer.writeheader() + writer.writerow(row) + for row in data_iterator: + writer.writerow(row) + + def identify(self, project: str, field: str, value: str) -> requests.Response: + response = self._request( + method="post", + url=OnyxClient.ENDPOINTS["identify"](self.config.domain, project, field), + json={"value": value}, + ) + return response + + def create( + self, + project: str, + fields: Dict[str, Any], + test: bool = False, + ) -> requests.Response: + if test: + endpoint = "testcreate" + else: + endpoint = "create" + + response = self._request( + method="post", + url=OnyxClient.ENDPOINTS[endpoint](self.config.domain, project), + json=fields, + ) + return response + def update( self, project: str, @@ -1407,6 +1428,46 @@ def to_csv( delimiter=delimiter, ) + @onyx_errors + def identify(self, project: str, field: str, value: str) -> Dict[str, str]: + """ + Get the anonymised identifier for a value on a field. + + Args: + project: Name of the project. + field: Field on the project. + value: Value to identify. + + Returns: + Dict containing the field, value and anonymised identifier. + + Examples: + ```python + import os + from onyx import OnyxConfig, OnyxEnv, OnyxClient + + config = OnyxConfig( + domain=os.environ[OnyxEnv.DOMAIN], + token=os.environ[OnyxEnv.TOKEN], + ) + + with OnyxClient(config) as client: + identification = client.identify("project", "sample_id", "hidden-value") + ``` + ```python + >>> identification + { + "field": "sample_id", + "value": "hidden-value", + "identifier": "S-1234567890", + } + ``` + """ + + response = super().identify(project, field, value) + response.raise_for_status() + return response.json()["data"] + @onyx_errors def create( self, diff --git a/onyx/cli.py b/onyx/cli.py index 2a1b76e..91bf61b 100644 --- a/onyx/cli.py +++ b/onyx/cli.py @@ -586,6 +586,45 @@ def filter( handle_error(e) +@app.command() +def identify( + context: typer.Context, + project: str = typer.Argument(...), + field: str = typer.Argument(...), + value: str = typer.Argument(...), + format: Optional[InfoFormats] = typer.Option( + InfoFormats.TABLE.value, + "-F", + "--format", + help=HelpText.FORMAT.value, + ), +): + """ + Get the anonymised identifier for a value on a field. + """ + + try: + api = setup_onyx_api(context.obj) + identified = api.client.identify(project, field, value) + if format == InfoFormats.TABLE: + table = Table( + show_lines=True, + ) + table.add_column("Field") + table.add_column("Value") + table.add_column("Identifier") + table.add_row( + identified["field"], + identified["value"], + identified["identifier"], + ) + console.print(table) + else: + typer.echo(json_dump_pretty(identified)) + except Exception as e: + handle_error(e) + + @app.command() def profile( context: typer.Context, diff --git a/onyx/version.py b/onyx/version.py index 131942e..f5f41e5 100644 --- a/onyx/version.py +++ b/onyx/version.py @@ -1 +1 @@ -__version__ = "3.0.2" +__version__ = "3.1.0"