From 20b3ce221ecbbe88cc723ceae3c92f42f937cb8d Mon Sep 17 00:00:00 2001 From: Harikabishai Date: Wed, 6 May 2026 17:52:43 -0400 Subject: [PATCH 1/9] updated packages --- requirements-dev.txt | 175 ++++++++++++++++++++++--------------------- requirements.txt | 92 +++++++++++------------ 2 files changed, 137 insertions(+), 130 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 2cdd37dd..d085272c 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,55 @@ 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 +301,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 +334,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,20 +346,21 @@ 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 @@ -362,11 +368,10 @@ typing-extensions==4.15.0 # 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 +379,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 +410,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 From 3331cec3230010b6eef3e4486b597feeba265d2e Mon Sep 17 00:00:00 2001 From: Harikabishai Date: Wed, 6 May 2026 18:36:22 -0400 Subject: [PATCH 2/9] mongo close changes Co-authored-by: Copilot --- scripts/create_new_beamline.py | 4 +- scripts/update_beamline.py | 9 +- src/nsls2api/infrastructure/app_setup.py | 5 +- src/nsls2api/infrastructure/mongodb_setup.py | 2 + src/nsls2api/tests/conftest.py | 167 ++++++++++--------- 5 files changed, 100 insertions(+), 87 deletions(-) diff --git a/scripts/create_new_beamline.py b/scripts/create_new_beamline.py index db416eab..a38e3d4b 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() + client.close() + if __name__ == "__main__": asyncio.run(main()) diff --git a/scripts/update_beamline.py b/scripts/update_beamline.py index 66c69503..5006a3b6 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() + 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..c15ed4d9 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 + 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/tests/conftest.py b/src/nsls2api/tests/conftest.py index 1d867059..38859d24 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -17,88 +17,91 @@ @pytest_asyncio.fixture(scope="function", autouse=True) async def db(): 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() + 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() + client.close() @pytest_asyncio.fixture(scope="function", autouse=True) From af486723dfc6300813e8c87564e65999a5652afc Mon Sep 17 00:00:00 2001 From: Harikabishai Date: Wed, 6 May 2026 18:44:08 -0400 Subject: [PATCH 3/9] asyncio changes --- src/nsls2api/tests/api/test_admin_api.py | 6 ++--- src/nsls2api/tests/api/test_beamline_api.py | 18 +++++++------- src/nsls2api/tests/api/test_facility_api.py | 8 +++---- .../tests/services/test_facility_service.py | 22 ++++++++--------- .../tests/services/test_proposal_service.py | 24 +++++++++---------- src/nsls2api/tests/test_home.py | 2 +- 6 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/nsls2api/tests/api/test_admin_api.py b/src/nsls2api/tests/api/test_admin_api.py index d54594af..a61de583 100644 --- a/src/nsls2api/tests/api/test_admin_api.py +++ b/src/nsls2api/tests/api/test_admin_api.py @@ -17,7 +17,7 @@ facility_name = "nsls2" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_lock_and_unlock_proposals(admin_api_key): key = admin_api_key["key"] # resetting to ensure locked is false @@ -94,7 +94,7 @@ async def test_lock_and_unlock_proposals(admin_api_key): assert not proposal_objects[0].locked -@pytest.mark.anyio +@pytest.mark.asyncio async def test_lock_and_unlock_beamlines(admin_api_key): key = admin_api_key["key"] # start with unlocking to ensure its unlocked @@ -152,7 +152,7 @@ async def test_lock_and_unlock_beamlines(admin_api_key): assert not proposal_objects[0].locked -@pytest.mark.anyio +@pytest.mark.asyncio async def test_lock_and_unlock_cycles(admin_api_key): key = admin_api_key["key"] diff --git a/src/nsls2api/tests/api/test_beamline_api.py b/src/nsls2api/tests/api/test_beamline_api.py index ade87d7f..77e1addd 100644 --- a/src/nsls2api/tests/api/test_beamline_api.py +++ b/src/nsls2api/tests/api/test_beamline_api.py @@ -5,7 +5,7 @@ from nsls2api.models.beamlines import Beamline, DetectorList, DirectoryList, ServiceAccounts -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_beamline_service_accounts(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -24,7 +24,7 @@ async def test_get_beamline_service_accounts(): assert accounts.lsdc is None or accounts.lsdc == "" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_beamline_lowercase(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -39,7 +39,7 @@ async def test_get_beamline_lowercase(): assert beamline.name == "ZZZ" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_beamline_uppercase(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -54,7 +54,7 @@ async def test_get_beamline_uppercase(): assert beamline.name == "ZZZ" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_beamline_directory_skeleton(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -74,7 +74,7 @@ async def test_get_beamline_directory_skeleton(): assert directory_skeleton.directory_count == 2 -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_nonexistent_beamline(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -84,7 +84,7 @@ async def test_get_nonexistent_beamline(): assert response.json() == {"detail": "Beamline 'DOES-NOT-EXIST' does not exist"} -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_service_accounts_for_nonexistent_beamline(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -94,7 +94,7 @@ async def test_get_service_accounts_for_nonexistent_beamline(): assert response.json() == {"detail": "Beamline 'DOES-NOT-EXIST' does not exist"} -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_directory_skeleton_for_nonexistent_beamline(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -103,7 +103,7 @@ async def test_get_directory_skeleton_for_nonexistent_beamline(): assert response.status_code == 404 -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_beamline_detectors_with_empty_list(): """Test that detectors endpoint returns empty array when no detectors exist.""" async with AsyncClient( @@ -120,7 +120,7 @@ async def test_get_beamline_detectors_with_empty_list(): assert detector_list.count == 0 -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_beamline_detectors_for_nonexistent_beamline(): """Test that 404 error is returned when requesting detectors for non-existent beamline.""" async with AsyncClient( diff --git a/src/nsls2api/tests/api/test_facility_api.py b/src/nsls2api/tests/api/test_facility_api.py index b9c6496b..fd7211e7 100644 --- a/src/nsls2api/tests/api/test_facility_api.py +++ b/src/nsls2api/tests/api/test_facility_api.py @@ -9,7 +9,7 @@ from nsls2api.main import app -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_current_operating_cycle(): facility_name = "nsls2" async with AsyncClient( @@ -26,7 +26,7 @@ async def test_get_current_operating_cycle(): assert current_cycle.cycle == "1999-1" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_facility_cycles(): facility_name = "nsls2" async with AsyncClient( @@ -67,7 +67,7 @@ async def test_get_proposals_for_cycle(): assert len(cycle_proposals.proposals) == 0 assert cycle_proposals.count == 0 -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_cycle_details_success(): facility_name = "nsls2" cycle_name = "1999-1" @@ -87,7 +87,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 +@pytest.mark.asyncio async def test_get_cycle_details_not_found(): facility_name = "nsls2" cycle_name = "nonexistent-cycle" diff --git a/src/nsls2api/tests/services/test_facility_service.py b/src/nsls2api/tests/services/test_facility_service.py index 3138b4a1..84d252b9 100644 --- a/src/nsls2api/tests/services/test_facility_service.py +++ b/src/nsls2api/tests/services/test_facility_service.py @@ -5,49 +5,49 @@ valid_cycle_name = "1999-1" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_pass_id_for_facility(): pass_id = await facility_service.pass_id_for_facility("nsls2") assert pass_id == "NSLS-II" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_facility_by_pass_id(): facility = await facility_service.facility_by_pass_id("NSLS-II") assert facility.name == "NSLS-II" assert facility.facility_id == "nsls2" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_data_admin_group(): data_admin_group = await facility_service.data_admin_group("nsls2") assert data_admin_group == "nsls2-data-admins" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_data_admins(): data_admins = await facility_service.get_data_admins("nsls2") assert data_admins == ["testy-mcdata"] -@pytest.mark.anyio +@pytest.mark.asyncio async def test_facilities_count(): assert await facility_service.facilities_count() == 1 -@pytest.mark.anyio +@pytest.mark.asyncio async def test_all_facilities(): facilities = await facility_service.all_facilities() assert type(facilities) is list -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_current_operating_cycle(): cycle = await facility_service.current_operating_cycle("nsls2") assert cycle == valid_cycle_name -@pytest.mark.anyio +@pytest.mark.asyncio async def test_set_current_operating_cycle(): cycle = await facility_service.set_current_operating_cycle( "nsls2", valid_cycle_name @@ -55,19 +55,19 @@ async def test_set_current_operating_cycle(): assert cycle == valid_cycle_name -@pytest.mark.anyio +@pytest.mark.asyncio async def test_set_current_operating_cycle_invalid(): with pytest.raises(facility_service.CycleNotFoundError): await facility_service.set_current_operating_cycle("nsls2", "invalid-cycle") -@pytest.mark.anyio +@pytest.mark.asyncio async def test_cycle_year(): cycle = await facility_service.cycle_year(valid_cycle_name) assert cycle == "1999" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_cycle_exists(): cycle_exists = await facility_service.cycle_exists( cycle_name=valid_cycle_name, facility="nsls2" diff --git a/src/nsls2api/tests/services/test_proposal_service.py b/src/nsls2api/tests/services/test_proposal_service.py index e6d6745e..c40b1192 100644 --- a/src/nsls2api/tests/services/test_proposal_service.py +++ b/src/nsls2api/tests/services/test_proposal_service.py @@ -14,7 +14,7 @@ PAGE = 1 PAGE_SIZE = 10 -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_beamline_specific_slack_channel_for_proposal(): slack_channels = ( await proposal_service.get_beamline_specific_slack_channel_for_proposal( @@ -26,14 +26,14 @@ async def test_get_beamline_specific_slack_channel_for_proposal(): assert slack_channels[0] == f"pass-{test_proposal_id}-zzz" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_proposal_by_id(): proposal: Proposal = await proposal_service.proposal_by_id(test_proposal_id) assert proposal is not None assert proposal.proposal_id == test_proposal_id -@pytest.mark.anyio +@pytest.mark.asyncio async def test_proposal_type_description_from_pass_type_id(): test_proposal_type_id = 999999 description = await proposal_service.proposal_type_description_from_pass_type_id( @@ -43,7 +43,7 @@ async def test_proposal_type_description_from_pass_type_id(): assert description == "Proposal Type X" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_proposal_type_description_from_nonexistent_pass_type_id(): test_proposal_type_id = 999991 # non-existent proposal type_id try: @@ -58,7 +58,7 @@ async def test_proposal_type_description_from_nonexistent_pass_type_id(): assert True -@pytest.mark.anyio +@pytest.mark.asyncio async def test_data_session_for_proposal(): data_session = await proposal_service.data_session_for_proposal( proposal_id=test_proposal_id @@ -67,7 +67,7 @@ async def test_data_session_for_proposal(): assert data_session == f"pass-{test_proposal_id}" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_beamlines_for_proposal(): beamlines = await proposal_service.beamlines_for_proposal( proposal_id=test_proposal_id @@ -77,7 +77,7 @@ async def test_beamlines_for_proposal(): assert beamlines[0] == "ZZZ" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_cycles_for_proposal(): cycles = await proposal_service.cycles_for_proposal(proposal_id=test_proposal_id) assert cycles is not None @@ -85,13 +85,13 @@ async def test_cycles_for_proposal(): assert cycles[0] == "1999-1" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_is_commissioning(): proposal = await proposal_service.proposal_by_id(test_proposal_id) assert await proposal_service.is_commissioning(proposal) is False -@pytest.mark.anyio +@pytest.mark.asyncio async def test_case_sensitivity_fetch_proposals(): proposal_objects_upper = await proposal_service.fetch_proposals( beamline=[test_beamline_name] @@ -104,7 +104,7 @@ async def test_case_sensitivity_fetch_proposals(): assert proposal_objects_lower[0].proposal_id == test_proposal_id -@pytest.mark.anyio +@pytest.mark.asyncio async def test_data_sessions_endpoint(admin_api_key): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test", @@ -137,7 +137,7 @@ 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 +@pytest.mark.asyncio async def test_data_sessions_pagination(admin_api_key): small_page_size = 2 async with AsyncClient( @@ -164,7 +164,7 @@ async def test_data_sessions_pagination(admin_api_key): assert "proposal_id" in p assert "data_session" in p -@pytest.mark.anyio +@pytest.mark.asyncio async def test_data_sessions_invalid_beamline(admin_api_key): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test", diff --git a/src/nsls2api/tests/test_home.py b/src/nsls2api/tests/test_home.py index f3647e0c..0d03794b 100644 --- a/src/nsls2api/tests/test_home.py +++ b/src/nsls2api/tests/test_home.py @@ -4,7 +4,7 @@ from nsls2api.main import app -@pytest.mark.anyio +@pytest.mark.asyncio async def test_healthy_endpoint(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" From 567d57b5b8bc2eab3dea6c1e90d82e0ac0c94b14 Mon Sep 17 00:00:00 2001 From: Harikabishai Date: Wed, 6 May 2026 21:52:16 -0400 Subject: [PATCH 4/9] awaiting on mongo.close() Co-authored-by: Copilot --- scripts/create_new_beamline.py | 2 +- scripts/update_beamline.py | 2 +- src/nsls2api/infrastructure/app_setup.py | 2 +- src/nsls2api/tests/conftest.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/create_new_beamline.py b/scripts/create_new_beamline.py index a38e3d4b..ac00180e 100644 --- a/scripts/create_new_beamline.py +++ b/scripts/create_new_beamline.py @@ -48,7 +48,7 @@ async def main(): # Uncomment this line to actually insert the new beamline into the database # await new_beamline.insert() - client.close() + await client.close() if __name__ == "__main__": diff --git a/scripts/update_beamline.py b/scripts/update_beamline.py index 5006a3b6..623fa977 100644 --- a/scripts/update_beamline.py +++ b/scripts/update_beamline.py @@ -56,7 +56,7 @@ async def main(): # Uncomment the line below to actually save the changes to the database # await beamline.save() - client.close() + await client.close() if __name__ == "__main__": diff --git a/src/nsls2api/infrastructure/app_setup.py b/src/nsls2api/infrastructure/app_setup.py index c15ed4d9..9b5b6c6d 100644 --- a/src/nsls2api/infrastructure/app_setup.py +++ b/src/nsls2api/infrastructure/app_setup.py @@ -37,4 +37,4 @@ async def app_lifespan(app: FastAPI): await httpx_client_wrapper.stop() # Close MongoDB client - mongodb_client.close() + await mongodb_client.close() diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py index 38859d24..17ef1d93 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -101,7 +101,7 @@ async def db(): print(f"dropping {model}") await model.get_pymongo_collection().drop() await model.get_pymongo_collection().drop_indexes() - client.close() + await client.close() @pytest_asyncio.fixture(scope="function", autouse=True) From a2cfb56ff75673cf2719319387ed7577be042df7 Mon Sep 17 00:00:00 2001 From: Harikabishai Date: Wed, 6 May 2026 22:24:21 -0400 Subject: [PATCH 5/9] warning fixes Co-authored-by: Copilot --- src/nsls2api/models/apikeys.py | 4 +++- src/nsls2api/models/proposals.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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 -- From 3edc61f46481a5eabe5bcbe5a0e4eb5489d13e20 Mon Sep 17 00:00:00 2001 From: Harikabishai Date: Thu, 7 May 2026 10:35:29 -0400 Subject: [PATCH 6/9] GHA updates --- .github/workflows/docker-publish.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) 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' }} From 405f4631105d182b64aee2d67f7454b873bb10d4 Mon Sep 17 00:00:00 2001 From: Harikabishai Date: Mon, 11 May 2026 16:02:11 -0400 Subject: [PATCH 7/9] anyio changes --- src/nsls2api/tests/api/test_admin_api.py | 6 +- src/nsls2api/tests/api/test_beamline_api.py | 33 ++++++----- src/nsls2api/tests/api/test_facility_api.py | 17 ++++-- src/nsls2api/tests/conftest.py | 8 +-- .../tests/services/test_facility_service.py | 22 ++++---- .../tests/services/test_proposal_service.py | 56 ++++++++++--------- src/nsls2api/tests/test_home.py | 2 +- 7 files changed, 79 insertions(+), 65 deletions(-) diff --git a/src/nsls2api/tests/api/test_admin_api.py b/src/nsls2api/tests/api/test_admin_api.py index a61de583..d54594af 100644 --- a/src/nsls2api/tests/api/test_admin_api.py +++ b/src/nsls2api/tests/api/test_admin_api.py @@ -17,7 +17,7 @@ facility_name = "nsls2" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_lock_and_unlock_proposals(admin_api_key): key = admin_api_key["key"] # resetting to ensure locked is false @@ -94,7 +94,7 @@ async def test_lock_and_unlock_proposals(admin_api_key): assert not proposal_objects[0].locked -@pytest.mark.asyncio +@pytest.mark.anyio async def test_lock_and_unlock_beamlines(admin_api_key): key = admin_api_key["key"] # start with unlocking to ensure its unlocked @@ -152,7 +152,7 @@ async def test_lock_and_unlock_beamlines(admin_api_key): assert not proposal_objects[0].locked -@pytest.mark.asyncio +@pytest.mark.anyio async def test_lock_and_unlock_cycles(admin_api_key): key = admin_api_key["key"] diff --git a/src/nsls2api/tests/api/test_beamline_api.py b/src/nsls2api/tests/api/test_beamline_api.py index 77e1addd..b539db4e 100644 --- a/src/nsls2api/tests/api/test_beamline_api.py +++ b/src/nsls2api/tests/api/test_beamline_api.py @@ -2,10 +2,15 @@ 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.asyncio +@pytest.mark.anyio async def test_get_beamline_service_accounts(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -24,7 +29,7 @@ async def test_get_beamline_service_accounts(): assert accounts.lsdc is None or accounts.lsdc == "" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_beamline_lowercase(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -39,7 +44,7 @@ async def test_get_beamline_lowercase(): assert beamline.name == "ZZZ" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_beamline_uppercase(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -54,7 +59,7 @@ async def test_get_beamline_uppercase(): assert beamline.name == "ZZZ" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_beamline_directory_skeleton(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -74,7 +79,7 @@ async def test_get_beamline_directory_skeleton(): assert directory_skeleton.directory_count == 2 -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_nonexistent_beamline(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -84,7 +89,7 @@ async def test_get_nonexistent_beamline(): assert response.json() == {"detail": "Beamline 'DOES-NOT-EXIST' does not exist"} -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_service_accounts_for_nonexistent_beamline(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -94,7 +99,7 @@ async def test_get_service_accounts_for_nonexistent_beamline(): assert response.json() == {"detail": "Beamline 'DOES-NOT-EXIST' does not exist"} -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_directory_skeleton_for_nonexistent_beamline(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" @@ -103,34 +108,32 @@ async def test_get_directory_skeleton_for_nonexistent_beamline(): assert response.status_code == 404 -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_beamline_detectors_with_empty_list(): """Test that detectors endpoint returns empty array when no detectors exist.""" async with AsyncClient( 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 == [] assert detector_list.count == 0 -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_beamline_detectors_for_nonexistent_beamline(): """Test that 404 error is returned when requesting detectors for non-existent beamline.""" async with AsyncClient( 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 fd7211e7..e606cd92 100644 --- a/src/nsls2api/tests/api/test_facility_api.py +++ b/src/nsls2api/tests/api/test_facility_api.py @@ -9,7 +9,7 @@ from nsls2api.main import app -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_current_operating_cycle(): facility_name = "nsls2" async with AsyncClient( @@ -26,7 +26,7 @@ async def test_get_current_operating_cycle(): assert current_cycle.cycle == "1999-1" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_facility_cycles(): facility_name = "nsls2" async with AsyncClient( @@ -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,7 +67,8 @@ async def test_get_proposals_for_cycle(): assert len(cycle_proposals.proposals) == 0 assert cycle_proposals.count == 0 -@pytest.mark.asyncio + +@pytest.mark.anyio async def test_get_cycle_details_success(): facility_name = "nsls2" cycle_name = "1999-1" @@ -87,7 +88,8 @@ async def test_get_cycle_details_success(): assert "is_current_operating_cycle" in response_json assert "accepting_proposals" in response_json -@pytest.mark.asyncio + +@pytest.mark.anyio async def test_get_cycle_details_not_found(): facility_name = "nsls2" cycle_name = "nonexistent-cycle" @@ -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 17ef1d93..fe75c7ef 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,7 +14,7 @@ from nsls2api.models.proposals import Proposal -@pytest_asyncio.fixture(scope="function", autouse=True) +@pytest.fixture(scope="function", autouse=True) async def db(): settings = get_settings() client = await init_connection(settings.mongodb_dsn) @@ -104,13 +104,13 @@ async def db(): await client.close() -@pytest_asyncio.fixture(scope="function", autouse=True) +@pytest.fixture(scope="function", autouse=True) async def api_key(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) +@pytest.fixture(scope="function", autouse=True) async def admin_api_key(db): """Generate and return an admin API key for test authentication.""" # Create API key for the admin test user diff --git a/src/nsls2api/tests/services/test_facility_service.py b/src/nsls2api/tests/services/test_facility_service.py index 84d252b9..3138b4a1 100644 --- a/src/nsls2api/tests/services/test_facility_service.py +++ b/src/nsls2api/tests/services/test_facility_service.py @@ -5,49 +5,49 @@ valid_cycle_name = "1999-1" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_pass_id_for_facility(): pass_id = await facility_service.pass_id_for_facility("nsls2") assert pass_id == "NSLS-II" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_facility_by_pass_id(): facility = await facility_service.facility_by_pass_id("NSLS-II") assert facility.name == "NSLS-II" assert facility.facility_id == "nsls2" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_data_admin_group(): data_admin_group = await facility_service.data_admin_group("nsls2") assert data_admin_group == "nsls2-data-admins" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_data_admins(): data_admins = await facility_service.get_data_admins("nsls2") assert data_admins == ["testy-mcdata"] -@pytest.mark.asyncio +@pytest.mark.anyio async def test_facilities_count(): assert await facility_service.facilities_count() == 1 -@pytest.mark.asyncio +@pytest.mark.anyio async def test_all_facilities(): facilities = await facility_service.all_facilities() assert type(facilities) is list -@pytest.mark.asyncio +@pytest.mark.anyio async def test_get_current_operating_cycle(): cycle = await facility_service.current_operating_cycle("nsls2") assert cycle == valid_cycle_name -@pytest.mark.asyncio +@pytest.mark.anyio async def test_set_current_operating_cycle(): cycle = await facility_service.set_current_operating_cycle( "nsls2", valid_cycle_name @@ -55,19 +55,19 @@ async def test_set_current_operating_cycle(): assert cycle == valid_cycle_name -@pytest.mark.asyncio +@pytest.mark.anyio async def test_set_current_operating_cycle_invalid(): with pytest.raises(facility_service.CycleNotFoundError): await facility_service.set_current_operating_cycle("nsls2", "invalid-cycle") -@pytest.mark.asyncio +@pytest.mark.anyio async def test_cycle_year(): cycle = await facility_service.cycle_year(valid_cycle_name) assert cycle == "1999" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_cycle_exists(): cycle_exists = await facility_service.cycle_exists( cycle_name=valid_cycle_name, facility="nsls2" diff --git a/src/nsls2api/tests/services/test_proposal_service.py b/src/nsls2api/tests/services/test_proposal_service.py index c40b1192..850a47a2 100644 --- a/src/nsls2api/tests/services/test_proposal_service.py +++ b/src/nsls2api/tests/services/test_proposal_service.py @@ -14,7 +14,8 @@ PAGE = 1 PAGE_SIZE = 10 -@pytest.mark.asyncio + +@pytest.mark.anyio async def test_get_beamline_specific_slack_channel_for_proposal(): slack_channels = ( await proposal_service.get_beamline_specific_slack_channel_for_proposal( @@ -26,14 +27,14 @@ async def test_get_beamline_specific_slack_channel_for_proposal(): assert slack_channels[0] == f"pass-{test_proposal_id}-zzz" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_proposal_by_id(): proposal: Proposal = await proposal_service.proposal_by_id(test_proposal_id) assert proposal is not None assert proposal.proposal_id == test_proposal_id -@pytest.mark.asyncio +@pytest.mark.anyio async def test_proposal_type_description_from_pass_type_id(): test_proposal_type_id = 999999 description = await proposal_service.proposal_type_description_from_pass_type_id( @@ -43,7 +44,7 @@ async def test_proposal_type_description_from_pass_type_id(): assert description == "Proposal Type X" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_proposal_type_description_from_nonexistent_pass_type_id(): test_proposal_type_id = 999991 # non-existent proposal type_id try: @@ -58,7 +59,7 @@ async def test_proposal_type_description_from_nonexistent_pass_type_id(): assert True -@pytest.mark.asyncio +@pytest.mark.anyio async def test_data_session_for_proposal(): data_session = await proposal_service.data_session_for_proposal( proposal_id=test_proposal_id @@ -67,7 +68,7 @@ async def test_data_session_for_proposal(): assert data_session == f"pass-{test_proposal_id}" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_beamlines_for_proposal(): beamlines = await proposal_service.beamlines_for_proposal( proposal_id=test_proposal_id @@ -77,7 +78,7 @@ async def test_beamlines_for_proposal(): assert beamlines[0] == "ZZZ" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_cycles_for_proposal(): cycles = await proposal_service.cycles_for_proposal(proposal_id=test_proposal_id) assert cycles is not None @@ -85,13 +86,13 @@ async def test_cycles_for_proposal(): assert cycles[0] == "1999-1" -@pytest.mark.asyncio +@pytest.mark.anyio async def test_is_commissioning(): proposal = await proposal_service.proposal_by_id(test_proposal_id) assert await proposal_service.is_commissioning(proposal) is False -@pytest.mark.asyncio +@pytest.mark.anyio async def test_case_sensitivity_fetch_proposals(): proposal_objects_upper = await proposal_service.fetch_proposals( beamline=[test_beamline_name] @@ -104,23 +105,24 @@ async def test_case_sensitivity_fetch_proposals(): assert proposal_objects_lower[0].proposal_id == test_proposal_id -@pytest.mark.asyncio +@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.asyncio + +@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.asyncio + +@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"] == [] diff --git a/src/nsls2api/tests/test_home.py b/src/nsls2api/tests/test_home.py index 0d03794b..f3647e0c 100644 --- a/src/nsls2api/tests/test_home.py +++ b/src/nsls2api/tests/test_home.py @@ -4,7 +4,7 @@ from nsls2api.main import app -@pytest.mark.asyncio +@pytest.mark.anyio async def test_healthy_endpoint(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" From 754708da78d81ef6ff868efa5233b797a14364df Mon Sep 17 00:00:00 2001 From: Harikabishai Date: Mon, 11 May 2026 18:50:43 -0400 Subject: [PATCH 8/9] anyio changes Co-authored-by: Copilot --- pyproject.toml | 3 +-- requirements-dev.in | 2 +- requirements-dev.txt | 6 +++--- src/nsls2api/tests/conftest.py | 12 ++++++++---- 4 files changed, 13 insertions(+), 10 deletions(-) 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..277afdab 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -13,7 +13,7 @@ locust pre-commit-uv pyright pytest -pytest-asyncio +pytest-anyio ruff textual-dev uv diff --git a/requirements-dev.txt b/requirements-dev.txt index d085272c..4b958270 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,6 +19,7 @@ anyio==4.13.0 # via # asyncer # httpx + # pytest-anyio # watchfiles asgi-lifespan==2.1.0 # via -r requirements-dev.in @@ -278,8 +279,8 @@ pytest==9.0.3 # via # -r requirements-dev.in # locust - # pytest-asyncio -pytest-asyncio==1.3.0 + # pytest-anyio +pytest-anyio==0.0.0 # via -r requirements-dev.in python-discovery==1.3.0 # via @@ -364,7 +365,6 @@ typing-extensions==4.15.0 # pydantic # pydantic-core # pyright - # pytest-asyncio # rich-toolkit # textual # textual-dev diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py index fe75c7ef..428cc79c 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -14,8 +14,13 @@ from nsls2api.models.proposals import Proposal +@pytest.fixture +def anyio_backend(): + return "asyncio" + + @pytest.fixture(scope="function", autouse=True) -async def db(): +async def db(anyio_backend): settings = get_settings() client = await init_connection(settings.mongodb_dsn) @@ -105,18 +110,17 @@ async def db(): @pytest.fixture(scope="function", autouse=True) -async def api_key(db): +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.fixture(scope="function", autouse=True) -async def admin_api_key(db): +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 From f8c03172cbf592e1029f6e2353f49d7e8043aef3 Mon Sep 17 00:00:00 2001 From: Harikabishai Date: Tue, 12 May 2026 11:43:21 -0400 Subject: [PATCH 9/9] remove anyio package Co-authored-by: Copilot --- requirements-dev.in | 1 - requirements-dev.txt | 4 ---- 2 files changed, 5 deletions(-) diff --git a/requirements-dev.in b/requirements-dev.in index 277afdab..4cebe044 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -13,7 +13,6 @@ locust pre-commit-uv pyright pytest -pytest-anyio ruff textual-dev uv diff --git a/requirements-dev.txt b/requirements-dev.txt index 4b958270..769e0376 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,7 +19,6 @@ anyio==4.13.0 # via # asyncer # httpx - # pytest-anyio # watchfiles asgi-lifespan==2.1.0 # via -r requirements-dev.in @@ -279,9 +278,6 @@ pytest==9.0.3 # via # -r requirements-dev.in # locust - # pytest-anyio -pytest-anyio==0.0.0 - # via -r requirements-dev.in python-discovery==1.3.0 # via # hatch