diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2d42af8..bf769a3 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.14 +current_version = 3.0.0 commit = True tag = True tag_name = v{new_version} diff --git a/angles_python_client/__init__.py b/angles_python_client/__init__.py index 3dd53af..1a81d35 100644 --- a/angles_python_client/__init__.py +++ b/angles_python_client/__init__.py @@ -8,6 +8,7 @@ """ from .http import AnglesHttpClient +from .models.enums import ExecutionTypes from .reporter import AnglesReporter, angles_reporter from .requests import ( BuildRequests, @@ -22,6 +23,7 @@ __all__ = [ "AnglesHttpClient", + "ExecutionTypes", "AnglesReporter", "angles_reporter", "BuildRequests", diff --git a/angles_python_client/models/__init__.py b/angles_python_client/models/__init__.py index b2e9999..56ee60a 100644 --- a/angles_python_client/models/__init__.py +++ b/angles_python_client/models/__init__.py @@ -20,7 +20,7 @@ DiffRegion, ) -from .enums import ExecutionStates, StepStates, GroupingPeriods +from .enums import ExecutionStates, ExecutionTypes, StepStates, GroupingPeriods from .requests import ( CreateBuild, @@ -56,6 +56,7 @@ "Step", "Versions", "ExecutionStates", + "ExecutionTypes", "StepStates", "GroupingPeriods", "CreateBuild", diff --git a/angles_python_client/models/build.py b/angles_python_client/models/build.py index bb31383..f5b402a 100644 --- a/angles_python_client/models/build.py +++ b/angles_python_client/models/build.py @@ -6,7 +6,7 @@ from .artifact import Artifact from .environment import Environment -from .enums import ExecutionStates +from .enums import ExecutionStates, ExecutionTypes from .team import Team @@ -24,3 +24,5 @@ class Build: team: Optional[Team] = None component: Optional[str] = None suites: Optional[List[Any]] = None + #: Server-assigned; "automated" unless this build backs a manual test run. + executionType: Optional[ExecutionTypes] = None diff --git a/angles_python_client/models/enums.py b/angles_python_client/models/enums.py index 52c2f40..1315fce 100644 --- a/angles_python_client/models/enums.py +++ b/angles_python_client/models/enums.py @@ -18,6 +18,19 @@ class StepStates(str, Enum): FAIL = "FAIL" +class ExecutionTypes(str, Enum): + """Whether a build or execution came from an automated framework or a manual test run. + + Read-only from this client's point of view. The value is assigned by the Angles + server - a reporting client setting it to MANUAL would put a build on the dashboard + that no manual test run exists to explain - so it is absent from CreateBuild and + CreateExecution and only ever used to read results back or to filter a list call. + """ + + AUTOMATED = "automated" + MANUAL = "manual" + + class GroupingPeriods(str, Enum): DAY = "day" WEEK = "week" diff --git a/angles_python_client/models/execution.py b/angles_python_client/models/execution.py index c3aecf1..9a8021a 100644 --- a/angles_python_client/models/execution.py +++ b/angles_python_client/models/execution.py @@ -6,7 +6,7 @@ from .action import Action from .build import Build -from .enums import ExecutionStates +from .enums import ExecutionStates, ExecutionTypes from .platform import Platform @@ -24,3 +24,10 @@ class Execution: tags: Optional[List[str]] = None meta: Optional[Dict[str, Any]] = None status: Optional[ExecutionStates] = None + #: Server-assigned; "automated" unless this came from a manual test run. + executionType: Optional[ExecutionTypes] = None + #: The fields below are populated only on manual executions. + manualTestCase: Optional[str] = None + manualTestCaseVersion: Optional[str] = None + versionNumber: Optional[int] = None + executedBy: Optional[str] = None diff --git a/angles_python_client/models/responses.py b/angles_python_client/models/responses.py index a2bff3a..8542374 100644 --- a/angles_python_client/models/responses.py +++ b/angles_python_client/models/responses.py @@ -50,6 +50,9 @@ class Period: result: Optional[Dict[str, Any]] = None buildCount: Optional[int] = None phases: Optional[List[Any]] = None + #: {"automated": n, "manual": n} - counts every execution in the period, so a + #: stacked chart built from it matches result["TOTAL"]. + executionTypeBreakdown: Optional[Dict[str, int]] = None @dataclass @@ -58,6 +61,8 @@ class PhaseMetrics: fromDate: Optional[_dt.date] = None groupingPeriod: Optional[str] = None periods: Optional[List[Period]] = None + #: Echoes the requested filter; absent when both types are included. + executionType: Optional[str] = None @dataclass diff --git a/angles_python_client/models/step.py b/angles_python_client/models/step.py index 26762e7..45ec2b9 100644 --- a/angles_python_client/models/step.py +++ b/angles_python_client/models/step.py @@ -2,7 +2,7 @@ import datetime as _dt from dataclasses import dataclass -from typing import Optional +from typing import List, Optional from .enums import StepStates @@ -16,3 +16,5 @@ class Step: status: Optional[StepStates] = None timestamp: Optional[_dt.datetime] = None screenshot: Optional[str] = None + #: Attachment ids referenced by a manual step result. + attachments: Optional[List[str]] = None diff --git a/angles_python_client/requests.py b/angles_python_client/requests.py index bee3f08..49dddfd 100644 --- a/angles_python_client/requests.py +++ b/angles_python_client/requests.py @@ -7,9 +7,16 @@ from ._serialize import jsonable, json_dumps from .http import AnglesHttpClient -from .models.enums import GroupingPeriods +from .models.enums import ExecutionTypes, GroupingPeriods + +def _execution_type_param(execution_type: Optional[ExecutionTypes]) -> Optional[str]: + """Normalises an ExecutionTypes member (or a plain string) to its wire value.""" + if execution_type is None: + return None + return execution_type.value if hasattr(execution_type, "value") else str(execution_type) + class BaseRequests: def __init__(self, http: AnglesHttpClient): self.http = http @@ -77,21 +84,27 @@ class BuildRequests(BaseRequests): def create_build(self, request: Any) -> Any: return self.post("build", request) - def get_builds(self, team_id: str, build_ids: Optional[List[str]] = None, return_execution_details: bool = False) -> Any: + def get_builds(self, team_id: str, build_ids: Optional[List[str]] = None, return_execution_details: bool = False, execution_type: Optional[ExecutionTypes] = None) -> Any: # matches JS: /build?teamId=...&buildIds=...&returnExecutionDetails=... params: Dict[str, Any] = {"teamId": team_id} if build_ids: params["buildIds"] = ",".join(build_ids) if return_execution_details: params["returnExecutionDetails"] = "true" + execution_type_value = _execution_type_param(execution_type) + if execution_type_value: + params["executionType"] = execution_type_value return self.get("build", params=params) - def get_builds_with_filters(self, team_id: str, filter_environments: Optional[List[str]] = None, filter_components: Optional[List[str]] = None, skip: int = 0, limit: int = 50) -> Any: + def get_builds_with_filters(self, team_id: str, filter_environments: Optional[List[str]] = None, filter_components: Optional[List[str]] = None, skip: int = 0, limit: int = 50, execution_type: Optional[ExecutionTypes] = None) -> Any: params: Dict[str, Any] = {"teamId": team_id, "skip": skip, "limit": limit} if filter_environments: params["environmentIds"] = ",".join(filter_environments) if filter_components: params["componentIds"] = ",".join(filter_components) + execution_type_value = _execution_type_param(execution_type) + if execution_type_value: + params["executionType"] = execution_type_value return self.get("build", params=params) def get_builds_with_date_filters( @@ -103,6 +116,7 @@ def get_builds_with_date_filters( limit: int = 50, from_date: Optional[_dt.date] = None, to_date: Optional[_dt.date] = None, + execution_type: Optional[ExecutionTypes] = None, ) -> Any: params: Dict[str, Any] = {"teamId": team_id, "skip": skip, "limit": limit} if from_date: @@ -113,6 +127,9 @@ def get_builds_with_date_filters( params["environmentIds"] = ",".join(filter_environments) if filter_components: params["componentIds"] = ",".join(filter_components) + execution_type_value = _execution_type_param(execution_type) + if execution_type_value: + params["executionType"] = execution_type_value return self.get("build", params=params) def delete_builds(self, team_id: str, age_in_days: int) -> Any: @@ -363,6 +380,7 @@ def get_phase_metrics( from_date: Optional[_dt.date] = None, to_date: Optional[_dt.date] = None, grouping_period: Optional[GroupingPeriods] = None, + execution_type: Optional[ExecutionTypes] = None, ) -> Any: params: Dict[str, Any] = {"teamId": team_id} if component_id: @@ -373,6 +391,9 @@ def get_phase_metrics( params["toDate"] = to_date.isoformat() if grouping_period: params["groupingPeriod"] = grouping_period.value if hasattr(grouping_period, "value") else str(grouping_period) + execution_type_value = _execution_type_param(execution_type) + if execution_type_value: + params["executionType"] = execution_type_value # JS uses an absolute URL via new URL(baseURL + '/metrics/phase?...') # We'll just pass a relative path + params; client will build URL. return self.get("metrics/phase", params=params) diff --git a/pyproject.toml b/pyproject.toml index 25c44c9..885915e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "angles-python-client" -version = "1.0.14" +version = "3.0.0" description = "Python client for the Angles Dashboard API" readme = "README.md" requires-python = ">=3.9"