class HTTPXSyncAdapter(SyncRequestAdapter): def __init__(self, session=None, **kwargs): self.session = session from httpx import Client, HTTPError if session: self.session = session else: self.session = Client(**kwargs) self.error = (HTTPError, InvalidSchemaError) def __enter__(self): return self def __exit__(self, *args): pass def __del__(self): if self.session: self.session.close()
class ConcourseSession: def __init__(self, concourse_url: str, token_provider: TokenProvider): self.concourse_url = concourse_url self._token_provider = token_provider self._client = None @property def is_open(self): return self._client is not None @property def client(self): if self._client is None: self._client = Client() self._update_token(self._client, False) return self._client def _make_request(self, make_request: Callable[[Client], Response]) -> Response: client = self.client for _ in range(2): r = make_request(client) if r.status_code == httpx.codes.UNAUTHORIZED: logging.warning( f"Got {r.status_code} when trying to access {self.concourse_url}" ) client.headers.update({"Authorization": ""}) self._update_token(client, True) else: return r raise Exception("Could not connect to concourse server") def _build_url(self, *args) -> str: parts = [self.concourse_url] parts.extend(str(p) for p in args) return '/'.join(parts) def _build_teams_url(self, *args): return self._build_url("api", "v1", "teams", *args) def _build_pipeline_url(self, team: str, pipeline: str): return self._build_teams_url(team, "pipelines", pipeline, "config") def _update_token(self, client: Client, force_new: bool): token = self._token_provider.get_token(client, force_new, self.concourse_url) client.headers.update({"Authorization": f"bearer {token}"}) def close(self): if self._client is None: return try: self._client.close() except Exception as e: logging.error(e) finally: self._client = None def get_pipeline(self, team: str, pipeline: str) -> Tuple[dict, str]: r = self._make_request( lambda c: c.get(self._build_pipeline_url(team, pipeline))) # https://github.com/concourse/concourse/blob/d55be66cc6b10101a8b4ddd9122e437cb10ccf52/go-concourse/concourse/configs.go#L40 if r.status_code == 404: return ({}, "") r.raise_for_status() pipeline_version = expect(r.headers.get("X-Concourse-Config-Version"), "BUG: X-Concourse-Config-Version not found") pipeline_cfg = r.json()["config"] return (pipeline_cfg, pipeline_version) def set_pipeline(self, team: str, pipeline: str, config: dict, pipeline_version: str): headers = {"X-Concourse-Config-Version": pipeline_version} # https://github.com/concourse/concourse/blob/d55be66cc6b10101a8b4ddd9122e437cb10ccf52/go-concourse/concourse/configs.go#L76 r = self._make_request( lambda c: c.put(self._build_pipeline_url(team, pipeline), json=config, headers=headers)) r.raise_for_status()
def client(): client = Client() client = CachingClient(client) yield client client.close()
class Blocking(Base, BlockingHttp): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._client = Client(auth=self._basic_auth, timeout=self._timeout) def __enter__(self) -> Blocking: return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: self.close() def close(self) -> None: """Closes sessions """ self._client.close() def match(self, match_id: str) -> BlockingMatch: """Used to interact with a match. Parameters ---------- match_id : str Dathost Match ID. Returns ------- BlockingMatch """ return BlockingMatch(self, match_id) def create_server( self, settings: ServerSettings) -> Tuple[ServerModel, ServerBlocking]: """Creates a new server. Parameters ---------- settings: ServerSettings Used to configure server. Returns ------- ServerModel Holds data on server. ServerBlocking Used to interact with the created server. """ data = cast( dict, self._post( url=SERVER.create, read_json=True, data=settings.payload, )) return ServerModel(data), self.server(data["id"]) def server(self, server_id: str) -> ServerBlocking: """Used for interacting with a server. Parameters ---------- server_id : str Datahost server ID. Returns ------- ServerBlocking Used to interact with the server. """ return ServerBlocking(self, server_id) def servers( self) -> Generator[Tuple[ServerModel, ServerBlocking], None, None]: """Used to list servers. Yields ------- ServerModel Holds data on server. ServerBlocking Used to interact with server. """ for server in cast(dict, self._get(SERVER.list)): yield ServerModel(server), self.server(server["id"]) def account(self) -> AccountModel: """Gets account details Returns ------- AccountModel Holds data on a account. """ return AccountModel(cast(dict, self._get(ACCOUNT.details))) def domains(self) -> Generator[str, None, None]: """Used to list domains. Returns ------- list List of domains. """ for domain in cast(list, self._get(CUSTOM_DOMAINS.details)): yield domain["name"]
class Blocking(Base, BlockingHTTP): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._client = Client(timeout=self._timeout, limits=self._limits, headers={"User-Agent": self._user_agent}) def close(self) -> None: """Closes any underlying TCP sessions. """ self._client.close() @authorize_required def download_by_name(self, bucket_name: str, file_name: str, settings: DownloadSettings = None) -> bytes: """Used to download a file by its name. Parameters ---------- bucket_name : str Name of bucket. file_name : str Name of file to download. settings : DownloadSettings, optional by default None Returns ------- bytes Notes ----- Use bucket.file.download instead whenever you can. """ if not settings: params = None headers = None else: params = settings.parameters headers = settings.headers return cast( bytes, self._get(url="{}/{}/{}".format(self._routes.download.file, bucket_name, file_name), headers=headers, params=params, resp_json=False, include_account=False)) @authorize_required def create_key(self, settings: KeySettings) -> Tuple[KeyModel, BlockingKey]: """Used to create a key. Parameters ---------- settings: KeySettings Used to hold details on a key. Returns ------- KeyModel Holds details on key. BlockingKey """ data = cast( dict, self._post(json=settings.payload, url=self._routes.key.create)) return KeyModel(data), self.key(data["applicationKeyId"]) @authorize_required def keys( self, limit: int = 100, start_key_id: str = None ) -> Generator[Tuple[KeyModel, BlockingKey], None, None]: """Used to list keys. Parameters ---------- limit : int, optional Used to limit the listing, by default 100 start_key_id : str, optional Key to start listing from, by default None Yields ------- KeyModel Holds details on key. BlockingKey """ data = cast( dict, self._post(url=self._routes.key.list, json={ "maxKeyCount": limit, "startApplicationKeyId": start_key_id })) for key in data["keys"]: yield KeyModel(key), self.key(key["applicationKeyId"]) def key(self, key_id: str) -> BlockingKey: """Used to interact with key. Parameters ---------- key_id : str ID of key. Returns ------- BlockingKey """ return BlockingKey(self, key_id) @authorize_required def create_bucket( self, settings: BucketSettings) -> Tuple[BucketModel, BlockingBucket]: """Used to create a bucket. Parameters ---------- settings: BucketSettings Holds bucket settings. Returns ------- BucketModel Holds details on a bucket. BlockingBucket Used to interact with a bucket. """ data = BucketModel( cast( dict, self._post( url=self._routes.bucket.create, json=settings.payload, ))) return data, self.bucket(data.bucket_id) @authorize_required def buckets( self, types: list = ["all"] ) -> Generator[Tuple[BucketModel, BlockingBucket], None, None]: """Lists buckets. Parameters ---------- types : list, optional Used to filter bucket types, by default ["all"] Yields ------- BucketModel Holds details on bucket. BlockingBucket Used for interacting with bucket. """ data = cast( dict, self._post(url=self._routes.bucket.list, json={"bucketTypes": types})) for bucket in data["buckets"]: yield BucketModel(bucket), self.bucket(bucket["bucketId"]) def bucket(self, bucket_id: str) -> BlockingBucket: """Used to interact with a bucket. Parameters ---------- bucket_id : str ID of Bucket. Returns ------- BlockingBucket """ return BlockingBucket(self, bucket_id) def __authorize_background(self) -> None: """Used to refresh auth tokens every 23.5 hours. """ self._running_task = True time.sleep(self._refresh_seconds + randint(0, 1500)) self._running_task = False self.authorize() self._check_cache() def authorize(self) -> AuthModel: """Used to authorize B2 account. Returns ------- AuthModel Holds data on account auth. """ resp = self._client.get(self._auth_url, auth=self._auth) resp.raise_for_status() data = AuthModel(resp.json()) self.account_id = data.account_id self._format_routes(data.api_url, data.download_url) self._client.headers["Authorization"] = data.authorization_token if not self._running_task: threading.Thread(target=self.__authorize_background) return data
class ZebrunnerAPI(metaclass=Singleton): authenticated = False def __init__(self, service_url: str = None, access_token: str = None): if service_url and access_token: self.service_url = service_url.rstrip("/") self.access_token = access_token self._client = Client() self._auth_token = None self.authenticated = False def _sign_request(self, request: Request) -> Request: request.headers["Authorization"] = f"Bearer {self._auth_token}" return request def auth(self) -> None: if not self.access_token or not self.service_url: return url = self.service_url + "/api/iam/v1/auth/refresh" try: response = self._client.post( url, json={"refreshToken": self.access_token}) except httpx.RequestError as e: logger.warning("Error while sending request to zebrunner.", exc_info=e) return if response.status_code != 200: log_response(response, logging.ERROR) return self._auth_token = response.json()["authToken"] self._client.auth = self._sign_request self.authenticated = True def start_test_run(self, project_key: str, body: StartTestRunModel) -> Optional[int]: url = self.service_url + "/api/reporting/v1/test-runs" try: response = self._client.post(url, params={"projectKey": project_key}, json=body.dict(exclude_none=True, by_alias=True)) except httpx.RequestError as e: logger.warning("Error while sending request to zebrunner.", exc_info=e) return None if response.status_code != 200: log_response(response, logging.ERROR) return None return response.json()["id"] def start_test(self, test_run_id: int, body: StartTestModel) -> Optional[int]: url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}/tests" try: response = self._client.post(url, json=body.dict(exclude_none=True, by_alias=True)) except httpx.RequestError as e: logger.warning("Error while sending request to zebrunner.", exc_info=e) return None if response.status_code != 200: log_response(response, logging.ERROR) return None return response.json()["id"] def finish_test(self, test_run_id: int, test_id: int, body: FinishTestModel) -> None: url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}/tests/{test_id}" try: response = self._client.put(url, json=body.dict(exclude_none=True, by_alias=True)) except httpx.RequestError as e: logger.warning("Error while sending request to zebrunner.", exc_info=e) return if response.status_code != 200: log_response(response, logging.ERROR) def finish_test_run(self, test_run_id: int) -> None: url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}" try: response = self._client.put( url, json={ "endedAt": (datetime.utcnow().replace(tzinfo=timezone.utc) - timedelta(seconds=1)).isoformat() }, ) except httpx.RequestError as e: logger.warning("Error while sending request to zebrunner.", exc_info=e) return if response.status_code != 200: log_response(response, logging.ERROR) return def send_logs(self, test_run_id: int, logs: List[LogRecordModel]) -> None: url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}/logs" body = [x.dict(exclude_none=True, by_alias=True) for x in logs] self._client.post(url, json=body) def send_screenshot(self, test_run_id: int, test_id: int, image_path: Union[str, Path]) -> None: url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}/tests/{test_id}/screenshots" with open(image_path, "rb") as image: self._client.post( url, content=image.read(), headers={ "Content-Type": "image/png", "x-zbr-screenshot-captured-at": round(time.time() * 1000) }, ) def send_artifact(self, filename: Union[str, Path], test_run_id: int, test_id: Optional[int] = None) -> None: if test_id: url = f"{self.service_url}/api/reporting/v1/test-runs/{test_run_id}/tests/{test_id}/artifacts" else: url = f"{self.service_url}/api/reporting/v1/test-runs/{test_run_id}/artifacts" with open(filename, "rb") as file: self._client.post(url, files={"file": file}) def send_artifact_references(self, references: List[ArtifactReferenceModel], test_run_id: int, test_id: Optional[int] = None) -> None: if test_id: url = f"{self.service_url}/api/reporting/v1/test-runs/{test_run_id}/tests/{test_id}/artifact-references" else: url = f"{self.service_url}/api/reporting/v1/test-runs/{test_run_id}/artifact-references/" json_items = [ item.dict(exclude_none=True, by_alias=True) for item in references ] self._client.put(url, json={"items": json_items}) def send_labels(self, labels: List[LabelModel], test_run_id: int, test_id: Optional[int] = None) -> None: if test_id: url = f"{self.service_url}/api/reporting/v1/test-runs/{test_run_id}/tests/{test_id}/labels" else: url = f"{self.service_url}/api/reporting/v1/test-runs/{test_run_id}/labels" labels_json = [ label.dict(exclude_none=True, by_alias=True) for label in labels ] self._client.put(url, json={"items": labels_json}) def start_test_session(self, test_run_id: int, body: StartTestSessionModel) -> Optional[str]: url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}/test-sessions" response = self._client.post(url, json=body.dict(exclude_none=True, by_alias=True)) if not response.status_code == 200: log_response(response, logging.ERROR) return None return response.json().get("id") def finish_test_session(self, test_run_id: int, zebrunner_id: str, body: FinishTestSessionModel) -> None: url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}/test-sessions/{zebrunner_id}" self._client.put(url, json=body.dict(exclude_none=True, by_alias=True)) def get_rerun_tests(self, run_context: str) -> RerunDataModel: url = self.service_url + "/api/reporting/v1/run-context-exchanges" run_context_dict = json.loads(run_context) response = self._client.post(url, json=run_context_dict) response_data = response.json() for test in response_data["tests"]: correlation_data = test["correlationData"] if correlation_data is not None: test["correlationData"] = json.loads(correlation_data) return RerunDataModel(**response_data) def close(self) -> None: self._client.close()