From 302028d680ed886daebf237f0446fe76747055e3 Mon Sep 17 00:00:00 2001 From: Vadym Moshynskyi Date: Wed, 26 Nov 2025 13:04:09 +0100 Subject: [PATCH 1/2] IEBH-353: Sync with the latest changes --- .env.example | 9 - .github/workflows/hdc-pipeline.yml | 36 +++ .pre-commit-config.yaml | 2 +- Jenkinsfile | 128 --------- README.md | 1 - app/components/request/__init__.py | 5 + app/components/request/context.py | 41 +++ app/components/request/http_client.py | 93 +++++++ app/config.py | 49 +--- app/main.py | 2 +- app/models/file_models.py | 8 +- app/models/project_models.py | 10 +- app/resources/authorization/decorator.py | 5 +- app/resources/dependencies.py | 3 +- app/resources/error_handler.py | 4 +- app/resources/health_check.py | 4 +- app/resources/helpers.py | 15 +- app/resources/validation_service.py | 2 +- app/routers/v1/api_dataset.py | 10 +- app/routers/v1/api_project.py | 22 +- app/services/dataset/client.py | 19 +- poetry.lock | 262 +++++++++--------- pyproject.toml | 17 +- tests/app/components/request/__init__.py | 5 + .../components/request/test_http_client.py | 27 ++ tests/conftest.py | 1 + tests/fixtures/fake.py | 6 +- tests/fixtures/request_context.py | 18 ++ tests/fixtures/services/dataset.py | 4 +- tests/routers/v1/test_api_dataset.py | 2 +- tests/routers/v1/test_api_file.py | 1 - 31 files changed, 423 insertions(+), 388 deletions(-) create mode 100644 .github/workflows/hdc-pipeline.yml delete mode 100644 Jenkinsfile create mode 100644 app/components/request/__init__.py create mode 100644 app/components/request/context.py create mode 100644 app/components/request/http_client.py create mode 100644 tests/app/components/request/__init__.py create mode 100644 tests/app/components/request/test_http_client.py create mode 100644 tests/fixtures/request_context.py diff --git a/.env.example b/.env.example index 05d6d54..2af6bb7 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,5 @@ -# Vault configuration -# is sensitive/secret -VAULT_TOKEN= - -# contains defaults but can be overriden -VAULT_URL= -VAULT_CRT= - # Service configuration # contains defaults but can be overriden -CONFIG_CENTER_ENABLED=false APP_NAME=bff-cli version=2.0.0 port=5080 diff --git a/.github/workflows/hdc-pipeline.yml b/.github/workflows/hdc-pipeline.yml new file mode 100644 index 0000000..7500b59 --- /dev/null +++ b/.github/workflows/hdc-pipeline.yml @@ -0,0 +1,36 @@ +name: HDC ci/cd pipeline + +permissions: + contents: write + issues: write + pull-requests: write + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + run_tests_hdc: + uses: PilotDataPlatform/pilot-hdc-ci-tools/.github/workflows/run_tests.yml@main + with: + min_coverage_percent: 84 + secrets: inherit + + build_and_publish_hdc: + needs: [run_tests_hdc] + uses: PilotDataPlatform/pilot-hdc-ci-tools/.github/workflows/build_and_publish.yml@main + with: + matrix_config: '["bff-cli"]' + service_name: 'bff-cli' + secrets: inherit + + deploy_hdc: + needs: [build_and_publish_hdc] + uses: PilotDataPlatform/pilot-hdc-ci-tools/.github/workflows/trigger_deployment.yml@main + with: + hdc_service_name: 'bff-cli' + secrets: inherit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f755eb..96be327 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -68,7 +68,7 @@ repos: ] - repo: https://github.com/PyCQA/docformatter - rev: v1.7.5 + rev: v1.7.7 hooks: - id: docformatter args: [ diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 760c200..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,128 +0,0 @@ -pipeline { - agent { label 'small' } - environment { - imagename_dev = "ghcr.io/pilotdataplatform/bff_cli" - imagename_staging = "ghcr.io/pilotdataplatform/bff_cli" - commit = sh(returnStdout: true, script: 'git describe --always').trim() - registryCredential = 'pilot-ghcr' - registryURL = "https://github.com/PilotDataPlatform/bff-cli.git" - registryURLBase = "https://ghcr.io" - dockerImage = '' - } - - stages { - - stage('Git clone for dev') { - when {branch "develop"} - steps{ - script { - git branch: "develop", - url: "$registryURL", - credentialsId: 'lzhao' - } - } - } -/** - stage('DEV unit test') { - when {branch "develop"} - steps{ - string(credentialsId:'VAULT_TOKEN', variable: 'VAULT_TOKEN'), - string(credentialsId:'VAULT_URL', variable: 'VAULT_URL'), - file(credentialsId:'VAULT_CRT', variable: 'VAULT_CRT') - { - sh """ - export CONFIG_CENTER_ENABLED='true' - export VAULT_TOKEN=${VAULT_TOKEN} - export VAULT_URL=${VAULT_URL} - export VAULT_CRT=${VAULT_CRT} - pip3 install virtualenv - /home/indoc/.local/bin/virtualenv -p python3.8 venv - . venv/bin/activate - PIP_USERNAME=${PIP_USERNAME} PIP_PASSWORD=${PIP_PASSWORD} pip3 install -r requirements.txt -r internal_requirements.txt -r tests/test_requirements.txt - pip freeze | grep logger - pytest -c tests/pytest.ini - """ - } - } - } -**/ - stage('DEV Build and push image') { - when {branch "develop"} - steps{ - script { - docker.withRegistry("$registryURLBase", registryCredential) { - customImage = docker.build("$imagename_dev:$commit-CAC", ".") - customImage.push() - } - } - } - } - stage('DEV Remove image') { - when {branch "develop"} - steps{ - sh "docker rmi $imagename_dev:$commit-CAC" - } - } - - stage('DEV Deploy') { - when {branch "develop"} - steps{ - build(job: "/VRE-IaC/UpdateAppVersion", parameters: [ - [$class: 'StringParameterValue', name: 'TF_TARGET_ENV', value: 'dev' ], - [$class: 'StringParameterValue', name: 'TARGET_RELEASE', value: 'bff-vrecli' ], - [$class: 'StringParameterValue', name: 'NEW_APP_VERSION', value: "$commit-CAC" ] - ]) - } - } -/** - stage('Git clone staging') { - when {branch "main"} - steps{ - script { - git branch: "main", - url: "registryURL", - credentialsId: 'lzhao' - } - } - } - - stage('STAGING Building and push image') { - when {branch "main"} - steps{ - script { - withCredentials([usernamePassword(credentialsId:'readonly', usernameVariable: 'PIP_USERNAME', passwordVariable: 'PIP_PASSWORD')]) { - docker.withRegistry("$registryURLBase", registryCredential) { - customImage = docker.build("$imagename_staging:$commit", ".") - customImage.push() - } - } - } - } - } - - stage('STAGING Remove image') { - when {branch "main"} - steps{ - sh "docker rmi $imagename_staging:$commit" - } - } - - stage('STAGING Deploy') { - when {branch "main"} - steps{ - build(job: "/VRE-IaC/Staging-UpdateAppVersion", parameters: [ - [$class: 'StringParameterValue', name: 'TF_TARGET_ENV', value: 'staging' ], - [$class: 'StringParameterValue', name: 'TARGET_RELEASE', value: 'bff-vrecli' ], - [$class: 'StringParameterValue', name: 'NEW_APP_VERSION', value: "$commit" ] - ]) - } - } -**/ - } - post { - failure { - slackSend color: '#FF0000', message: "Build Failed! - ${env.JOB_NAME} $commit (<${env.BUILD_URL}|Open>)", channel: 'jenkins-dev-staging-monitor' - } - } - -} diff --git a/README.md b/README.md index 7a08d44..3c1c360 100644 --- a/README.md +++ b/README.md @@ -68,4 +68,3 @@ The development of the HealthDataCloud open source software was supported by the This project has received funding from the European Union’s Horizon Europe research and innovation programme under grant agreement No 101058516. Views and opinions expressed are however those of the author(s) only and do not necessarily reflect those of the European Union or other granting authorities. Neither the European Union nor other granting authorities can be held responsible for them. ![EU HDC Acknowledgement](https://hdc.humanbrainproject.eu/img/HDC-EU-acknowledgement.png) - diff --git a/app/components/request/__init__.py b/app/components/request/__init__.py new file mode 100644 index 0000000..105ee19 --- /dev/null +++ b/app/components/request/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2022-Present Indoc Systems +# +# Licensed under the GNU AFFERO GENERAL PUBLIC LICENSE, +# Version 3.0 (the "License") available at https://www.gnu.org/licenses/agpl-3.0.en.html. +# You may not use this file except in compliance with the License. diff --git a/app/components/request/context.py b/app/components/request/context.py new file mode 100644 index 0000000..e7c9892 --- /dev/null +++ b/app/components/request/context.py @@ -0,0 +1,41 @@ +# Copyright (C) 2022-Present Indoc Systems +# +# Licensed under the GNU AFFERO GENERAL PUBLIC LICENSE, +# Version 3.0 (the "License") available at https://www.gnu.org/licenses/agpl-3.0.en.html. +# You may not use this file except in compliance with the License. + +from typing import Annotated + +from fastapi import Depends +from fastapi import Request + +from app.components.request.http_client import HTTPClient +from app.config import SettingsDependency + + +class RequestContext: + def __init__(self, *, request: Request, client_timeout: int, allowed_headers: set[str] | None = None) -> None: + self.request = request + + self.allowed_headers = allowed_headers or { + 'forwarded', + 'x-forwarded-for', + 'x-userinfo', + 'authorization', + 'session-id', + 'vm-info', + } + self.headers = {} + + for key, value in self.request.headers.items(): + if key in self.allowed_headers: + self.headers[key] = value + + self.client = HTTPClient(headers=self.headers, timeout=client_timeout) + + +def get_request_context(request: Request, settings: SettingsDependency) -> RequestContext: + return RequestContext(request=request, client_timeout=settings.SERVICE_CLIENT_TIMEOUT) + + +RequestContextDependency = Annotated[RequestContext, Depends(get_request_context)] diff --git a/app/components/request/http_client.py b/app/components/request/http_client.py new file mode 100644 index 0000000..d8735a8 --- /dev/null +++ b/app/components/request/http_client.py @@ -0,0 +1,93 @@ +# Copyright (C) 2022-Present Indoc Systems +# +# Licensed under the GNU AFFERO GENERAL PUBLIC LICENSE, +# Version 3.0 (the "License") available at https://www.gnu.org/licenses/agpl-3.0.en.html. +# You may not use this file except in compliance with the License. + +from typing import Any + +from httpx import URL +from httpx import AsyncClient +from httpx import Response +from httpx._types import HeaderTypes +from httpx._types import QueryParamTypes +from httpx._types import RequestContent +from httpx._types import RequestData +from httpx._types import TimeoutTypes + + +class HTTPClient: + + def __init__(self, *, headers: HeaderTypes, timeout: TimeoutTypes) -> None: + self.headers = headers + self.timeout = timeout + + @property + def client(self) -> AsyncClient: + return AsyncClient(headers=self.headers, timeout=self.timeout) + + async def get( + self, + url: URL | str, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + ) -> Response: + async with self.client as client: + return await client.request('GET', url, params=params, headers=headers) + + async def post( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + json: Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + ) -> Response: + async with self.client as client: + return await client.request( + 'POST', url, content=content, data=data, json=json, params=params, headers=headers + ) + + async def put( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + json: Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + ) -> Response: + async with self.client as client: + return await client.request( + 'PUT', url, content=content, data=data, json=json, params=params, headers=headers + ) + + async def patch( + self, + url: URL | str, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + json: Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + ) -> Response: + async with self.client as client: + return await client.request( + 'PATCH', url, content=content, data=data, json=json, params=params, headers=headers + ) + + async def delete( + self, + url: URL | str, + *, + json: Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + ) -> Response: + async with self.client as client: + return await client.request('DELETE', url, json=json, params=params, headers=headers) diff --git a/app/config.py b/app/config.py index 183018f..e213695 100644 --- a/app/config.py +++ b/app/config.py @@ -6,42 +6,15 @@ import logging from functools import lru_cache -from typing import Any -from typing import Dict -from typing import Optional +from typing import Annotated -from common import VaultClient +from fastapi import Depends from pydantic import BaseSettings from pydantic import Extra -class VaultConfig(BaseSettings): - """Store vault related configuration.""" - - APP_NAME: str = 'bff-cli' - CONFIG_CENTER_ENABLED: bool = False - - VAULT_URL: Optional[str] - VAULT_CRT: Optional[str] - VAULT_TOKEN: Optional[str] - - class Config: - env_file = '.env' - env_file_encoding = 'utf-8' - - -def load_vault_settings(settings: BaseSettings) -> Dict[str, Any]: - config = VaultConfig() - - if not config.CONFIG_CENTER_ENABLED: - return {} - - client = VaultClient(config.VAULT_URL, config.VAULT_CRT, config.VAULT_TOKEN) - return client.get_from_vault(config.APP_NAME) - - class Settings(BaseSettings): - version = '2.2.0a0' + version = '2.2.14' port: int = 5080 host: str = '0.0.0.0' @@ -93,20 +66,6 @@ class Config: env_file_encoding = 'utf-8' extra = Extra.allow - @classmethod - def customise_sources( - cls, - init_settings, - env_settings, - file_secret_settings, - ): - return ( - env_settings, - load_vault_settings, - init_settings, - file_secret_settings, - ) - @lru_cache(1) def get_settings(): @@ -115,4 +74,6 @@ def get_settings(): return settings +SettingsDependency = Annotated[Settings, Depends(get_settings)] + ConfigClass = get_settings() diff --git a/app/main.py b/app/main.py index e3d3c2d..c68b72b 100644 --- a/app/main.py +++ b/app/main.py @@ -42,7 +42,7 @@ def instrument_app(app) -> None: def create_app(): - """create app function.""" + """Create app function.""" app = FastAPI(title='BFF CLI', description='BFF for cli', docs_url='/v1/api-doc', version=ConfigClass.version) configure_logging(ConfigClass.LOGGING_LEVEL, ConfigClass.LOGGING_FORMAT) diff --git a/app/models/file_models.py b/app/models/file_models.py index 022de6c..6bc88f5 100644 --- a/app/models/file_models.py +++ b/app/models/file_models.py @@ -13,17 +13,19 @@ class ItemStatus(str, Enum): - """The new enum type for file status + """The new enum type for file status. + - REGISTERED means file is created by upload service but not complete yet. either in progress or fail. - ACTIVE means file uploading is complete. - - ARCHIVED means the file has been deleted.""" + - ARCHIVED means the file has been deleted. + """ REGISTERED = 'REGISTERED' ACTIVE = 'ACTIVE' ARCHIVED = 'ARCHIVED' def __str__(self): - return '%s' % self.name + return f'{self.name}' class GetProjectFileList(BaseModel): diff --git a/app/models/project_models.py b/app/models/project_models.py index 52c9e60..dafba29 100644 --- a/app/models/project_models.py +++ b/app/models/project_models.py @@ -5,8 +5,6 @@ # You may not use this file except in compliance with the License. from typing import Any -from typing import Dict -from typing import List from pydantic import BaseModel from pydantic import Field @@ -44,8 +42,8 @@ class POSTProjectFile(BaseModel): zone: str current_folder_node: str parent_folder_id: str - data: List[Dict[str, Any]] - folder_tags: List[str] = [] + data: list[dict[str, Any]] + folder_tags: list[str] = [] class ResumableUploadPOST(BaseModel): @@ -58,14 +56,14 @@ class ObjectInfo(BaseModel): bucket: str zone: str - object_infos: List[ObjectInfo] + object_infos: list[ObjectInfo] class PreDownloadProjectFile(BaseModel): class DownloadFileList(BaseModel): id: str - files: List[DownloadFileList] + files: list[DownloadFileList] zone: str operator: str container_code: str diff --git a/app/resources/authorization/decorator.py b/app/resources/authorization/decorator.py index d22942a..ea895b8 100644 --- a/app/resources/authorization/decorator.py +++ b/app/resources/authorization/decorator.py @@ -5,7 +5,6 @@ # You may not use this file except in compliance with the License. from functools import wraps -from typing import Dict import jwt @@ -29,7 +28,7 @@ def decorator(func): async def inner(*arg, **kwargs): api_response = APIResponse() - request = kwargs.get('request') + request = kwargs.get('request', kwargs.get('request_context')) project_code = kwargs.get('project_code', None) target_zone = kwargs.get('data').zone @@ -71,7 +70,7 @@ async def inner(*arg, **kwargs): return decorator -async def VM_info_enforcement(vm_info: Dict[str, str], incoming_ip: str, project_code: str) -> None: +async def VM_info_enforcement(vm_info: dict[str, str], incoming_ip: str, project_code: str) -> None: """ Summary: the function will check the following attribute in the signed diff --git a/app/resources/dependencies.py b/app/resources/dependencies.py index 5a9c10e..9a40f5e 100644 --- a/app/resources/dependencies.py +++ b/app/resources/dependencies.py @@ -137,7 +137,7 @@ def select_url_by_zone(zone): return url -async def transfer_to_pre(data, project_code, header): +async def transfer_to_pre(data, project_code, headers): try: logger.info('transfer_to_pre'.center(80, '-')) payload = { @@ -148,7 +148,6 @@ async def transfer_to_pre(data, project_code, header): 'data': data.data, 'job_type': data.job_type, } - headers = {'Session-ID': header.get('Session-ID'), 'authorization': header.get('authorization')} url = select_url_by_zone(data.zone) async with httpx.AsyncClient() as client: result = await client.post(url, headers=headers, json=payload, timeout=None) diff --git a/app/resources/error_handler.py b/app/resources/error_handler.py index 1526837..37e318b 100644 --- a/app/resources/error_handler.py +++ b/app/resources/error_handler.py @@ -12,7 +12,7 @@ def catch_internal(api_namespace): - """decorator to catch internal server error.""" + """Decorator to catch internal server error.""" def decorator(func): @wraps(func) @@ -55,7 +55,7 @@ class ECustomizedError(enum.Enum): def customized_error_template(customized_error: ECustomizedError): - """get error template.""" + """Get error template.""" return { 'INTERNAL': '[Internal] %s', 'FILE_NOT_FOUND': 'File Not Exist', diff --git a/app/resources/health_check.py b/app/resources/health_check.py index cbaeb1b..7c43d8c 100644 --- a/app/resources/health_check.py +++ b/app/resources/health_check.py @@ -4,7 +4,7 @@ # Version 3.0 (the "License") available at https://www.gnu.org/licenses/agpl-3.0.en.html. # You may not use this file except in compliance with the License. -from aioredis import StrictRedis +from redis.asyncio import Redis from app.logger import logger @@ -13,7 +13,7 @@ async def redis_check(): try: - res = await StrictRedis( + res = await Redis( host=ConfigClass.REDIS_HOST, port=ConfigClass.REDIS_PORT, db=ConfigClass.REDIS_DB, diff --git a/app/resources/helpers.py b/app/resources/helpers.py index 6b91af5..1555391 100644 --- a/app/resources/helpers.py +++ b/app/resources/helpers.py @@ -7,6 +7,7 @@ import httpx from common.project.project_client import ProjectClient +from app.components.request.http_client import HTTPClient from app.config import ConfigClass from app.logger import logger from app.models.file_models import ItemStatus @@ -65,14 +66,13 @@ async def batch_query_node_by_geid(geid_list): return located_geid, query_result -async def get_dataset(dataset_code): - """get dataset node information.""" +async def get_dataset(client: HTTPClient, dataset_code): + """Get dataset node information.""" logger.info('get_dataset'.center(80, '-')) try: url = ConfigClass.DATASET_SERVICE + f'/v1/datasets/{dataset_code}' logger.info(f'Getting dataset url: {url}') - async with httpx.AsyncClient() as client: - response = await client.get(url) + response = await client.get(url) logger.info(f'Getting dataset response: {response.text}') response.raise_for_status() result = response.json() @@ -140,9 +140,10 @@ async def query_file_folder(params, request): return response except Exception as e: logger.error(f'Error file/folder: {e}') + raise -async def get_dataset_versions(event): +async def get_dataset_versions(client: HTTPClient, event): logger.info('get_dataset_versions'.center(80, '-')) logger.info(f'Query event: {event}') try: @@ -155,8 +156,7 @@ async def get_dataset_versions(event): 'order': 'desc', 'sorting': 'created_at', } - async with httpx.AsyncClient() as client: - res = await client.get(url, params=params) + res = await client.get(url, params=params) res_json = res.json() dataset_versions = [] @@ -178,6 +178,7 @@ async def get_dataset_versions(event): return dataset_versions except Exception as e: logger.error(f'Error getting dataset version: {e}') + raise def separate_rel_path(folder_path): diff --git a/app/resources/validation_service.py b/app/resources/validation_service.py index 5f3fbf1..6737ef7 100644 --- a/app/resources/validation_service.py +++ b/app/resources/validation_service.py @@ -35,7 +35,7 @@ def decryption(encrypted_message, secret): backend=default_backend(), ) # use the key from current device information - key = base64.urlsafe_b64encode(kdf.derive('SECRETKEYPASSWORD'.encode())) + key = base64.urlsafe_b64encode(kdf.derive(b'SECRETKEYPASSWORD')) f = Fernet(key) decrypted = f.decrypt(base64.b64decode(encrypted_message)) return decrypted.decode() diff --git a/app/routers/v1/api_dataset.py b/app/routers/v1/api_dataset.py index 1e125e5..7670f68 100644 --- a/app/routers/v1/api_dataset.py +++ b/app/routers/v1/api_dataset.py @@ -4,8 +4,8 @@ # Version 3.0 (the "License") available at https://www.gnu.org/licenses/agpl-3.0.en.html. # You may not use this file except in compliance with the License. +from collections.abc import Mapping from typing import ClassVar -from typing import Mapping import fastapi import httpx @@ -132,7 +132,8 @@ async def modify_request_parameters(self, parameters: MultiDict[str]) -> MultiDi - If the creator parameter is specified it is set with the current username. - If the project_code parameter is specified it is part of projects to which the current user has access. - - If neither creator nor project_code parameters are specified filtering by projects where user has admin roles or where user is the creator. + - If neither creator nor project_code parameters are specified filtering by projects where user has admin roles + or where user is the creator. """ modified_parameters = MultiDict(parameters) @@ -183,6 +184,7 @@ async def process_response(self, response: httpx.Response) -> fastapi.Response: class GetDataset: current_identity: CurrentUser = Depends(jwt_required) project_service_client: ProjectServiceClient = Depends(get_project_service_client) + dataset_service_client: DatasetServiceClient = Depends(get_dataset_service_client) @router.get( '/dataset/{dataset_code}', @@ -208,7 +210,7 @@ async def get_dataset(self, dataset_code, page=0, page_size=10): api_response = DatasetDetailResponse() logger.info(f'User request with identity: {self.current_identity}') - dataset = await get_dataset(dataset_code) + dataset = await get_dataset(self.dataset_service_client.client, dataset_code) logger.info(f'Getting user dataset node: {dataset}') if not dataset: api_response.code = EAPIResponseCode.not_found @@ -224,7 +226,7 @@ async def get_dataset(self, dataset_code, page=0, page_size=10): dataset_query_event = {'dataset_geid': node_geid, 'page': page, 'page_size': page_size} logger.info(f'Dataset query: {dataset_query_event}') - versions = await get_dataset_versions(dataset_query_event) + versions = await get_dataset_versions(self.dataset_service_client.client, dataset_query_event) logger.info(f'Dataset versions: {versions}') dataset_detail = {'general_info': dataset, 'version_detail': versions, 'version_no': len(versions)} api_response.result = dataset_detail diff --git a/app/routers/v1/api_project.py b/app/routers/v1/api_project.py index 6f09ed0..216634a 100644 --- a/app/routers/v1/api_project.py +++ b/app/routers/v1/api_project.py @@ -11,6 +11,7 @@ from fastapi import Request from fastapi_utils.cbv import cbv +from app.components.request.context import RequestContextDependency from app.components.user.models import CurrentUser from app.config import ConfigClass from app.logger import logger @@ -75,7 +76,7 @@ async def list_project(self, page=0, page_size=10, order='created_at', order_by= async def project_file_preupload( self, project_code, - request: Request, + request_context: RequestContextDependency, data: POSTProjectFile, ): """PRE upload and check existence of file in project.""" @@ -106,7 +107,7 @@ async def project_file_preupload( try: logger.info('Tansfering to pre upload') - result = await transfer_to_pre(data, project_code, request.headers) + result = await transfer_to_pre(data, project_code, request_context.headers) logger.info(result.text) if result.status_code == 409: api_response.error_msg = result.json()['error_msg'] @@ -135,7 +136,7 @@ async def project_file_preupload( async def project_file_resumable( self, project_code, - request: Request, + request_context: RequestContextDependency, data: ResumableUploadPOST, ): """ @@ -172,11 +173,8 @@ async def project_file_resumable( logger.info('Tansfering to pre upload') async with httpx.AsyncClient() as client: url = ConfigClass.UPLOAD_SERVICE_GREENROOM + '/v1/files/resumable' - payload = await request.json() - headers = { - 'authorization': request.headers.get('authorization'), - } - res = await client.post(url, headers=headers, json=payload, timeout=None) + payload = await request_context.request.json() + res = await client.post(url, headers=request_context.headers, json=payload, timeout=None) api_response.result = res.json().get('result', []) return api_response.json_response() @@ -195,7 +193,7 @@ async def project_file_resumable( async def project_file_predownload( self, project_code, - request: Request, + request_context: RequestContextDependency, data: PreDownloadProjectFile, ): """PRE upload and check existence of file in project.""" @@ -224,10 +222,6 @@ async def project_file_predownload( else: url = ConfigClass.DOWNLOAD_SERVICE_CORE + '/v2/download/pre/' async with httpx.AsyncClient() as client: - headers = { - 'Session-ID': request.headers.get('Session-ID'), - 'authorization': request.headers.get('authorization'), - } payload = { 'files': [dict(x) for x in data.files], 'zone': data.zone, @@ -235,7 +229,7 @@ async def project_file_predownload( 'container_code': data.container_code, 'container_type': data.container_type, } - result = await client.post(url, headers=headers, json=payload) + result = await client.post(url, headers=request_context.headers, json=payload) if result.status_code != 200: raise Exception(result.json().get('error_msg')) diff --git a/app/services/dataset/client.py b/app/services/dataset/client.py index 46b4540..8e8aecc 100644 --- a/app/services/dataset/client.py +++ b/app/services/dataset/client.py @@ -4,22 +4,21 @@ # Version 3.0 (the "License") available at https://www.gnu.org/licenses/agpl-3.0.en.html. # You may not use this file except in compliance with the License. -from typing import Mapping +from collections.abc import Mapping -from fastapi import Depends -from httpx import AsyncClient from httpx import Response -from app.config import Settings -from app.config import get_settings +from app.components.request.context import RequestContextDependency +from app.components.request.http_client import HTTPClient +from app.config import SettingsDependency class DatasetServiceClient: """Client for dataset service.""" - def __init__(self, endpoint: str, timeout: int) -> None: + def __init__(self, endpoint: str, client: HTTPClient) -> None: self.endpoint_v1 = f'{endpoint}/v1' - self.client = AsyncClient(timeout=timeout) + self.client = client async def list_datasets(self, parameters: Mapping[str, str]) -> Response: """Get list of datasets.""" @@ -27,7 +26,9 @@ async def list_datasets(self, parameters: Mapping[str, str]) -> Response: return await self.client.get(f'{self.endpoint_v1}/datasets/', params=parameters) -def get_dataset_service_client(settings: Settings = Depends(get_settings)) -> DatasetServiceClient: +def get_dataset_service_client( + request_context: RequestContextDependency, settings: SettingsDependency +) -> DatasetServiceClient: """Get Dataset Service Client as a FastAPI dependency.""" - return DatasetServiceClient(settings.DATASET_SERVICE, settings.SERVICE_CLIENT_TIMEOUT) + return DatasetServiceClient(settings.DATASET_SERVICE.replace('/v1/', ''), request_context.client) diff --git a/poetry.lock b/poetry.lock index 46f1f12..4daba6d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "aioboto3" @@ -188,24 +188,6 @@ files = [ dev = ["attribution (==1.8.0)", "black (==24.8.0)", "build (>=1.2)", "coverage (==7.6.1)", "flake8 (==7.1.1)", "flit (==3.9.0)", "mypy (==1.11.2)", "ufmt (==2.7.1)", "usort (==1.0.8.post1)"] docs = ["sphinx (==8.0.2)", "sphinx-mdinclude (==0.6.2)"] -[[package]] -name = "aioredis" -version = "2.0.1" -description = "asyncio (PEP 3156) Redis support" -optional = false -python-versions = ">=3.6" -files = [ - {file = "aioredis-2.0.1-py3-none-any.whl", hash = "sha256:9ac0d0b3b485d293b8ca1987e6de8658d7dafcca1cddfcd1d506cae8cdebfdd6"}, - {file = "aioredis-2.0.1.tar.gz", hash = "sha256:eaa51aaf993f2d71f54b70527c440437ba65340588afeb786cd87c55c89cd98e"}, -] - -[package.dependencies] -async-timeout = "*" -typing-extensions = "*" - -[package.extras] -hiredis = ["hiredis (>=1.0)"] - [[package]] name = "aiosignal" version = "1.3.1" @@ -619,46 +601,62 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "39.0.0" +version = "44.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.6" -files = [ - {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288"}, - {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96"}, - {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717"}, - {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df"}, - {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1"}, - {file = "cryptography-39.0.0-cp36-abi3-win32.whl", hash = "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de"}, - {file = "cryptography-39.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190"}, - {file = "cryptography-39.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8"}, - {file = "cryptography-39.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39"}, - {file = "cryptography-39.0.0.tar.gz", hash = "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf"}, +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +files = [ + {file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"}, + {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"}, + {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"}, + {file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"}, + {file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"}, + {file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"}, + {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"}, + {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"}, + {file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"}, + {file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"}, + {file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"}, ] [package.dependencies] -cffi = ">=1.12" +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "ruff"] -sdist = ["setuptools-rust (>=0.11.4)"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] [[package]] name = "deprecated" @@ -741,24 +739,23 @@ python-dateutil = ">=2.4" [[package]] name = "fastapi" -version = "0.90.1" +version = "0.115.11" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "fastapi-0.90.1-py3-none-any.whl", hash = "sha256:d4e4bd820204eeaa19879bf129bbce73db09bdc209d5d83a064b40b2b00557b0"}, - {file = "fastapi-0.90.1.tar.gz", hash = "sha256:d17e85deb3a350b731467e7bf035e158faffa381310a1b7356421e916fcc64f2"}, + {file = "fastapi-0.115.11-py3-none-any.whl", hash = "sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64"}, + {file = "fastapi-0.115.11.tar.gz", hash = "sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f"}, ] [package.dependencies] -pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = ">=0.22.0,<0.24.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.47.0" +typing-extensions = ">=4.8.0" [package.extras] -all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.8.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.10.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.6.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "fastapi-health" @@ -1036,55 +1033,56 @@ protobuf = ["grpcio-tools (>=1.66.1)"] [[package]] name = "gunicorn" -version = "20.0.4" +version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false -python-versions = ">=3.4" +python-versions = ">=3.7" files = [ - {file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"}, - {file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"}, + {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, + {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, ] [package.dependencies] -setuptools = ">=3.0" +packaging = "*" [package.extras] -eventlet = ["eventlet (>=0.9.7)"] -gevent = ["gevent (>=0.13)"] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] +gevent = ["gevent (>=1.4.0)"] setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] tornado = ["tornado (>=0.2)"] [[package]] name = "h11" -version = "0.12.0" +version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, - {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] [[package]] name = "httpcore" -version = "0.15.0" +version = "1.0.9" description = "A minimal low-level HTTP client." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"}, - {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"}, + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, ] [package.dependencies] -anyio = "==3.*" certifi = "*" -h11 = ">=0.11,<0.13" -sniffio = "==1.*" +h11 = ">=0.16" [package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httptools" @@ -1136,26 +1134,28 @@ test = ["Cython (>=0.29.24,<0.30.0)"] [[package]] name = "httpx" -version = "0.23.0" +version = "0.27.2" description = "The next generation HTTP client." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, - {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, ] [package.dependencies] +anyio = "*" certifi = "*" -httpcore = ">=0.15.0,<0.16.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +httpcore = "==1.*" +idna = "*" sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "idna" @@ -1520,26 +1520,25 @@ files = [ [[package]] name = "pilot-platform-common" -version = "0.3.0" -description = "Generates entity ID and connects with Vault (secret engine) to retrieve credentials" +version = "0.8.0" +description = "Centralize cross-service functionality shared across all Pilot Platform services." optional = false -python-versions = ">=3.6" +python-versions = "<3.11,>=3.10" files = [ - {file = "pilot-platform-common-0.3.0.tar.gz", hash = "sha256:407ad6da7ee93616d70e1591f242fd5f703e84a7b2d873a2b2ffda900794935e"}, - {file = "pilot_platform_common-0.3.0-py3-none-any.whl", hash = "sha256:cea3e59dacc6fb9236ac764daba5233b3ecb6956655a31b2422ae6bc896f20a2"}, + {file = "pilot_platform_common-0.8.0-py3-none-any.whl", hash = "sha256:851a7bb2bc88c11b257d5d3f3620d278b7d8dcf2aaef6fcc8af15eaac5e522ad"}, + {file = "pilot_platform_common-0.8.0.tar.gz", hash = "sha256:0639b6a01dd38d7b83c7978d1f420ea92048e9773a19469bacf346cc3d8a991b"}, ] [package.dependencies] -aioboto3 = "9.6.0" -aioredis = ">=2.0.0,<3.0.0" -cryptography = "39.0.0" -httpx = "0.23.0" -minio = "7.1.8" -pyjwt = "2.6.0" -python-dotenv = "0.19.1" -python-json-logger = "2.0.2" -starlette = ">=0.19.1,<0.24.0" -xmltodict = "0.13.0" +aioboto3 = ">=9.6.0,<=13.2.0" +httpx = ">=0.23.0,<=0.27.2" +minio = ">=7.1.8,<8.0.0" +pyjwt = ">=2.6.0,<=2.9.0" +python-dotenv = ">=0.19.1" +python-json-logger = ">=0.1.11,<=2.0.7" +redis = ">=4.5.0,<=5.2.0" +starlette = "<0.45.0" +xmltodict = ">=0.13.0,<=0.14.2" [[package]] name = "pluggy" @@ -1763,21 +1762,21 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-httpx" -version = "0.21.3" +version = "0.30.0" description = "Send responses to httpx." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "pytest_httpx-0.21.3-py3-none-any.whl", hash = "sha256:50b52b910f6f6cfb0aa65039d6f5bedb6ae3a0c02a98c4a7187543fe437c428a"}, - {file = "pytest_httpx-0.21.3.tar.gz", hash = "sha256:edcb62baceffbd57753c1a7afc4656b0e71e91c7a512e143c0adbac762d979c1"}, + {file = "pytest-httpx-0.30.0.tar.gz", hash = "sha256:755b8edca87c974dd4f3605c374fda11db84631de3d163b99c0df5807023a19a"}, + {file = "pytest_httpx-0.30.0-py3-none-any.whl", hash = "sha256:6d47849691faf11d2532565d0c8e0e02b9f4ee730da31687feae315581d7520c"}, ] [package.dependencies] -httpx = "==0.23.*" -pytest = ">=6.0,<8.0" +httpx = "==0.27.*" +pytest = ">=7,<9" [package.extras] -testing = ["pytest-asyncio (==0.20.*)", "pytest-cov (==4.*)"] +testing = ["pytest-asyncio (==0.23.*)", "pytest-cov (==4.*)"] [[package]] name = "pytest-mock" @@ -1885,17 +1884,21 @@ files = [ [[package]] name = "redis" -version = "3.5.3" -description = "Python client for Redis key-value store" +version = "4.5.5" +description = "Python client for Redis database and key-value store" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" files = [ - {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, - {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, + {file = "redis-4.5.5-py3-none-any.whl", hash = "sha256:77929bc7f5dab9adf3acba2d3bb7d7658f1e0c2f1cafe7eb36434e751c471119"}, + {file = "redis-4.5.5.tar.gz", hash = "sha256:dc87a0bdef6c8bfe1ef1e1c40be7034390c2ae02d92dcd0c7ca1729443899880"}, ] +[package.dependencies] +async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""} + [package.extras] -hiredis = ["hiredis (>=0.1.3)"] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] [[package]] name = "requests" @@ -1918,23 +1921,6 @@ urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" security = ["cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -optional = false -python-versions = "*" -files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] - [[package]] name = "s3transfer" version = "0.5.2" @@ -2094,20 +2080,20 @@ url = ["furl (>=0.4.1)"] [[package]] name = "starlette" -version = "0.23.1" +version = "0.44.0" description = "The little ASGI library that shines." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "starlette-0.23.1-py3-none-any.whl", hash = "sha256:ec69736c90be8dbfc6ec6800ba6feb79c8c44f9b1706c0b2bb27f936bcf362cc"}, - {file = "starlette-0.23.1.tar.gz", hash = "sha256:8510e5b3d670326326c5c1d4cb657cc66832193fe5d5b7015a51c7b1e1b1bf42"}, + {file = "starlette-0.44.0-py3-none-any.whl", hash = "sha256:19edeb75844c16dcd4f9dd72f22f9108c1539f3fc9c4c88885654fef64f85aea"}, + {file = "starlette-0.44.0.tar.gz", hash = "sha256:e35166950a3ccccc701962fe0711db0bc14f2ecd37c6f9fe5e3eae0cbaea8715"}, ] [package.dependencies] anyio = ">=3.4.0,<5" [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] [[package]] name = "testcontainers" @@ -2468,5 +2454,5 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" -python-versions = "^3.10" -content-hash = "9f058257428a23ced9ccba85bc4e5e506695a7ca642020b146692a8be730a2d7" +python-versions = "~3.10" +content-hash = "3ffdc1481b9eac23abca7577fcbeba64d8b7fc473779705ab41ac5d1b9fbb2a6" diff --git a/pyproject.toml b/pyproject.toml index c09e254..2803b7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [tool.poetry] name = "bff_cli" -version = "2.2.10" +version = "2.2.15" description = "This service is designed to support command line interface" authors = ["Indoc Research"] [tool.poetry.dependencies] -python = "^3.10" +python = ">=3.10,<3.11" fastapi-utils = "^0.7" uvicorn = "0.12.3" -gunicorn = "20.0.4" +gunicorn = "23.0.0" uvloop = "0.20.0" httptools = "^0.6" SQLAlchemy = "1.4.31" @@ -21,24 +21,25 @@ opentelemetry-instrumentation-sqlalchemy = "0.26b1" opentelemetry-instrumentation-httpx = "0.26b1" opentelemetry-instrumentation-asyncpg = "0.26b1" fastapi-health = "^0.4.0" -fastapi = "^0.90" -pilot-platform-common = "0.3.0" +fastapi = "0.115.11" +pilot-platform-common = "^0.8.0" +httpx = "0.27.2" +redis = "4.5.5" +cryptography = "44.0.3" [tool.poetry.dev-dependencies] pytest = "7.1.2" pytest-cov = "^3.0.0" pytest-asyncio = "0.17.2" -pytest-httpx="^0.21.0" +pytest-httpx = "0.30.0" pytest-random-order = "1.1.1" pytest-mock = "3.7.0" faker = "28.4.1" sqlalchemy-utils = "0.38.2" uvicorn = "0.12.3" -gunicorn = "20.0.4" requests = "2.24.0" python-multipart = "0.0.5" aiofiles = "0.6.0" -redis = "3.5.3" async-asgi-testclient = "1.4.9" testcontainers = "3.4.2" diff --git a/tests/app/components/request/__init__.py b/tests/app/components/request/__init__.py new file mode 100644 index 0000000..105ee19 --- /dev/null +++ b/tests/app/components/request/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2022-Present Indoc Systems +# +# Licensed under the GNU AFFERO GENERAL PUBLIC LICENSE, +# Version 3.0 (the "License") available at https://www.gnu.org/licenses/agpl-3.0.en.html. +# You may not use this file except in compliance with the License. diff --git a/tests/app/components/request/test_http_client.py b/tests/app/components/request/test_http_client.py new file mode 100644 index 0000000..3f5b27f --- /dev/null +++ b/tests/app/components/request/test_http_client.py @@ -0,0 +1,27 @@ +# Copyright (C) 2022-Present Indoc Systems +# +# Licensed under the GNU AFFERO GENERAL PUBLIC LICENSE, +# Version 3.0 (the "License") available at https://www.gnu.org/licenses/agpl-3.0.en.html. +# You may not use this file except in compliance with the License. + +import pytest + +from app.components.request.http_client import HTTPClient + + +class TestHTTPClient: + + @pytest.mark.parametrize('method', ['get', 'post', 'put', 'patch', 'delete']) + async def test_method_merges_headers_from_constructor_and_arguments(self, method, fake, httpx_mock): + headers_1 = fake.headers(3) + headers_2 = fake.headers(3) + url = fake.url() + http_client = HTTPClient(headers=headers_1, timeout=5) + httpx_mock.add_response(method=method, url=url) + + function = getattr(http_client, method) + await function(url, headers=headers_2) + + requests = httpx_mock.get_requests() + assert len(requests) == 1 + assert (headers_1 | headers_2).items() <= requests[0].headers.items() diff --git a/tests/conftest.py b/tests/conftest.py index 96e17ae..a128c93 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -179,4 +179,5 @@ def settings() -> Settings: 'tests.fixtures.services.dataset', 'tests.fixtures.services.project', 'tests.fixtures.fake', + 'tests.fixtures.request_context', ] diff --git a/tests/fixtures/fake.py b/tests/fixtures/fake.py index 7416338..181a997 100644 --- a/tests/fixtures/fake.py +++ b/tests/fixtures/fake.py @@ -24,7 +24,11 @@ def project_id(self) -> str: def dataset_id(self) -> str: return self.uuid4() + def headers(self, elements: int) -> dict[str, str]: + headers = self.pydict(elements, value_types=(str,)) + return {k.lower(): v for k, v in headers.items()} + @pytest.fixture(scope='session') def fake() -> Faker: - yield Faker() + return Faker() diff --git a/tests/fixtures/request_context.py b/tests/fixtures/request_context.py new file mode 100644 index 0000000..00a0d58 --- /dev/null +++ b/tests/fixtures/request_context.py @@ -0,0 +1,18 @@ +# Copyright (C) 2022-Present Indoc Systems +# +# Licensed under the GNU AFFERO GENERAL PUBLIC LICENSE, +# Version 3.0 (the "License") available at https://www.gnu.org/licenses/agpl-3.0.en.html. +# You may not use this file except in compliance with the License. + +import pytest +from fastapi.datastructures import Headers +from fastapi.requests import Request + +from app.components.request.context import RequestContext +from app.components.request.context import get_request_context + + +@pytest.fixture +def request_context(settings) -> RequestContext: + request = Request(scope={'type': 'http', 'headers': Headers().raw}) + return get_request_context(request, settings) diff --git a/tests/fixtures/services/dataset.py b/tests/fixtures/services/dataset.py index 02f3421..8a9240f 100644 --- a/tests/fixtures/services/dataset.py +++ b/tests/fixtures/services/dataset.py @@ -66,5 +66,5 @@ def dataset_factory(fake, httpx_mock, settings) -> DatasetFactory: @pytest.fixture -def dataset_service_client(settings) -> DatasetServiceClient: - return get_dataset_service_client(settings) +def dataset_service_client(request_context, settings) -> DatasetServiceClient: + return get_dataset_service_client(request_context, settings) diff --git a/tests/routers/v1/test_api_dataset.py b/tests/routers/v1/test_api_dataset.py index f701cb9..5781306 100644 --- a/tests/routers/v1/test_api_dataset.py +++ b/tests/routers/v1/test_api_dataset.py @@ -4,7 +4,7 @@ # Version 3.0 (the "License") available at https://www.gnu.org/licenses/agpl-3.0.en.html. # You may not use this file except in compliance with the License. -from typing import Callable +from collections.abc import Callable import pytest from pytest_httpx import HTTPXMock diff --git a/tests/routers/v1/test_api_file.py b/tests/routers/v1/test_api_file.py index 5846c36..e52b06e 100644 --- a/tests/routers/v1/test_api_file.py +++ b/tests/routers/v1/test_api_file.py @@ -32,7 +32,6 @@ async def test_get_name_folders_in_project_should_return_200(test_async_client_a 'http://metadata_service/v1/items/search/' '?container_code=test_project' '&container_type=project' - '&parent_path=' '&recursive=false' '&zone=0' '&status=ACTIVE' From 820fb9f25cbb80de213d0dd8ecf1ea647dd956f7 Mon Sep 17 00:00:00 2001 From: Vadym Moshynskyi Date: Wed, 26 Nov 2025 14:01:08 +0100 Subject: [PATCH 2/2] IEBH-353: Fix versions --- README.md | 2 +- app/config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3c1c360..be07128 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # BFF-CLI -[![Python](https://img.shields.io/badge/python-3.8-brightgreen.svg)](https://www.python.org/) +[![Python](https://img.shields.io/badge/python-3.10-brightgreen.svg)](https://www.python.org/) ## Getting Started The backend for the command line interface. diff --git a/app/config.py b/app/config.py index e213695..537f661 100644 --- a/app/config.py +++ b/app/config.py @@ -14,7 +14,7 @@ class Settings(BaseSettings): - version = '2.2.14' + version = '2.2.15' port: int = 5080 host: str = '0.0.0.0'