diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index df544c58..ce923056 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -7,22 +7,21 @@ name: Docker on: schedule: - - cron: '39 11 * * *' + - cron: "39 11 * * *" push: - branches: [ "main"] + branches: [ "main" ] # Publish semver tags as releases. - tags: [ 'v*.*.*' ] + tags: [ "v*.*.*" ] pull_request: branches: [ "main" ] workflow_dispatch: - + env: # Use docker.io for Docker Hub if empty REGISTRY: ghcr.io # github.repository as / IMAGE_NAME: ${{ github.repository }} - jobs: build: @@ -39,23 +38,23 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - fetch-tags: 'true' + fetch-tags: "true" persist-credentials: false - name: Install cosign if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 # Set up BuildKit Docker container builder to be able to build # multi-platform images and export cache # https://github.com/docker/setup-buildx-action - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -65,7 +64,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -73,7 +72,7 @@ jobs: # https://github.com/docker/build-push-action - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . push: ${{ github.event_name != 'pull_request' }} diff --git a/pyproject.toml b/pyproject.toml index acb18b24..cbde1d30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,8 +60,7 @@ target_version = ['py312'] include = '\.pyi?$' [tool.pytest.ini_options] -asyncio_default_fixture_loop_scope = "function" -asyncio_mode = "auto" +anyio_mode = "auto" [tool.ruff] target-version = "py312" diff --git a/requirements-dev.in b/requirements-dev.in index 15483fe5..4cebe044 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -13,7 +13,6 @@ locust pre-commit-uv pyright pytest -pytest-asyncio ruff textual-dev uv diff --git a/requirements-dev.txt b/requirements-dev.txt index 2cdd37dd..769e0376 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ # uv pip compile requirements-dev.in -o requirements-dev.txt aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.3 +aiohttp==3.13.5 # via # aiohttp-jinja2 # textual-dev @@ -11,9 +11,11 @@ aiohttp-jinja2==1.6 # via textual-serve aiosignal==1.4.0 # via aiohttp +annotated-doc==0.0.4 + # via typer annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.13.0 # via # asyncer # httpx @@ -22,15 +24,15 @@ asgi-lifespan==2.1.0 # via -r requirements-dev.in asttokens==3.0.1 # via stack-data -asyncer==0.0.11 +asyncer==0.0.17 # via -r requirements-dev.in -attrs==25.4.0 +attrs==26.1.0 # via aiohttp -backports-zstd==1.2.0 +backports-zstd==1.4.0 # via hatch bidict==0.23.1 # via python-socketio -black==25.12.0 +black==26.3.1 # via -r requirements-dev.in blinker==1.9.0 # via flask @@ -38,7 +40,7 @@ brotli==1.2.0 # via geventhttpclient bunnet==1.3.0 # via -r requirements-dev.in -certifi==2025.11.12 +certifi==2026.4.22 # via # geventhttpclient # httpcore @@ -48,9 +50,9 @@ cffi==2.0.0 # via cryptography cfgv==3.5.0 # via pre-commit -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -click==8.3.1 +click==8.3.3 # via # black # bunnet @@ -61,13 +63,11 @@ click==8.3.1 # typer # userpath # uvicorn -configargparse==1.7.1 - # via - # locust - # locust-cloud -coverage==7.13.0 +configargparse==1.7.5 + # via locust +coverage==7.13.5 # via -r requirements-dev.in -cryptography==46.0.3 +cryptography==48.0.0 # via secretstorage decorator==5.2.1 # via ipython @@ -77,16 +77,18 @@ dnspython==2.8.0 # via pymongo executing==2.2.1 # via stack-data -fastapi-cli==0.0.16 +fastapi-cli==0.0.24 # via -r requirements-dev.in -filelock==3.20.3 - # via virtualenv -flask==3.1.2 +filelock==3.29.0 + # via + # python-discovery + # virtualenv +flask==3.1.3 # via # flask-cors # flask-login # locust -flask-cors==6.0.1 +flask-cors==6.0.2 # via locust flask-login==0.6.3 # via locust @@ -98,23 +100,22 @@ gevent==25.9.1 # via # geventhttpclient # locust - # locust-cloud -geventhttpclient==2.3.7 +geventhttpclient==2.3.9 # via locust -greenlet==3.3.0 +greenlet==3.5.0 # via gevent h11==0.16.0 # via # httpcore # uvicorn # wsproto -hatch==1.16.2 +hatch==1.16.5 # via -r requirements-dev.in hatch-requirements-txt==0.4.1 # via -r requirements-dev.in hatch-vcs==0.5.0 # via -r requirements-dev.in -hatchling==1.28.0 +hatchling==1.29.0 # via # -r requirements-dev.in # hatch @@ -128,9 +129,9 @@ httpx==0.28.1 # via hatch hyperlink==21.0.0 # via hatch -identify==2.6.15 +identify==2.6.19 # via pre-commit -idna==3.11 +idna==3.13 # via # anyio # httpx @@ -139,7 +140,7 @@ idna==3.11 # yarl iniconfig==2.3.0 # via pytest -ipython==9.8.0 +ipython==9.13.0 # via -r requirements-dev.in ipython-pygments-lexers==1.1.1 # via ipython @@ -147,11 +148,11 @@ itsdangerous==2.2.0 # via flask jaraco-classes==3.4.0 # via keyring -jaraco-context==6.0.1 +jaraco-context==6.1.2 # via keyring -jaraco-functools==4.3.0 +jaraco-functools==4.4.0 # via keyring -jedi==0.19.2 +jedi==0.20.0 # via ipython jeepney==0.9.0 # via @@ -166,13 +167,11 @@ keyring==25.7.0 # via hatch lazy-model==0.2.0 # via bunnet -linkify-it-py==2.0.3 +linkify-it-py==2.1.0 # via markdown-it-py -locust==2.42.6 +locust==2.43.4 # via -r requirements-dev.in -locust-cloud==1.29.5 - # via locust -markdown-it-py[linkify]==4.0.0 +markdown-it-py==4.1.0 # via # mdit-py-plugins # rich @@ -188,7 +187,7 @@ mdit-py-plugins==0.5.0 # via textual mdurl==0.1.2 # via markdown-it-py -more-itertools==10.8.0 +more-itertools==11.0.2 # via # jaraco-classes # jaraco-functools @@ -196,17 +195,17 @@ msgpack==1.1.2 # via # locust # textual-dev -multidict==6.7.0 +multidict==6.7.1 # via # aiohttp # yarl mypy-extensions==1.1.0 # via black -nodeenv==1.9.1 +nodeenv==1.10.0 # via # pre-commit # pyright -packaging==25.0 +packaging==26.2 # via # black # hatch @@ -214,9 +213,10 @@ packaging==25.0 # hatchling # pytest # setuptools-scm -parso==0.8.5 + # vcs-versioning +parso==0.8.7 # via jedi -pathspec==0.12.1 +pathspec==1.1.1 # via # black # hatchling @@ -224,20 +224,20 @@ pexpect==4.9.0 # via # hatch # ipython -platformdirs==4.5.1 +platformdirs==4.9.6 # via # black # hatch - # locust-cloud + # python-discovery # textual # virtualenv pluggy==1.6.0 # via # hatchling # pytest -pre-commit==4.5.0 +pre-commit==4.6.0 # via pre-commit-uv -pre-commit-uv==4.2.0 +pre-commit-uv==4.2.1 # via -r requirements-dev.in prompt-toolkit==3.0.52 # via ipython @@ -245,52 +245,52 @@ propcache==0.4.1 # via # aiohttp # yarl -psutil==7.1.3 - # via locust +psutil==7.2.2 + # via + # ipython + # locust ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 # via stack-data -pycparser==2.23 +pycparser==3.0 # via cffi -pydantic==2.12.5 +pydantic==2.13.4 # via # bunnet # lazy-model -pydantic-core==2.41.5 +pydantic-core==2.46.4 # via pydantic -pygments==2.19.2 +pygments==2.20.0 # via # ipython # ipython-pygments-lexers # pytest # rich # textual -pymongo==4.15.5 +pymongo==4.17.0 # via bunnet pyproject-hooks==1.2.0 # via hatch -pyright==1.1.407 +pyright==1.1.409 # via -r requirements-dev.in -pytest==9.0.2 +pytest==9.0.3 # via # -r requirements-dev.in # locust - # pytest-asyncio -pytest-asyncio==1.3.0 - # via -r requirements-dev.in -python-dotenv==1.2.1 +python-discovery==1.3.0 + # via + # hatch + # virtualenv +python-dotenv==1.2.2 # via uvicorn -python-engineio==4.12.3 +python-engineio==4.13.1 # via # locust - # locust-cloud # python-socketio -python-socketio[client]==5.15.0 - # via - # locust - # locust-cloud -pytokens==0.3.0 +python-socketio==5.16.1 + # via locust +pytokens==0.4.1 # via black pyyaml==6.0.3 # via @@ -298,24 +298,26 @@ pyyaml==6.0.3 # uvicorn pyzmq==27.1.0 # via locust -requests==2.32.4 +requests==2.33.1 # via # locust # python-socketio -rich==14.2.0 +rich==15.0.0 # via # hatch # rich-toolkit # textual # textual-serve # typer -rich-toolkit==0.17.0 +rich-toolkit==0.19.7 # via fastapi-cli -ruff==0.14.8 +ruff==0.15.12 # via -r requirements-dev.in secretstorage==3.5.0 # via keyring -setuptools-scm==9.2.2 +setuptools==82.0.1 + # via setuptools-scm +setuptools-scm==10.0.5 # via hatch-vcs shellingham==1.5.4 # via @@ -329,7 +331,7 @@ sniffio==1.3.1 # asyncer stack-data==0.6.3 # via ipython -textual==6.8.0 +textual==8.2.5 # via # textual-dev # textual-serve @@ -341,32 +343,31 @@ toml==0.10.2 # via bunnet tomli-w==1.2.0 # via hatch -tomlkit==0.13.3 +tomlkit==0.14.0 # via hatch -traitlets==5.14.3 +traitlets==5.15.0 # via # ipython # matplotlib-inline -trove-classifiers==2025.12.1.14 +trove-classifiers==2026.4.28.13 # via hatchling -typer==0.20.0 +typer==0.25.1 # via fastapi-cli typing-extensions==4.15.0 # via # aiosignal # anyio + # asyncer # pydantic # pydantic-core # pyright - # pytest-asyncio # rich-toolkit # textual # textual-dev - # typer # typing-inspection typing-inspection==0.4.2 # via pydantic -uc-micro-py==1.0.3 +uc-micro-py==2.0.0 # via linkify-it-py urllib3==2.6.3 # via @@ -374,28 +375,30 @@ urllib3==2.6.3 # requests userpath==1.9.2 # via hatch -uv==0.9.16 +uv==0.11.11 # via # -r requirements-dev.in # hatch # pre-commit-uv -uvicorn[standard]==0.38.0 +uvicorn==0.46.0 # via fastapi-cli uvloop==0.22.1 # via uvicorn -virtualenv==20.36.1 +vcs-versioning==1.1.1 + # via setuptools-scm +virtualenv==21.3.1 # via # hatch # pre-commit watchfiles==1.1.1 # via uvicorn -wcwidth==0.2.14 +wcwidth==0.7.0 # via prompt-toolkit websocket-client==1.9.0 # via python-socketio -websockets==15.0.1 +websockets==16.0 # via uvicorn -werkzeug==3.1.5 +werkzeug==3.1.8 # via # flask # flask-cors @@ -403,9 +406,9 @@ werkzeug==3.1.5 # locust wsproto==1.3.2 # via simple-websocket -yarl==1.22.0 +yarl==1.23.0 # via aiohttp -zope-event==6.1 +zope-event==6.2 # via gevent -zope-interface==8.1.1 +zope-interface==8.4 # via gevent diff --git a/requirements.txt b/requirements.txt index 558ea077..600e2302 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,10 +3,12 @@ aiofiles==25.1.0 # via -r requirements.in annotated-doc==0.0.4 - # via fastapi + # via + # fastapi + # typer annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.13.0 # via # httpx # starlette @@ -16,15 +18,15 @@ argon2-cffi-bindings==25.1.0 # via argon2-cffi asgi-correlation-id==4.3.4 # via -r requirements.in -beanie==2.0.1 +beanie==2.1.0 # via -r requirements.in -certifi==2025.11.12 +certifi==2026.4.22 # via # httpcore # httpx cffi==2.0.0 # via argon2-cffi-bindings -click==8.3.1 +click==8.3.3 # via # beanie # typer @@ -33,21 +35,21 @@ decorator==5.2.1 # via gssapi dnspython==2.8.0 # via pymongo -dunamai==1.25.0 +dunamai==1.26.1 # via uv-dynamic-versioning -faker==38.2.0 +faker==40.15.0 # via -r requirements.in -fastapi==0.124.0 +fastapi==0.136.1 # via -r requirements.in -gssapi==1.10.1 +gssapi==1.11.1 # via n2snusertools -gunicorn==23.0.0 +gunicorn==26.0.0 # via -r requirements.in h11==0.16.0 # via # httpcore # uvicorn -hatchling==1.28.0 +hatchling==1.29.0 # via uv-dynamic-versioning httpcore==1.0.9 # via @@ -57,13 +59,13 @@ httpx==0.28.1 # via # -r requirements.in # httpx-socks -httpx-socks[asyncio]==0.11.0 +httpx-socks==0.11.0 # via -r requirements.in -idna==3.11 +idna==3.13 # via # anyio # httpx -jinja-partials==0.3.0 +jinja-partials==0.3.1 # via -r requirements.in jinja2==3.1.6 # via @@ -74,9 +76,9 @@ lazy-model==0.4.0 # via beanie ldap3==2.9.1 # via n2snusertools -linkify-it-py==2.0.3 +linkify-it-py==2.1.0 # via markdown-it-py -markdown-it-py[linkify]==4.0.0 +markdown-it-py==4.1.0 # via # mdit-py-plugins # rich @@ -91,7 +93,7 @@ mdurl==0.1.2 # via markdown-it-py n2snusertools==0.3.10 # via -r requirements.in -packaging==25.0 +packaging==26.2 # via # asgi-correlation-id # dunamai @@ -99,76 +101,76 @@ packaging==25.0 # hatchling passlib==1.7.4 # via -r requirements.in -pathspec==0.12.1 +pathspec==1.1.1 # via hatchling -platformdirs==4.5.1 +platformdirs==4.9.6 # via textual pluggy==1.6.0 # via hatchling prettytable==3.17.0 # via n2snusertools -prometheus-client==0.23.1 +prometheus-client==0.25.0 # via prometheus-fastapi-instrumentator prometheus-fastapi-instrumentator==7.1.0 # via -r requirements.in -pyasn1==0.6.2 +pyasn1==0.6.3 # via ldap3 -pycparser==2.23 +pycparser==3.0 # via cffi -pydantic==2.12.5 +pydantic==2.13.4 # via # -r requirements.in # beanie # fastapi # lazy-model # pydantic-settings -pydantic-core==2.41.5 +pydantic-core==2.46.4 # via pydantic -pydantic-settings==2.12.0 +pydantic-settings==2.14.0 # via -r requirements.in -pygments==2.19.2 +pygments==2.20.0 # via # rich # textual -pymongo==4.15.5 +pymongo==4.17.0 # via # -r requirements.in # beanie -python-dotenv==1.2.1 +python-dotenv==1.2.2 # via pydantic-settings -python-multipart==0.0.20 +python-multipart==0.0.27 # via -r requirements.in -python-socks==2.8.0 +python-socks==2.8.1 # via httpx-socks pyyaml==6.0.3 # via n2snusertools -rich==14.2.0 +rich==15.0.0 # via # -r requirements.in # textual # typer shellingham==1.5.4 # via typer -slack-bolt==1.27.0 +slack-bolt==1.28.0 # via -r requirements.in -slack-sdk==3.39.0 +slack-sdk==3.41.0 # via # -r requirements.in # slack-bolt sniffio==1.3.1 # via httpx-socks -starlette==0.50.0 +starlette==0.52.1 # via # asgi-correlation-id # fastapi # prometheus-fastapi-instrumentator -textual==6.8.0 +textual==8.2.5 # via -r requirements.in -tomlkit==0.13.3 +tomlkit==0.14.0 # via uv-dynamic-versioning -trove-classifiers==2025.12.1.14 +trove-classifiers==2026.4.28.13 # via hatchling -typer==0.20.0 +typer==0.25.1 # via -r requirements.in typing-extensions==4.15.0 # via @@ -179,23 +181,21 @@ typing-extensions==4.15.0 # pydantic-core # starlette # textual - # typer # typing-inspection typing-inspection==0.4.2 # via + # fastapi # pydantic # pydantic-settings -tzdata==2025.2 - # via faker -uc-micro-py==1.0.3 +uc-micro-py==2.0.0 # via linkify-it-py uuid==1.30 # via -r requirements.in -uv-dynamic-versioning==0.11.2 +uv-dynamic-versioning==0.14.0 # via -r requirements.in -uvicorn==0.38.0 +uvicorn==0.46.0 # via -r requirements.in -wcwidth==0.2.14 +wcwidth==0.7.0 # via prettytable -werkzeug==3.1.5 +werkzeug==3.1.8 # via -r requirements.in diff --git a/scripts/create_new_beamline.py b/scripts/create_new_beamline.py index db416eab..ac00180e 100644 --- a/scripts/create_new_beamline.py +++ b/scripts/create_new_beamline.py @@ -13,7 +13,7 @@ async def main(): # Initialize Beanie - await mongodb_setup.init_connection(settings.mongodb_dsn) + client = await mongodb_setup.init_connection(settings.mongodb_dsn) pass_resources = await pass_service.get_pass_resources() pass_ids = [r["ID"] for r in pass_resources if r["Code"] == BEAMLINE_NAME] @@ -48,6 +48,8 @@ async def main(): # Uncomment this line to actually insert the new beamline into the database # await new_beamline.insert() + await client.close() + if __name__ == "__main__": asyncio.run(main()) diff --git a/scripts/update_beamline.py b/scripts/update_beamline.py index 66c69503..623fa977 100644 --- a/scripts/update_beamline.py +++ b/scripts/update_beamline.py @@ -14,7 +14,7 @@ async def main(): # Initialize Beanie - await mongodb_setup.init_connection(settings.mongodb_dsn) + client = await mongodb_setup.init_connection(settings.mongodb_dsn) pass_resources = await pass_service.get_pass_resources() pass_ids = [r["ID"] for r in pass_resources if r["Code"] == BEAMLINE_NAME] @@ -27,7 +27,7 @@ async def main(): beamline = await Beamline.find_one(Beamline.pass_id == str(pass_ids[0])) if not beamline: - raise KeyError(f"No beamline found with pass_id {pass_ids[0]}") + raise KeyError(f"No beamline found with pass_id {pass_ids[0]}") print("Current beamline:") print(beamline) @@ -40,7 +40,7 @@ async def main(): workflow="workflow-tla", bluesky="bluesky-tla", operator="xf99id", - lsdc=None + lsdc=None, ) # INCLUDE ADDITIONAL CHANGES TO THE BEAMLINE OBJECT HERE AS NEEDED @@ -56,5 +56,8 @@ async def main(): # Uncomment the line below to actually save the changes to the database # await beamline.save() + await client.close() + + if __name__ == "__main__": asyncio.run(main()) diff --git a/src/nsls2api/infrastructure/app_setup.py b/src/nsls2api/infrastructure/app_setup.py index b29b73ee..9b5b6c6d 100644 --- a/src/nsls2api/infrastructure/app_setup.py +++ b/src/nsls2api/infrastructure/app_setup.py @@ -21,7 +21,7 @@ async def app_lifespan(app: FastAPI): logger.info(f"NSLS-II API Version: {get_version()}") # Initialize the MongoDB connection - await mongodb_setup.init_connection(settings.mongodb_dsn) + mongodb_client = await mongodb_setup.init_connection(settings.mongodb_dsn) # Create a shared httpx client httpx_client_wrapper.start() @@ -35,3 +35,6 @@ async def app_lifespan(app: FastAPI): # Cleanup httpx client await httpx_client_wrapper.stop() + + # Close MongoDB client + await mongodb_client.close() diff --git a/src/nsls2api/infrastructure/mongodb_setup.py b/src/nsls2api/infrastructure/mongodb_setup.py index 6a079e71..a2cabd31 100644 --- a/src/nsls2api/infrastructure/mongodb_setup.py +++ b/src/nsls2api/infrastructure/mongodb_setup.py @@ -37,3 +37,5 @@ async def init_connection(mongodb_dsn: MongoDsn): logger.info( f"Connected to {click.style(client.get_default_database().name, fg='green')} database." ) + + return client diff --git a/src/nsls2api/models/apikeys.py b/src/nsls2api/models/apikeys.py index a51600ff..0321aa9f 100644 --- a/src/nsls2api/models/apikeys.py +++ b/src/nsls2api/models/apikeys.py @@ -41,7 +41,9 @@ class ApiUser(beanie.Document): last_updated: datetime.datetime = pydantic.Field( default_factory=datetime.datetime.now ) - user_api_keys: BackLink["ApiKey"] = Field(original_field="user") + user_api_keys: BackLink["ApiKey"] = Field( + json_schema_extra={"original_field": "user"} + ) # user_api_keys: Optional[List[BackLink["ApiKey"]]]= Field(original_field="user") class Settings: diff --git a/src/nsls2api/models/proposals.py b/src/nsls2api/models/proposals.py index 93f48427..e6a8eff1 100644 --- a/src/nsls2api/models/proposals.py +++ b/src/nsls2api/models/proposals.py @@ -4,6 +4,7 @@ import beanie import pydantic import pymongo +from pydantic import ConfigDict from nsls2api.models.slack_models import SlackChannel @@ -48,8 +49,7 @@ class ProposalBase(pydantic.BaseModel): # -- Pydantic Model for Display/Transport -- class ProposalDisplay(ProposalBase): # Prevent unwanted fields (like MongoDB _id) from breaking deserialization - class Config: - extra = "ignore" + model_config = ConfigDict(extra="ignore") # -- Beanie Model for Database -- diff --git a/src/nsls2api/tests/api/test_beamline_api.py b/src/nsls2api/tests/api/test_beamline_api.py index ade87d7f..b539db4e 100644 --- a/src/nsls2api/tests/api/test_beamline_api.py +++ b/src/nsls2api/tests/api/test_beamline_api.py @@ -2,7 +2,12 @@ from httpx import ASGITransport, AsyncClient from nsls2api.main import app -from nsls2api.models.beamlines import Beamline, DetectorList, DirectoryList, ServiceAccounts +from nsls2api.models.beamlines import ( + Beamline, + DetectorList, + DirectoryList, + ServiceAccounts, +) @pytest.mark.anyio @@ -110,10 +115,10 @@ async def test_get_beamline_detectors_with_empty_list(): transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get("/v1/beamline/zzz/detectors") - + assert response.status_code == 200 response_json = response.json() - + # Verify the response structure detector_list = DetectorList(**response_json) assert detector_list.detectors == [] @@ -127,10 +132,8 @@ async def test_get_beamline_detectors_for_nonexistent_beamline(): transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get("/v1/beamline/does-not-exist/detectors") - + assert response.status_code == 404 response_json = response.json() assert "detail" in response_json assert "does not exist" in response_json["detail"].lower() - - diff --git a/src/nsls2api/tests/api/test_facility_api.py b/src/nsls2api/tests/api/test_facility_api.py index b9c6496b..e606cd92 100644 --- a/src/nsls2api/tests/api/test_facility_api.py +++ b/src/nsls2api/tests/api/test_facility_api.py @@ -44,7 +44,7 @@ async def test_get_facility_cycles(): assert facility_cycles.cycles[0] == "1999-1" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_proposals_for_cycle(): facility_name = "nsls2" cycle_name = "1999-1" @@ -67,6 +67,7 @@ async def test_get_proposals_for_cycle(): assert len(cycle_proposals.proposals) == 0 assert cycle_proposals.count == 0 + @pytest.mark.anyio async def test_get_cycle_details_success(): facility_name = "nsls2" @@ -87,6 +88,7 @@ async def test_get_cycle_details_success(): assert "is_current_operating_cycle" in response_json assert "accepting_proposals" in response_json + @pytest.mark.anyio async def test_get_cycle_details_not_found(): facility_name = "nsls2" @@ -100,4 +102,7 @@ async def test_get_cycle_details_not_found(): response_json = response.json() assert response.status_code == 404 assert "error" in response_json - assert f"Requested Cycle '{cycle_name}' does not exist for facility '{facility_name}'." in response_json["error"] \ No newline at end of file + assert ( + f"Requested Cycle '{cycle_name}' does not exist for facility '{facility_name}'." + in response_json["error"] + ) diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py index 1d867059..428cc79c 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -1,6 +1,6 @@ import datetime -import pytest_asyncio +import pytest from nsls2api import models from nsls2api.infrastructure.config import get_settings @@ -14,106 +14,113 @@ from nsls2api.models.proposals import Proposal -@pytest_asyncio.fixture(scope="function", autouse=True) -async def db(): +@pytest.fixture +def anyio_backend(): + return "asyncio" + + +@pytest.fixture(scope="function", autouse=True) +async def db(anyio_backend): settings = get_settings() - await init_connection(settings.mongodb_dsn) - - # Insert a beamline into the database - beamline = Beamline( - name="ZZZ", - port="66-ID-6", - long_name="Magical PyTest X-Ray Beamline", - alternative_name="66-ID", - network_locations=["xf66id6"], - pass_name="Beamline 66-ID-6", - pass_id="666666", - nsls2_redhat_satellite_location_name="Nowhere", - service_accounts=ServiceAccounts( - ioc="testy-mctestface-ioc", - bluesky="testy-mctestface-bluesky", - epics_services="testy-mctestface-epics-services", - operator="testy-mctestface-xf66id6", - workflow="testy-mctestface-workflow", - ), - ) - await beamline.insert() - - # Insert a facility into the database - facility = Facility( - name="NSLS-II", - facility_id="nsls2", - fullname="National Synchrotron Light Source II", - pass_facility_id="NSLS-II", - data_admin_group="nsls2-data-admins", - data_admins=["testy-mcdata"], - ) - await facility.insert() - - # Insert a cycle into the database - cycle = Cycle( - name="1999-1", - facility="nsls2", - year="1999", - start_date=datetime.datetime.fromisoformat("1999-01-01"), - end_date=datetime.datetime.fromisoformat("1999-06-30"), - is_current_operating_cycle=True, - pass_description="January - June", - pass_id="111111", - ) - await cycle.insert() - - # Insert a proposal type into the database - proposal_type = ProposalType( - code="X", - facility_id="nsls2", - description="Proposal Type X", - pass_id="999999", - pass_description="Proposal Type X", - ) - await proposal_type.insert() - - # Insert a proposal into the database - test_proposal_id = "314159" - proposal = Proposal( - proposal_id=test_proposal_id, - data_session=f"pass-{test_proposal_id}", - title="Test Proposal", - type=proposal_type.description, - pass_type_id=proposal_type.pass_id, - instruments=["ZZZ"], - cycles=[cycle.name], - users=[], - safs=[], - slack_channels=[], - created_on=datetime.datetime.fromisoformat("1999-01-01"), - last_updated=datetime.datetime.now(), - locked=False, - ) - await proposal.insert() - - yield - - # Clean up the database collections - for model in models.all_models: - print(f"dropping {model}") - await model.get_pymongo_collection().drop() - await model.get_pymongo_collection().drop_indexes() - - -@pytest_asyncio.fixture(scope="function", autouse=True) -async def api_key(db): + client = await init_connection(settings.mongodb_dsn) + + try: + # Insert a beamline into the database + beamline = Beamline( + name="ZZZ", + port="66-ID-6", + long_name="Magical PyTest X-Ray Beamline", + alternative_name="66-ID", + network_locations=["xf66id6"], + pass_name="Beamline 66-ID-6", + pass_id="666666", + nsls2_redhat_satellite_location_name="Nowhere", + service_accounts=ServiceAccounts( + ioc="testy-mctestface-ioc", + bluesky="testy-mctestface-bluesky", + epics_services="testy-mctestface-epics-services", + operator="testy-mctestface-xf66id6", + workflow="testy-mctestface-workflow", + ), + ) + await beamline.insert() + + # Insert a facility into the database + facility = Facility( + name="NSLS-II", + facility_id="nsls2", + fullname="National Synchrotron Light Source II", + pass_facility_id="NSLS-II", + data_admin_group="nsls2-data-admins", + data_admins=["testy-mcdata"], + ) + await facility.insert() + + # Insert a cycle into the database + cycle = Cycle( + name="1999-1", + facility="nsls2", + year="1999", + start_date=datetime.datetime.fromisoformat("1999-01-01"), + end_date=datetime.datetime.fromisoformat("1999-06-30"), + is_current_operating_cycle=True, + pass_description="January - June", + pass_id="111111", + ) + await cycle.insert() + + # Insert a proposal type into the database + proposal_type = ProposalType( + code="X", + facility_id="nsls2", + description="Proposal Type X", + pass_id="999999", + pass_description="Proposal Type X", + ) + await proposal_type.insert() + + # Insert a proposal into the database + test_proposal_id = "314159" + proposal = Proposal( + proposal_id=test_proposal_id, + data_session=f"pass-{test_proposal_id}", + title="Test Proposal", + type=proposal_type.description, + pass_type_id=proposal_type.pass_id, + instruments=["ZZZ"], + cycles=[cycle.name], + users=[], + safs=[], + slack_channels=[], + created_on=datetime.datetime.fromisoformat("1999-01-01"), + last_updated=datetime.datetime.now(), + locked=False, + ) + await proposal.insert() + + yield + + finally: + # Clean up the database collections + for model in models.all_models: + print(f"dropping {model}") + await model.get_pymongo_collection().drop() + await model.get_pymongo_collection().drop_indexes() + await client.close() + + +@pytest.fixture(scope="function", autouse=True) +async def api_key(anyio_backend, db): """Generate and return an API key for test authentication.""" return await generate_api_key(username="test_user", usertype=ApiUserType.user) -@pytest_asyncio.fixture(scope="function", autouse=True) -async def admin_api_key(db): +@pytest.fixture(scope="function", autouse=True) +async def admin_api_key(anyio_backend, db): """Generate and return an admin API key for test authentication.""" # Create API key for the admin test user key = await generate_api_key(username="test_admin", usertype=ApiUserType.user) # Promote this user to admin await set_user_role("test_admin", ApiUserRole.admin) - return key diff --git a/src/nsls2api/tests/services/test_proposal_service.py b/src/nsls2api/tests/services/test_proposal_service.py index e6d6745e..850a47a2 100644 --- a/src/nsls2api/tests/services/test_proposal_service.py +++ b/src/nsls2api/tests/services/test_proposal_service.py @@ -14,6 +14,7 @@ PAGE = 1 PAGE_SIZE = 10 + @pytest.mark.anyio async def test_get_beamline_specific_slack_channel_for_proposal(): slack_channels = ( @@ -107,20 +108,21 @@ async def test_case_sensitivity_fetch_proposals(): @pytest.mark.anyio async def test_data_sessions_endpoint(admin_api_key): async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test", - headers={"Authorization": admin_api_key['key']} # Use the api_key fixture here + transport=ASGITransport(app=app), + base_url="http://test", + headers={"Authorization": admin_api_key["key"]}, # Use the api_key fixture here ) as ac: resp = await ac.get( "/v1/proposals/data-sessions", params={ "beamline": test_beamline_name, "cycle": test_cycle_name, - "facility": "nsls2", - "page": PAGE, - "page_size": PAGE_SIZE, - }, - ) - + "facility": "nsls2", + "page": PAGE, + "page_size": PAGE_SIZE, + }, + ) + assert resp.status_code == 200 body = resp.json() print(resp.json()) @@ -137,12 +139,14 @@ async def test_data_sessions_endpoint(admin_api_key): assert first["proposal_id"] == test_proposal_id assert first["data_session"] == f"pass-{test_proposal_id}" + @pytest.mark.anyio async def test_data_sessions_pagination(admin_api_key): small_page_size = 2 async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test", - headers={"Authorization": admin_api_key['key']} # Use the api_key fixture here + transport=ASGITransport(app=app), + base_url="http://test", + headers={"Authorization": admin_api_key["key"]}, # Use the api_key fixture here ) as ac: resp = await ac.get( "/v1/proposals/data-sessions", @@ -164,11 +168,13 @@ async def test_data_sessions_pagination(admin_api_key): assert "proposal_id" in p assert "data_session" in p + @pytest.mark.anyio async def test_data_sessions_invalid_beamline(admin_api_key): async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test", - headers={"Authorization": admin_api_key['key']} # Use the api_key fixture here + transport=ASGITransport(app=app), + base_url="http://test", + headers={"Authorization": admin_api_key["key"]}, # Use the api_key fixture here ) as ac: resp = await ac.get( "/v1/proposals/data-sessions", @@ -183,4 +189,4 @@ async def test_data_sessions_invalid_beamline(admin_api_key): assert resp.status_code == 200 body = resp.json() assert body["count"] == 0 - assert body["proposals"] == [] \ No newline at end of file + assert body["proposals"] == []