From 119f17eef38328d561fe63bd107e0517f83a3031 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Wed, 9 Oct 2024 13:38:17 -0400 Subject: [PATCH 01/46] Add pytest defaults --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ea41bfce..84d42635 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,10 @@ exclude = [ target_version = ['py311'] include = '\.pyi?$' +[tool.pytest.ini_options] +asyncio_default_fixture_loop_scope = "function" +asyncio_mode = "auto" + [tool.ruff] target-version = "py311" From 8e736be8935b879942540607c31db933ee45c8f3 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Wed, 9 Oct 2024 13:38:45 -0400 Subject: [PATCH 02/46] Add pytest fixtures --- src/nsls2api/tests/__init__.py | 0 src/nsls2api/tests/api/__init__.py | 0 src/nsls2api/tests/api/conftest.py | 36 ++++++++++++++++++++++++++++++ src/nsls2api/tests/conftest.py | 19 ++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 src/nsls2api/tests/__init__.py create mode 100644 src/nsls2api/tests/api/__init__.py create mode 100644 src/nsls2api/tests/api/conftest.py create mode 100644 src/nsls2api/tests/conftest.py diff --git a/src/nsls2api/tests/__init__.py b/src/nsls2api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/nsls2api/tests/api/__init__.py b/src/nsls2api/tests/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/nsls2api/tests/api/conftest.py b/src/nsls2api/tests/api/conftest.py new file mode 100644 index 00000000..73e983e8 --- /dev/null +++ b/src/nsls2api/tests/api/conftest.py @@ -0,0 +1,36 @@ +import pytest +from asgi_lifespan import LifespanManager +from httpx import AsyncClient + +from nsls2api.main import app +from nsls2api.models import facilities, jobs, apikeys, proposals, beamlines, cycles, proposal_types + +@pytest.fixture() +async def test_client(db): + async with LifespanManager(app, startup_timeout=100, shutdown_timeout=100): + server_name = "http://localhost" + async with AsyncClient(app=app, base_url=server_name, follow_redirects=True) as client: + yield client + +# +# @pytest.fixture(autouse=True) +# async def clean_db(db): +# all_models = [ +# facilities.Facility, +# cycles.Cycle, +# proposal_types.ProposalType, +# beamlines.Beamline, +# proposals.Proposal, +# apikeys.ApiKey, +# apikeys.ApiUser, +# jobs.BackgroundJob, +# ] +# yield None +# +# for model in all_models: +# print(f"dropping {model}") +# # await model.get_motor_collection().drop() +# # await model.get_motor_collection().drop_indexes() +# + + diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py new file mode 100644 index 00000000..c720f6fa --- /dev/null +++ b/src/nsls2api/tests/conftest.py @@ -0,0 +1,19 @@ +import motor.motor_asyncio +import pytest +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + mongodb_dsn: str = "mongodb://localhost:27017/test_db" + mongodb_db_name: str = "test_db" + + +@pytest.fixture +def settings(): + return Settings() + +@pytest.fixture +def db(settings: Settings) -> motor.motor_asyncio.AsyncIOMotorDatabase: + client = motor.motor_asyncio.AsyncIOMotorClient(settings.mongodb_dsn) + print(f'client: {client[settings.mongodb_db_name]}') + return client[settings.mongodb_db_name] From 48345a7e190e5282243c033e5e97b0c0ed3d09b4 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Wed, 9 Oct 2024 13:39:28 -0400 Subject: [PATCH 03/46] Add beamline api tests --- src/nsls2api/tests/api/test_beamline_api.py | 43 +++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/nsls2api/tests/api/test_beamline_api.py diff --git a/src/nsls2api/tests/api/test_beamline_api.py b/src/nsls2api/tests/api/test_beamline_api.py new file mode 100644 index 00000000..858c9a0e --- /dev/null +++ b/src/nsls2api/tests/api/test_beamline_api.py @@ -0,0 +1,43 @@ +import pytest +from httpx import AsyncClient +from nsls2api.models.beamlines import ServiceAccounts, Beamline +from nsls2api.models.validation_error import ValidationError + + +async def test_get_beamline_service_accounts(test_client: AsyncClient): + response = await test_client.get("/v1/beamline/tst/service-accounts") + response_json = response.json() + assert response.status_code == 200 + + # Make sure we can create a ServiceAccounts object from the response + accounts = ServiceAccounts(**response_json) + assert accounts.workflow == "workflow-tst" + assert accounts.ioc == "softioc-tst" + assert accounts.bluesky == "bluesky-tst" + assert accounts.epics_services == "epics-services-tst" + assert accounts.operator == "xf31id" + assert accounts.lsdc is None or accounts.lsdc == "" + + +async def test_get_beamline_lowercase(test_client: AsyncClient): + response = await test_client.get("/v1/beamline/tst") + response_json = response.json() + assert response.status_code == 200 + + # Make sure we can create a Beamline object from the response + beamline = Beamline(**response_json) + assert beamline.name == "TST" + +async def test_get_beamline_uppercase(test_client: AsyncClient): + response = await test_client.get("/v1/beamline/TST") + response_json = response.json() + assert response.status_code == 200 + + # Make sure we can create a Beamline object from the response + beamline = Beamline(**response_json) + assert beamline.name == "TST" + +async def test_get_beamline_directory_skeleton(test_client: AsyncClient): + response = await test_client.get("/v1/beamline/tst/directory-skeleton") + response_json = response.json() + assert response.status_code == 200 From 6e592a2d386bbb41f5e7deda4824b42e9547400b Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Wed, 9 Oct 2024 13:40:47 -0400 Subject: [PATCH 04/46] Add asgi-lifespan and pytest-asyncio for testing --- requirements-dev.in | 2 ++ requirements-dev.txt | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/requirements-dev.in b/requirements-dev.in index 80a28c92..58d96260 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -1,8 +1,10 @@ +asgi-lifespan asyncer black bunnet ipython locust pytest +pytest-asyncio ruff textual-dev \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index abdbb260..48031e00 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,6 +15,8 @@ annotated-types==0.7.0 # via pydantic anyio==4.4.0 # via asyncer +asgi-lifespan==2.1.0 + # via -r requirements-dev.in asttokens==2.4.1 # via stack-data asyncer==0.0.8 @@ -156,6 +158,10 @@ pygments==2.18.0 pymongo==4.8.0 # via bunnet pytest==8.3.3 + # via + # -r requirements-dev.in + # pytest-asyncio +pytest-asyncio==0.24.0 # via -r requirements-dev.in pyzmq==26.2.0 # via locust @@ -174,7 +180,9 @@ setuptools==75.1.0 six==1.16.0 # via asttokens sniffio==1.3.1 - # via anyio + # via + # anyio + # asgi-lifespan stack-data==0.6.3 # via ipython textual==0.79.1 From 56ee0223ecdacecbcef692511fae90e81a85982a Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 08:12:56 -0400 Subject: [PATCH 05/46] Added pyright to dev requirements --- requirements-dev.in | 1 + requirements-dev.txt | 45 +++++++++++++++++++++++++------------------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/requirements-dev.in b/requirements-dev.in index 58d96260..f49bf59e 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -4,6 +4,7 @@ black bunnet ipython locust +pyright pytest pytest-asyncio ruff diff --git a/requirements-dev.txt b/requirements-dev.txt index 48031e00..1da0ff23 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: # uv pip compile requirements-dev.in -o requirements-dev.txt -aiohappyeyeballs==2.4.0 +aiohappyeyeballs==2.4.3 # via aiohttp -aiohttp==3.10.5 +aiohttp==3.10.9 # via # aiohttp-jinja2 # textual-dev @@ -13,7 +13,7 @@ aiosignal==1.3.1 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.6.0 # via asyncer asgi-lifespan==2.1.0 # via -r requirements-dev.in @@ -23,7 +23,7 @@ asyncer==0.0.8 # via -r requirements-dev.in attrs==24.2.0 # via aiohttp -black==24.8.0 +black==24.10.0 # via -r requirements-dev.in blinker==1.8.2 # via flask @@ -35,7 +35,7 @@ certifi==2024.8.30 # via # geventhttpclient # requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via requests click==8.1.7 # via @@ -47,7 +47,7 @@ configargparse==1.7 # via locust decorator==5.1.1 # via ipython -dnspython==2.6.1 +dnspython==2.7.0 # via pymongo executing==2.1.0 # via stack-data @@ -64,13 +64,13 @@ frozenlist==1.4.1 # via # aiohttp # aiosignal -gevent==24.2.1 +gevent==24.10.1 # via # geventhttpclient # locust geventhttpclient==2.3.1 # via locust -greenlet==3.1.0 +greenlet==3.1.1 # via gevent idna==3.10 # via @@ -79,7 +79,7 @@ idna==3.10 # yarl iniconfig==2.0.0 # via pytest -ipython==8.27.0 +ipython==8.28.0 # via -r requirements-dev.in itsdangerous==2.2.0 # via flask @@ -94,14 +94,14 @@ lazy-model==0.2.0 # via bunnet linkify-it-py==2.0.3 # via markdown-it-py -locust==2.31.6 +locust==2.31.8 # via -r requirements-dev.in markdown-it-py==3.0.0 # via # mdit-py-plugins # rich # textual -markupsafe==2.1.5 +markupsafe==3.0.1 # via # jinja2 # werkzeug @@ -121,6 +121,8 @@ multidict==6.1.0 # yarl mypy-extensions==1.0.0 # via black +nodeenv==1.9.1 + # via pyright packaging==24.1 # via # black @@ -131,14 +133,16 @@ pathspec==0.12.1 # via black pexpect==4.9.0 # via ipython -platformdirs==4.3.4 +platformdirs==4.3.6 # via # black # textual pluggy==1.5.0 # via pytest -prompt-toolkit==3.0.47 +prompt-toolkit==3.0.48 # via ipython +propcache==0.2.0 + # via yarl psutil==6.0.0 # via locust ptyprocess==0.7.0 @@ -155,8 +159,10 @@ pygments==2.18.0 # via # ipython # rich -pymongo==4.8.0 +pymongo==4.10.1 # via bunnet +pyright==1.1.384 + # via -r requirements-dev.in pytest==8.3.3 # via # -r requirements-dev.in @@ -167,11 +173,11 @@ pyzmq==26.2.0 # via locust requests==2.32.3 # via locust -rich==13.8.1 +rich==13.9.2 # via # textual # textual-serve -ruff==0.6.5 +ruff==0.6.9 # via -r requirements-dev.in setuptools==75.1.0 # via @@ -185,7 +191,7 @@ sniffio==1.3.1 # asgi-lifespan stack-data==0.6.3 # via ipython -textual==0.79.1 +textual==0.82.0 # via # textual-dev # textual-serve @@ -203,6 +209,7 @@ typing-extensions==4.12.2 # via # pydantic # pydantic-core + # pyright # textual # textual-dev uc-micro-py==1.0.3 @@ -218,9 +225,9 @@ werkzeug==3.0.4 # flask # flask-login # locust -yarl==1.11.1 +yarl==1.14.0 # via aiohttp zope-event==5.0 # via gevent -zope-interface==7.0.3 +zope-interface==7.1.0 # via gevent From 01ed1d849910a7ea1724a19e5e259486aff52e56 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 11:35:06 -0400 Subject: [PATCH 06/46] upgrade dependencies --- requirements.txt | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/requirements.txt b/requirements.txt index edb72547..d940d497 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ aiofiles==24.1.0 # via -r requirements.in annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.6.0 # via # httpx # starlette @@ -16,7 +16,7 @@ asgi-correlation-id==4.3.3 # via -r requirements.in async-timeout==4.0.3 # via httpx-socks -beanie==1.26.0 +beanie==1.27.0 # via -r requirements.in certifi==2024.8.30 # via @@ -31,13 +31,13 @@ click==8.1.7 # uvicorn decorator==5.1.1 # via gssapi -dnspython==2.6.1 +dnspython==2.7.0 # via pymongo -faker==28.4.1 +faker==30.3.0 # via -r requirements.in -fastapi==0.114.2 +fastapi==0.115.0 # via -r requirements.in -gssapi==1.8.3 +gssapi==1.9.0 # via n2snusertools gunicorn==23.0.0 # via -r requirements.in @@ -45,7 +45,7 @@ h11==0.14.0 # via # httpcore # uvicorn -httpcore==1.0.5 +httpcore==1.0.6 # via # httpx # httpx-socks @@ -76,7 +76,7 @@ markdown-it-py==3.0.0 # mdit-py-plugins # rich # textual -markupsafe==2.1.5 +markupsafe==3.0.1 # via # jinja2 # werkzeug @@ -84,7 +84,7 @@ mdit-py-plugins==0.4.2 # via markdown-it-py mdurl==0.1.2 # via markdown-it-py -motor==3.5.1 +motor==3.6.0 # via beanie n2snusertools==0.3.7 # via -r requirements.in @@ -92,7 +92,7 @@ packaging==24.1 # via gunicorn passlib==1.7.4 # via -r requirements.in -platformdirs==4.3.4 +platformdirs==4.3.6 # via textual prettytable==3.11.0 # via n2snusertools @@ -113,19 +113,19 @@ pydantic-settings==2.5.2 # via -r requirements.in pygments==2.18.0 # via rich -pymongo==4.8.0 +pymongo==4.9.2 # via motor python-dateutil==2.9.0.post0 # via faker python-dotenv==1.0.1 # via pydantic-settings -python-multipart==0.0.9 +python-multipart==0.0.12 # via -r requirements.in -python-socks==2.5.1 +python-socks==2.5.3 # via httpx-socks pyyaml==6.0.2 # via n2snusertools -rich==13.8.1 +rich==13.9.2 # via # -r requirements.in # textual @@ -136,7 +136,7 @@ six==1.16.0 # via python-dateutil slack-bolt==1.20.1 # via -r requirements.in -slack-sdk==3.33.0 +slack-sdk==3.33.1 # via # -r requirements.in # slack-bolt @@ -144,11 +144,11 @@ sniffio==1.3.1 # via # anyio # httpx -starlette==0.38.5 +starlette==0.38.6 # via # asgi-correlation-id # fastapi -textual==0.79.1 +textual==0.83.0 # via -r requirements.in toml==0.10.2 # via beanie @@ -156,6 +156,8 @@ typer==0.12.5 # via -r requirements.in typing-extensions==4.12.2 # via + # beanie + # faker # fastapi # pydantic # pydantic-core @@ -165,7 +167,7 @@ uc-micro-py==1.0.3 # via linkify-it-py uuid==1.30 # via -r requirements.in -uvicorn==0.30.6 +uvicorn==0.31.1 # via -r requirements.in wcwidth==0.2.13 # via prettytable From 4ef50802a0586b4352bf8c6d047f227c134613a3 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 11:35:20 -0400 Subject: [PATCH 07/46] Add initial infrastructure tests --- src/nsls2api/tests/api/conftest.py | 6 ++-- src/nsls2api/tests/conftest.py | 28 +++++++++---------- src/nsls2api/tests/infrastructure/__init__.py | 0 .../infrastructure/test_mongodb_setup.py | 19 +++++++++++++ 4 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 src/nsls2api/tests/infrastructure/__init__.py create mode 100644 src/nsls2api/tests/infrastructure/test_mongodb_setup.py diff --git a/src/nsls2api/tests/api/conftest.py b/src/nsls2api/tests/api/conftest.py index 73e983e8..9ad3a07e 100644 --- a/src/nsls2api/tests/api/conftest.py +++ b/src/nsls2api/tests/api/conftest.py @@ -1,15 +1,15 @@ import pytest from asgi_lifespan import LifespanManager -from httpx import AsyncClient +from httpx import AsyncClient, ASGITransport from nsls2api.main import app from nsls2api.models import facilities, jobs, apikeys, proposals, beamlines, cycles, proposal_types -@pytest.fixture() +@pytest.fixture(scope="session", autouse=True) async def test_client(db): async with LifespanManager(app, startup_timeout=100, shutdown_timeout=100): server_name = "http://localhost" - async with AsyncClient(app=app, base_url=server_name, follow_redirects=True) as client: + async with AsyncClient(transport=ASGITransport(app=app), base_url=server_name, follow_redirects=True) as client: yield client # diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py index c720f6fa..912d2aad 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -1,19 +1,19 @@ -import motor.motor_asyncio +import asyncio + import pytest +from pydantic import MongoDsn from pydantic_settings import BaseSettings +from nsls2api.infrastructure.mongodb_setup import create_connection_string, init_connection -class Settings(BaseSettings): - mongodb_dsn: str = "mongodb://localhost:27017/test_db" - mongodb_db_name: str = "test_db" - - -@pytest.fixture -def settings(): - return Settings() +@pytest.fixture(scope="session") +def event_loop(): + return asyncio.get_event_loop() -@pytest.fixture -def db(settings: Settings) -> motor.motor_asyncio.AsyncIOMotorDatabase: - client = motor.motor_asyncio.AsyncIOMotorClient(settings.mongodb_dsn) - print(f'client: {client[settings.mongodb_db_name]}') - return client[settings.mongodb_db_name] +@pytest.fixture(scope="session", autouse=True) +async def db(): + mongodb_dsn = create_connection_string(host="localhost", port=27017, db_name="test_db") + await init_connection(mongodb_dsn.unicode_string()) + # client = motor.motor_asyncio.AsyncIOMotorClient(settings.mongodb_dsn) + # print(f'client: {client[settings.mongodb_db_name]}') + # return client[settings.mongodb_db_name] diff --git a/src/nsls2api/tests/infrastructure/__init__.py b/src/nsls2api/tests/infrastructure/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/nsls2api/tests/infrastructure/test_mongodb_setup.py b/src/nsls2api/tests/infrastructure/test_mongodb_setup.py new file mode 100644 index 00000000..657ff2e8 --- /dev/null +++ b/src/nsls2api/tests/infrastructure/test_mongodb_setup.py @@ -0,0 +1,19 @@ +import pytest +from pydantic import MongoDsn + +from nsls2api.infrastructure.mongodb_setup import create_connection_string + +def test_create_connection_string(): + mongo_dsn = create_connection_string(host="testhost", + port=27017, + db_name="pytest", + username="testuser", + password="testpassword") + assert mongo_dsn == MongoDsn.build( + scheme="mongodb", + host="testhost", + port=27017, + path="pytest", + username="testuser", + password="testpassword", + ) From 98138be9c75db32aec1e58cbdc7936ea4b82fa4b Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 16:55:41 -0400 Subject: [PATCH 08/46] Add blank-ish .env file for pytest --- src/nsls2api/pytest.env | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/nsls2api/pytest.env diff --git a/src/nsls2api/pytest.env b/src/nsls2api/pytest.env new file mode 100644 index 00000000..ae48142f --- /dev/null +++ b/src/nsls2api/pytest.env @@ -0,0 +1,16 @@ +PASS_API_KEY= +PASS_API_URL=https://passservices.bnl.gov/passapi +ACTIVE_DIRECTORY_SERVER= +ACTIVE_DIRECTORY_SERVER_LIST= +N2SN_USER_SEARCH= +N2SN_GROUP_SEARCH= +BNLROOT_CA_CERTS_FILE= +MONGODB_DSN=mongodb://localhost:27017/nsls2core-pytest +USE_SOCKS_PROXY=False +SOCKS_PROXY= +SLACK_BOT_TOKEN= +SUPERADMIN_SLACK_USER_TOKEN= +SLACK_SIGNING_SECRET= +NSLS2_WORKSPACE_TEAM_ID= +UPS_USERNAME= +UPS_PASSWORD= \ No newline at end of file From 4f254e305d0422611e8ea0804742c3fe2f7103aa Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 16:56:05 -0400 Subject: [PATCH 09/46] Refactor so we don't connect to slack on import --- src/nsls2api/services/slack_service.py | 28 ++++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/nsls2api/services/slack_service.py b/src/nsls2api/services/slack_service.py index 22d1bc1c..2b61fcf7 100644 --- a/src/nsls2api/services/slack_service.py +++ b/src/nsls2api/services/slack_service.py @@ -7,12 +7,18 @@ from nsls2api.models.slack_models import SlackBot settings = get_settings() -app = App(token=settings.slack_bot_token, signing_secret=settings.slack_signing_secret) -super_app = App( - token=settings.superadmin_slack_user_token, - signing_secret=settings.slack_signing_secret, -) +# def get_super_app() -> App: +# return App( +# token=settings.superadmin_slack_user_token, +# signing_secret=settings.slack_signing_secret, +# ) + +def get_boring_app() -> App: + return App( + token=settings.slack_bot_token, + signing_secret=settings.slack_signing_secret, + ) def get_bot_details() -> SlackBot: """ @@ -21,7 +27,7 @@ def get_bot_details() -> SlackBot: Returns: SlackBot: An instance of the SlackBot class containing the bot details. """ - response = app.client.auth_test() + response = get_boring_app().client.auth_test() return SlackBot( username=response.data["user"], @@ -43,7 +49,7 @@ def get_channel_members(channel_id: str) -> list[str]: list[str]: A list of member IDs in the channel. """ try: - response = app.client.conversations_members(channel=channel_id) + response = get_boring_app().client.conversations_members(channel=channel_id) except SlackApiError as error: logger.exception(error) return [] @@ -98,7 +104,7 @@ async def is_channel_private(channel_id: str) -> bool: Returns: bool: True if the channel is private, False otherwise. """ - response = await app.client.conversations_info(channel=channel_id) + response = get_boring_app().client.conversations_info(channel=channel_id) return response.data["channel"]["is_private"] @@ -211,7 +217,7 @@ def rename_channel(name: str, new_name: str) -> str | None: if channel_id is None: raise Exception(f"Channel {name} not found.") - response = app.client.conversations_rename(channel=channel_id, name=new_name) + response = get_boring_app().client.conversations_rename(channel=channel_id, name=new_name) if response.data["ok"] is not True: raise Exception(f"Failed to rename channel {name} to {new_name}") @@ -232,7 +238,7 @@ def lookup_userid_by_email(email: str) -> str | None: Returns: str | None: The user ID if found, None otherwise. """ - response = app.client.users_lookupByEmail(email=email) + response = get_boring_app().client.users_lookupByEmail(email=email) if response.data["ok"] is True: return response.data["user"]["id"] @@ -247,7 +253,7 @@ def lookup_username_by_email(email: str) -> str | None: Returns: str | None: The username associated with the email address, or None if not found. """ - response = app.client.users_lookupByEmail(email=email) + response = get_boring_app().client.users_lookupByEmail(email=email) if response.data["ok"] is True: return response.data["user"]["name"] From 41f88dbb12096ac13ab1631cf16d8a5de613eaf9 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 16:56:32 -0400 Subject: [PATCH 10/46] Settings checks for running in pytest --- src/nsls2api/infrastructure/config.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/nsls2api/infrastructure/config.py b/src/nsls2api/infrastructure/config.py index 5e988e3d..510a846d 100644 --- a/src/nsls2api/infrastructure/config.py +++ b/src/nsls2api/infrastructure/config.py @@ -1,3 +1,4 @@ +import os from functools import lru_cache from pathlib import Path @@ -39,16 +40,17 @@ class Settings(BaseSettings): settings.bnlroot_ca_certs_file = "/path/to/ca_certs.pem" """ - pass_api_key: str - pass_api_url: HttpUrl = "https://passservices.bnl.gov/passapi" + # Active Directory settings active_directory_server: str active_directory_server_list: str n2sn_user_search: str n2sn_group_search: str bnlroot_ca_certs_file: str + + # MongoDB settings mongodb_dsn: MongoDsn - # mongodb_dsn: MongoDsn = "mongodb://localhost:27017/nsls2core-test" + # Proxy settings use_socks_proxy: bool = False socks_proxy: str @@ -58,6 +60,10 @@ class Settings(BaseSettings): slack_signing_secret: str | None = "" nsls2_workspace_team_id: str | None = "" + # PASS settings + pass_api_key: str + pass_api_url: HttpUrl = "https://passservices.bnl.gov/passapi" + # Universal Proposal System universal_proposal_system_api_url: HttpUrl = "https://ups.servicenowservices.com/api" universal_proposal_system_api_user: str | None = "" @@ -76,4 +82,9 @@ def get_settings() -> Settings: :returns: The dictionary of current settings. """ - return Settings() \ No newline at end of file + if os.environ.get("PYTEST_VERSION") is not None: + settings = Settings(_env_file=str(Path(__file__).parent.parent / "pytest.env")) + else: + settings = Settings() + + return settings \ No newline at end of file From 192412e141e30f34ee323bd65e365a3d4e523f66 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 16:57:52 -0400 Subject: [PATCH 11/46] Remove extra check for local mongodb If we want to add a development mode, it should be added in the config::get_settings() method (similar to where we are checking for running under pytest) --- src/nsls2api/infrastructure/app_setup.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/nsls2api/infrastructure/app_setup.py b/src/nsls2api/infrastructure/app_setup.py index b5752ea2..ebbdfb03 100644 --- a/src/nsls2api/infrastructure/app_setup.py +++ b/src/nsls2api/infrastructure/app_setup.py @@ -1,4 +1,5 @@ import asyncio +import os from contextlib import asynccontextmanager from nsls2api.infrastructure import mongodb_setup @@ -10,18 +11,11 @@ local_development_mode = False - @asynccontextmanager async def app_lifespan(_): - if local_development_mode: - # Default to local mongodb with default port - # and no authentication for development. - development_dsn = mongodb_setup.create_connection_string( - host="localhost", port=27017, db_name="nsls2core-development" - ) - await mongodb_setup.init_connection(development_dsn.unicode_string()) - else: - await mongodb_setup.init_connection(settings.mongodb_dsn.unicode_string()) + + # Initialize the MongoDB connection + await mongodb_setup.init_connection(settings.mongodb_dsn.unicode_string()) # Create a shared httpx client httpx_client_wrapper.start() From b645a0eb16767ea0cf388340aa076a093e2b186b Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 16:58:12 -0400 Subject: [PATCH 12/46] Populate database and alter tests to match --- src/nsls2api/tests/api/test_beamline_api.py | 24 +++++------ src/nsls2api/tests/conftest.py | 48 ++++++++++++++++----- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/nsls2api/tests/api/test_beamline_api.py b/src/nsls2api/tests/api/test_beamline_api.py index 858c9a0e..f2410055 100644 --- a/src/nsls2api/tests/api/test_beamline_api.py +++ b/src/nsls2api/tests/api/test_beamline_api.py @@ -1,43 +1,41 @@ import pytest from httpx import AsyncClient from nsls2api.models.beamlines import ServiceAccounts, Beamline -from nsls2api.models.validation_error import ValidationError - async def test_get_beamline_service_accounts(test_client: AsyncClient): - response = await test_client.get("/v1/beamline/tst/service-accounts") + response = await test_client.get("/v1/beamline/zzz/service-accounts") response_json = response.json() assert response.status_code == 200 # Make sure we can create a ServiceAccounts object from the response accounts = ServiceAccounts(**response_json) - assert accounts.workflow == "workflow-tst" - assert accounts.ioc == "softioc-tst" - assert accounts.bluesky == "bluesky-tst" - assert accounts.epics_services == "epics-services-tst" - assert accounts.operator == "xf31id" + assert accounts.workflow == "testy-mctestface-workflow" + assert accounts.ioc == "testy-mctestface-ioc" + assert accounts.bluesky == "testy-mctestface-bluesky" + assert accounts.epics_services == "testy-mctestface-epics-services" + assert accounts.operator == "testy-mctestface-xf66id6" assert accounts.lsdc is None or accounts.lsdc == "" async def test_get_beamline_lowercase(test_client: AsyncClient): - response = await test_client.get("/v1/beamline/tst") + response = await test_client.get("/v1/beamline/zzz") response_json = response.json() assert response.status_code == 200 # Make sure we can create a Beamline object from the response beamline = Beamline(**response_json) - assert beamline.name == "TST" + assert beamline.name == "ZZZ" async def test_get_beamline_uppercase(test_client: AsyncClient): - response = await test_client.get("/v1/beamline/TST") + response = await test_client.get("/v1/beamline/ZZZ") response_json = response.json() assert response.status_code == 200 # Make sure we can create a Beamline object from the response beamline = Beamline(**response_json) - assert beamline.name == "TST" + assert beamline.name == "ZZZ" async def test_get_beamline_directory_skeleton(test_client: AsyncClient): - response = await test_client.get("/v1/beamline/tst/directory-skeleton") + response = await test_client.get("/v1/beamline/zzz/directory-skeleton") response_json = response.json() assert response.status_code == 200 diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py index 912d2aad..fd1257a7 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -1,19 +1,47 @@ import asyncio - import pytest -from pydantic import MongoDsn -from pydantic_settings import BaseSettings -from nsls2api.infrastructure.mongodb_setup import create_connection_string, init_connection +from nsls2api.infrastructure.config import get_settings +from nsls2api.infrastructure.mongodb_setup import init_connection +from nsls2api import models +from nsls2api.models.beamlines import Beamline, ServiceAccounts +from nsls2api.models.facilities import Facility +from nsls2api.services.beamline_service import service_accounts + @pytest.fixture(scope="session") def event_loop(): return asyncio.get_event_loop() -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope="session") async def db(): - mongodb_dsn = create_connection_string(host="localhost", port=27017, db_name="test_db") - await init_connection(mongodb_dsn.unicode_string()) - # client = motor.motor_asyncio.AsyncIOMotorClient(settings.mongodb_dsn) - # print(f'client: {client[settings.mongodb_db_name]}') - # return client[settings.mongodb_db_name] + + settings = get_settings() + await init_connection(settings.mongodb_dsn.unicode_string()) + + # 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") + await facility.insert() + + yield + + # Cleanup the database collections + for model in models.all_models: + print(f"dropping {model}") + await model.get_motor_collection().drop() + await model.get_motor_collection().drop_indexes() \ No newline at end of file From b25fef031957b07d70d140c74179ee86fe11ce16 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 17:21:06 -0400 Subject: [PATCH 13/46] Create ci.yml --- .github/workflows/ci.yml | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..1ff26238 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - "3.12" + + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install uv + uv pip install -r requirements.txt + + - name: Install + run: | + pip install . + + # This requirements-dev.txt is too much for this step - will trim later. + - name: Install test requirements + run: | + uv pip install -r requirements-dev.txt + + - name: Start MongoDB + uses: supercharge/mongodb-github-action@1.11.0 + with: + mongodb-version: "8.0.0" + + - name: Test with pytest + run: | + pytest -v + From 821a3cb2d855ff4e08c7352ef8b4ea14b3b4bed6 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 17:23:57 -0400 Subject: [PATCH 14/46] fix indentation error --- .github/workflows/ci.yml | 76 ++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ff26238..866e0b7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,42 +10,42 @@ on: jobs: build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: - - "3.12" - - steps: - - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - "3.12" + + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install uv + uv pip install -r requirements.txt + + - name: Install + run: | + pip install . + + # This requirements-dev.txt is too much for this step - will trim later. + - name: Install test requirements + run: | + uv pip install -r requirements-dev.txt + + - name: Start MongoDB + uses: supercharge/mongodb-github-action@1.11.0 + with: + mongodb-version: "8.0.0" + + - name: Test with pytest + run: | + pytest -v - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install uv - uv pip install -r requirements.txt - - - name: Install - run: | - pip install . - - # This requirements-dev.txt is too much for this step - will trim later. - - name: Install test requirements - run: | - uv pip install -r requirements-dev.txt - - - name: Start MongoDB - uses: supercharge/mongodb-github-action@1.11.0 - with: - mongodb-version: "8.0.0" - - - name: Test with pytest - run: | - pytest -v - From 926b32fb9a1e6c71cc0f0f60f394a8b64287e718 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 17:26:09 -0400 Subject: [PATCH 15/46] Revert to boring stuff for now --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 866e0b7a..dcbb8e16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,8 +28,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install uv - uv pip install -r requirements.txt + pip install -r requirements.txt - name: Install run: | @@ -38,7 +37,7 @@ jobs: # This requirements-dev.txt is too much for this step - will trim later. - name: Install test requirements run: | - uv pip install -r requirements-dev.txt + pip install -r requirements-dev.txt - name: Start MongoDB uses: supercharge/mongodb-github-action@1.11.0 From 99124d83b5f51d3d6666b5dbe8e17784a8f1868b Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 17:30:18 -0400 Subject: [PATCH 16/46] Install krb5 (for n2snusertools) --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcbb8e16..02a0e881 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,10 @@ jobs: steps: - uses: actions/checkout@v4 + # Install Kerberos dependencies (for N2SNUserTools) + - name: Install krb5-devel + run: sudo apt-get -y install libkrb5-dev + - name: Setup Python uses: actions/setup-python@v5 with: From b5c99833e6833773bbf93228e948a2fd5c45175e Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Thu, 10 Oct 2024 17:32:19 -0400 Subject: [PATCH 17/46] install wheel early as well --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02a0e881..eb631b00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - name: Install dependencies run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip wheel pip install -r requirements.txt - name: Install From bce1b240d4c95ef4580dfd13e8a2185707ad5289 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Fri, 11 Oct 2024 16:36:40 -0400 Subject: [PATCH 18/46] Add hook to ensure we captialize beamline name before inserting Document --- src/nsls2api/models/beamlines.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/nsls2api/models/beamlines.py b/src/nsls2api/models/beamlines.py index abb9db1d..b4fd790d 100644 --- a/src/nsls2api/models/beamlines.py +++ b/src/nsls2api/models/beamlines.py @@ -1,6 +1,6 @@ import datetime from typing import Optional - +from beanie import Insert, before_event import beanie import pydantic @@ -214,6 +214,10 @@ class Beamline(beanie.Document): default_factory=datetime.datetime.now ) + @before_event(Insert) + def uppercase_name(self): + self.name = self.name.upper() + class Settings: name = "beamlines" keep_nulls = False From 6a22d66367c02fa0cf6af6c6f571bdec0ed4f934 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Fri, 11 Oct 2024 17:53:46 -0400 Subject: [PATCH 19/46] Add cycle to test database --- src/nsls2api/tests/conftest.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py index fd1257a7..623284ac 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -5,6 +5,7 @@ from nsls2api.infrastructure.mongodb_setup import init_connection from nsls2api import models from nsls2api.models.beamlines import Beamline, ServiceAccounts +from nsls2api.models.cycles import Cycle from nsls2api.models.facilities import Facility from nsls2api.services.beamline_service import service_accounts @@ -38,6 +39,15 @@ async def db(): pass_facility_id="NSLS-II") await facility.insert() + cycle = Cycle(name="1999-1", facility="nsls2", year="1999", + start_date="1999-01-01T00:00:00.000+00:00", + end_date="1999-06-30T00:00:00.000+00:00", + is_current_operating_cycle=True, + pass_description="January - June", + pass_id="111111" + ) + await cycle.insert() + yield # Cleanup the database collections From ee26bfbab1467032f78db195db9e9de0f833599c Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Fri, 11 Oct 2024 18:06:59 -0400 Subject: [PATCH 20/46] Added tests for facility api endpoints --- src/nsls2api/tests/api/test_facility_api.py | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/nsls2api/tests/api/test_facility_api.py diff --git a/src/nsls2api/tests/api/test_facility_api.py b/src/nsls2api/tests/api/test_facility_api.py new file mode 100644 index 00000000..8299843b --- /dev/null +++ b/src/nsls2api/tests/api/test_facility_api.py @@ -0,0 +1,45 @@ +from httpx import AsyncClient + +from nsls2api.api.models.facility_model import FacilityCyclesResponseModel, FacilityCurrentOperatingCycleResponseModel +from nsls2api.api.models.proposal_model import CycleProposalList + + +async def test_get_current_operating_cycle(test_client: AsyncClient): + facility_name = "nsls2" + response = await test_client.get(f"/v1/facility/{facility_name}/cycles/current") + response_json = response.json() + assert response.status_code == 200 + + # This should be returning a FacilityCurrentOperatingCycleResponseModel + current_cycle = FacilityCurrentOperatingCycleResponseModel(**response_json) + assert current_cycle.facility == facility_name + assert current_cycle.cycle == "1999-1" + + +async def test_get_facility_cycles(test_client: AsyncClient): + facility_name = "nsls2" + response = await test_client.get(f"/v1/facility/{facility_name}/cycles") + response_json = response.json() + assert response.status_code == 200 + + # This should be returning a FacilityCyclesResponseModel + facility_cycles = FacilityCyclesResponseModel(**response_json) + assert facility_cycles.facility == facility_name + assert len(facility_cycles.cycles) == 1 + assert facility_cycles.cycles[0] == "1999-1" + + +async def test_get_proposals_for_cycle(test_client: AsyncClient): + facility_name = "nsls2" + cycle_name = "1999-1" + response = await test_client.get(f"/v1/facility/{facility_name}/cycle/{cycle_name}/proposals") + response_json = response.json() + assert response.status_code == 200 + + # This should be returning a CycleProposalList + cycle_proposals = CycleProposalList(**response_json) + assert cycle_proposals.cycle == cycle_name + + # For now we are not returning any proposals + assert len(cycle_proposals.proposals) == 0 + assert cycle_proposals.count == 0 From 0fa636136f03d402c02ef4aa1185f87846f46c9c Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Fri, 11 Oct 2024 18:09:29 -0400 Subject: [PATCH 21/46] Added proposal type into test database --- src/nsls2api/tests/conftest.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py index 623284ac..2c8175cb 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -7,6 +7,7 @@ from nsls2api.models.beamlines import Beamline, ServiceAccounts from nsls2api.models.cycles import Cycle from nsls2api.models.facilities import Facility +from nsls2api.models.proposal_types import ProposalType from nsls2api.services.beamline_service import service_accounts @@ -14,9 +15,9 @@ def event_loop(): return asyncio.get_event_loop() + @pytest.fixture(scope="session") async def db(): - settings = get_settings() await init_connection(settings.mongodb_dsn.unicode_string()) @@ -39,6 +40,7 @@ async def db(): pass_facility_id="NSLS-II") await facility.insert() + # Insert a cycle into the database cycle = Cycle(name="1999-1", facility="nsls2", year="1999", start_date="1999-01-01T00:00:00.000+00:00", end_date="1999-06-30T00:00:00.000+00:00", @@ -48,10 +50,18 @@ async def db(): ) 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() + yield # Cleanup the database collections for model in models.all_models: print(f"dropping {model}") await model.get_motor_collection().drop() - await model.get_motor_collection().drop_indexes() \ No newline at end of file + await model.get_motor_collection().drop_indexes() From 75896901cb3e0ebdf1f0883607f46a51073c34ea Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Mon, 14 Oct 2024 14:30:30 -0400 Subject: [PATCH 22/46] Make types consistent for mongodb DSN --- src/nsls2api/infrastructure/app_setup.py | 2 +- src/nsls2api/infrastructure/mongodb_setup.py | 2 +- src/nsls2api/tests/conftest.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/nsls2api/infrastructure/app_setup.py b/src/nsls2api/infrastructure/app_setup.py index ebbdfb03..e244ef6d 100644 --- a/src/nsls2api/infrastructure/app_setup.py +++ b/src/nsls2api/infrastructure/app_setup.py @@ -15,7 +15,7 @@ async def app_lifespan(_): # Initialize the MongoDB connection - await mongodb_setup.init_connection(settings.mongodb_dsn.unicode_string()) + await mongodb_setup.init_connection(settings.mongodb_dsn) # Create a shared httpx client httpx_client_wrapper.start() diff --git a/src/nsls2api/infrastructure/mongodb_setup.py b/src/nsls2api/infrastructure/mongodb_setup.py index 99932ad3..ccdc0818 100644 --- a/src/nsls2api/infrastructure/mongodb_setup.py +++ b/src/nsls2api/infrastructure/mongodb_setup.py @@ -24,7 +24,7 @@ async def init_connection(mongodb_dsn: MongoDsn): logger.info(f"Attempting to connect to {click.style(str(mongodb_dsn), fg='green')}") client = motor.motor_asyncio.AsyncIOMotorClient( - mongodb_dsn, uuidRepresentation="standard" + mongodb_dsn.unicode_string(), uuidRepresentation="standard" ) await beanie.init_beanie( database=client.get_default_database(), diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py index 2c8175cb..9ac622f6 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -19,7 +19,7 @@ def event_loop(): @pytest.fixture(scope="session") async def db(): settings = get_settings() - await init_connection(settings.mongodb_dsn.unicode_string()) + 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", @@ -63,5 +63,5 @@ async def db(): # Cleanup the database collections for model in models.all_models: print(f"dropping {model}") - await model.get_motor_collection().drop() - await model.get_motor_collection().drop_indexes() + # await model.get_motor_collection().drop() + # await model.get_motor_collection().drop_indexes() From af93df2f2d4085dca923be457c06e6905b1581ad Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Mon, 14 Oct 2024 14:39:33 -0400 Subject: [PATCH 23/46] Add coverage --- .github/workflows/ci.yml | 5 ++++- requirements-dev.in | 3 ++- requirements-dev.txt | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb631b00..84ef5554 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ jobs: strategy: matrix: python-version: + - "3.11" - "3.12" steps: @@ -50,5 +51,7 @@ jobs: - name: Test with pytest run: | - pytest -v + set -vxeuo pipefail + coverage run -m pytest -v + coverage report diff --git a/requirements-dev.in b/requirements-dev.in index f49bf59e..ede68e72 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -2,10 +2,11 @@ asgi-lifespan asyncer black bunnet +coverage ipython locust pyright pytest pytest-asyncio ruff -textual-dev \ No newline at end of file +textual-dev diff --git a/requirements-dev.txt b/requirements-dev.txt index 1da0ff23..308991b6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -45,6 +45,8 @@ click==8.1.7 # textual-dev configargparse==1.7 # via locust +coverage==7.6.3 + # via -r requirements-dev.in decorator==5.1.1 # via ipython dnspython==2.7.0 From df5758bc406627daeab34af1418f488451531a1c Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Mon, 14 Oct 2024 16:01:11 -0400 Subject: [PATCH 24/46] Add python 3.13 to test matrix --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84ef5554..7a5371b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ jobs: python-version: - "3.11" - "3.12" + - "3.13" steps: - uses: actions/checkout@v4 From 5d72465d15ff783bb27c46bd54640ca78f85f909 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Wed, 16 Oct 2024 08:41:14 -0400 Subject: [PATCH 25/46] Reformat and change default asyncio pytest loop scope --- pyproject.toml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 84d42635..f0ea7b7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,23 +4,23 @@ build-backend = "hatchling.build" [project] name = "nsls2api" -dynamic = ["version","dependencies"] +dynamic = ["version", "dependencies"] description = '' readme = "README.md" requires-python = ">=3.9" license = "BSD-3-Clause" authors = [ - { name = "Stuart Campbell", email = "scampbell@bnl.gov" }, + { name = "Stuart Campbell", email = "scampbell@bnl.gov" }, ] classifiers = [ - "Development Status :: 4 - Beta", - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: Implementation :: CPython", + "Development Status :: 4 - Beta", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", ] [project.scripts] @@ -41,7 +41,7 @@ hooks.vcs.version-file = "src/nsls2api/_version.py" [tool.hatch.build.targets.sdist] exclude = [ - "/.github", + "/.github", ] #[tool.hatch.metadata.hooks.requirements_txt.optional-dependencies] @@ -52,7 +52,7 @@ target_version = ['py311'] include = '\.pyi?$' [tool.pytest.ini_options] -asyncio_default_fixture_loop_scope = "function" +asyncio_default_fixture_loop_scope = "session" asyncio_mode = "auto" [tool.ruff] From c145b1631e335fb3f3d1227f3e828e03f27244b1 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Wed, 16 Oct 2024 08:41:35 -0400 Subject: [PATCH 26/46] Mark tests as asyncio --- src/nsls2api/tests/api/test_beamline_api.py | 8 ++++++++ src/nsls2api/tests/api/test_facility_api.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/nsls2api/tests/api/test_beamline_api.py b/src/nsls2api/tests/api/test_beamline_api.py index f2410055..f52f87d8 100644 --- a/src/nsls2api/tests/api/test_beamline_api.py +++ b/src/nsls2api/tests/api/test_beamline_api.py @@ -1,7 +1,10 @@ import pytest from httpx import AsyncClient + from nsls2api.models.beamlines import ServiceAccounts, Beamline + +@pytest.mark.asyncio async def test_get_beamline_service_accounts(test_client: AsyncClient): response = await test_client.get("/v1/beamline/zzz/service-accounts") response_json = response.json() @@ -17,6 +20,7 @@ async def test_get_beamline_service_accounts(test_client: AsyncClient): assert accounts.lsdc is None or accounts.lsdc == "" +@pytest.mark.asyncio async def test_get_beamline_lowercase(test_client: AsyncClient): response = await test_client.get("/v1/beamline/zzz") response_json = response.json() @@ -26,6 +30,8 @@ async def test_get_beamline_lowercase(test_client: AsyncClient): beamline = Beamline(**response_json) assert beamline.name == "ZZZ" + +@pytest.mark.asyncio async def test_get_beamline_uppercase(test_client: AsyncClient): response = await test_client.get("/v1/beamline/ZZZ") response_json = response.json() @@ -35,6 +41,8 @@ async def test_get_beamline_uppercase(test_client: AsyncClient): beamline = Beamline(**response_json) assert beamline.name == "ZZZ" + +@pytest.mark.asyncio async def test_get_beamline_directory_skeleton(test_client: AsyncClient): response = await test_client.get("/v1/beamline/zzz/directory-skeleton") response_json = response.json() diff --git a/src/nsls2api/tests/api/test_facility_api.py b/src/nsls2api/tests/api/test_facility_api.py index 8299843b..ea9c1d6d 100644 --- a/src/nsls2api/tests/api/test_facility_api.py +++ b/src/nsls2api/tests/api/test_facility_api.py @@ -1,9 +1,11 @@ +import pytest from httpx import AsyncClient from nsls2api.api.models.facility_model import FacilityCyclesResponseModel, FacilityCurrentOperatingCycleResponseModel from nsls2api.api.models.proposal_model import CycleProposalList +@pytest.mark.asyncio async def test_get_current_operating_cycle(test_client: AsyncClient): facility_name = "nsls2" response = await test_client.get(f"/v1/facility/{facility_name}/cycles/current") @@ -16,6 +18,7 @@ async def test_get_current_operating_cycle(test_client: AsyncClient): assert current_cycle.cycle == "1999-1" +@pytest.mark.asyncio async def test_get_facility_cycles(test_client: AsyncClient): facility_name = "nsls2" response = await test_client.get(f"/v1/facility/{facility_name}/cycles") @@ -29,6 +32,7 @@ async def test_get_facility_cycles(test_client: AsyncClient): assert facility_cycles.cycles[0] == "1999-1" +@pytest.mark.asyncio async def test_get_proposals_for_cycle(test_client: AsyncClient): facility_name = "nsls2" cycle_name = "1999-1" From 0eea6f761503f3a159895d576255604d5a9a57e2 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Wed, 16 Oct 2024 08:41:43 -0400 Subject: [PATCH 27/46] Reformat --- src/nsls2api/tests/test_home.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nsls2api/tests/test_home.py b/src/nsls2api/tests/test_home.py index 4e97ce17..294e26ff 100644 --- a/src/nsls2api/tests/test_home.py +++ b/src/nsls2api/tests/test_home.py @@ -4,8 +4,8 @@ client = TestClient(app) + def test_healthy_endpoint(): response = client.get("/healthy") assert response.status_code == 200 assert response.text == "OK" - From d2fea12d229acf71b3e01b00c0d6a92ae049ccf4 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Wed, 16 Oct 2024 08:43:00 -0400 Subject: [PATCH 28/46] Update for newest pytest-asyncio event_loop changes --- src/nsls2api/tests/api/conftest.py | 29 +++-------------------------- src/nsls2api/tests/conftest.py | 13 +++---------- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/src/nsls2api/tests/api/conftest.py b/src/nsls2api/tests/api/conftest.py index 9ad3a07e..692df539 100644 --- a/src/nsls2api/tests/api/conftest.py +++ b/src/nsls2api/tests/api/conftest.py @@ -1,36 +1,13 @@ -import pytest +import pytest_asyncio from asgi_lifespan import LifespanManager from httpx import AsyncClient, ASGITransport from nsls2api.main import app -from nsls2api.models import facilities, jobs, apikeys, proposals, beamlines, cycles, proposal_types -@pytest.fixture(scope="session", autouse=True) + +@pytest_asyncio.fixture async def test_client(db): async with LifespanManager(app, startup_timeout=100, shutdown_timeout=100): server_name = "http://localhost" async with AsyncClient(transport=ASGITransport(app=app), base_url=server_name, follow_redirects=True) as client: yield client - -# -# @pytest.fixture(autouse=True) -# async def clean_db(db): -# all_models = [ -# facilities.Facility, -# cycles.Cycle, -# proposal_types.ProposalType, -# beamlines.Beamline, -# proposals.Proposal, -# apikeys.ApiKey, -# apikeys.ApiUser, -# jobs.BackgroundJob, -# ] -# yield None -# -# for model in all_models: -# print(f"dropping {model}") -# # await model.get_motor_collection().drop() -# # await model.get_motor_collection().drop_indexes() -# - - diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py index 9ac622f6..7eef0b39 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -1,22 +1,15 @@ -import asyncio -import pytest +import pytest_asyncio +from nsls2api import models from nsls2api.infrastructure.config import get_settings from nsls2api.infrastructure.mongodb_setup import init_connection -from nsls2api import models from nsls2api.models.beamlines import Beamline, ServiceAccounts from nsls2api.models.cycles import Cycle from nsls2api.models.facilities import Facility from nsls2api.models.proposal_types import ProposalType -from nsls2api.services.beamline_service import service_accounts - - -@pytest.fixture(scope="session") -def event_loop(): - return asyncio.get_event_loop() -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session", loop_scope="session") async def db(): settings = get_settings() await init_connection(settings.mongodb_dsn) From c900abbf22b7b7510149ec115b60f91d3637004b Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Wed, 16 Oct 2024 08:52:33 -0400 Subject: [PATCH 29/46] Add back scope that was accidentally removed --- src/nsls2api/tests/api/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nsls2api/tests/api/conftest.py b/src/nsls2api/tests/api/conftest.py index 692df539..8d6405b5 100644 --- a/src/nsls2api/tests/api/conftest.py +++ b/src/nsls2api/tests/api/conftest.py @@ -5,7 +5,7 @@ from nsls2api.main import app -@pytest_asyncio.fixture +@pytest_asyncio.fixture(scope="function", autouse=True, loop_scope="function") async def test_client(db): async with LifespanManager(app, startup_timeout=100, shutdown_timeout=100): server_name = "http://localhost" From 77a64a6ab4ca1e3fa03ae4eb1e88f56eec007916 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Wed, 16 Oct 2024 08:52:44 -0400 Subject: [PATCH 30/46] cleanup test database --- src/nsls2api/tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py index 7eef0b39..3e803019 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -56,5 +56,5 @@ async def db(): # Cleanup the database collections for model in models.all_models: print(f"dropping {model}") - # await model.get_motor_collection().drop() - # await model.get_motor_collection().drop_indexes() + await model.get_motor_collection().drop() + await model.get_motor_collection().drop_indexes() From b88f6e2d55f8461423f76f6d4f50a6a90fb624ce Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Wed, 16 Oct 2024 08:53:06 -0400 Subject: [PATCH 31/46] Update requirements --- requirements-dev.txt | 20 +++++++++----------- requirements.txt | 10 +++++----- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 308991b6..04648910 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.4.3 # via aiohttp -aiohttp==3.10.9 +aiohttp==3.10.10 # via # aiohttp-jinja2 # textual-dev @@ -13,7 +13,7 @@ aiosignal==1.3.1 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.6.0 +anyio==4.6.2.post1 # via asyncer asgi-lifespan==2.1.0 # via -r requirements-dev.in @@ -66,10 +66,8 @@ frozenlist==1.4.1 # via # aiohttp # aiosignal -gevent==24.10.1 - # via - # geventhttpclient - # locust +gevent==24.10.2 + # via geventhttpclient geventhttpclient==2.3.1 # via locust greenlet==3.1.1 @@ -96,7 +94,7 @@ lazy-model==0.2.0 # via bunnet linkify-it-py==2.0.3 # via markdown-it-py -locust==2.31.8 +locust==2.32.0 # via -r requirements-dev.in markdown-it-py==3.0.0 # via @@ -163,7 +161,7 @@ pygments==2.18.0 # rich pymongo==4.10.1 # via bunnet -pyright==1.1.384 +pyright==1.1.385 # via -r requirements-dev.in pytest==8.3.3 # via @@ -181,7 +179,7 @@ rich==13.9.2 # textual-serve ruff==0.6.9 # via -r requirements-dev.in -setuptools==75.1.0 +setuptools==75.2.0 # via # zope-event # zope-interface @@ -193,7 +191,7 @@ sniffio==1.3.1 # asgi-lifespan stack-data==0.6.3 # via ipython -textual==0.82.0 +textual==0.83.0 # via # textual-dev # textual-serve @@ -227,7 +225,7 @@ werkzeug==3.0.4 # flask # flask-login # locust -yarl==1.14.0 +yarl==1.15.3 # via aiohttp zope-event==5.0 # via gevent diff --git a/requirements.txt b/requirements.txt index d940d497..96d0ddea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ aiofiles==24.1.0 # via -r requirements.in annotated-types==0.7.0 # via pydantic -anyio==4.6.0 +anyio==4.6.2.post1 # via # httpx # starlette @@ -33,9 +33,9 @@ decorator==5.1.1 # via gssapi dnspython==2.7.0 # via pymongo -faker==30.3.0 +faker==30.4.0 # via -r requirements.in -fastapi==0.115.0 +fastapi==0.115.2 # via -r requirements.in gssapi==1.9.0 # via n2snusertools @@ -144,7 +144,7 @@ sniffio==1.3.1 # via # anyio # httpx -starlette==0.38.6 +starlette==0.40.0 # via # asgi-correlation-id # fastapi @@ -167,7 +167,7 @@ uc-micro-py==1.0.3 # via linkify-it-py uuid==1.30 # via -r requirements.in -uvicorn==0.31.1 +uvicorn==0.32.0 # via -r requirements.in wcwidth==0.2.13 # via prettytable From 007b779483993fb1653ea2d289701e38a7c245d4 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Mon, 21 Oct 2024 14:41:10 -0400 Subject: [PATCH 32/46] Fix default values in method to match type hints --- src/nsls2api/infrastructure/mongodb_setup.py | 8 +++++++- src/nsls2api/tests/infrastructure/test_mongodb_setup.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/nsls2api/infrastructure/mongodb_setup.py b/src/nsls2api/infrastructure/mongodb_setup.py index ccdc0818..1fb95e11 100644 --- a/src/nsls2api/infrastructure/mongodb_setup.py +++ b/src/nsls2api/infrastructure/mongodb_setup.py @@ -1,3 +1,5 @@ +import asyncio + import beanie import click import motor.motor_asyncio @@ -8,7 +10,7 @@ def create_connection_string( - host: str, port: int, db_name: str, username: str = None, password: str = None + host: str, port: int, db_name: str, username: str, password: str ) -> MongoDsn: return MongoDsn.build( scheme="mongodb", @@ -26,6 +28,10 @@ async def init_connection(mongodb_dsn: MongoDsn): client = motor.motor_asyncio.AsyncIOMotorClient( mongodb_dsn.unicode_string(), uuidRepresentation="standard" ) + + # This is to make sure that the client is using the same event loop as the rest of the application + client.get_io_loop = asyncio.get_event_loop + await beanie.init_beanie( database=client.get_default_database(), document_models=models.all_models, diff --git a/src/nsls2api/tests/infrastructure/test_mongodb_setup.py b/src/nsls2api/tests/infrastructure/test_mongodb_setup.py index 657ff2e8..7ddf157f 100644 --- a/src/nsls2api/tests/infrastructure/test_mongodb_setup.py +++ b/src/nsls2api/tests/infrastructure/test_mongodb_setup.py @@ -1,8 +1,8 @@ -import pytest from pydantic import MongoDsn from nsls2api.infrastructure.mongodb_setup import create_connection_string + def test_create_connection_string(): mongo_dsn = create_connection_string(host="testhost", port=27017, From 412e6c012b0bf421419f36500c4ef90fa9d3d5d9 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Mon, 21 Oct 2024 14:41:37 -0400 Subject: [PATCH 33/46] Change default asyncio loop scope --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f0ea7b7c..57e9d568 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ target_version = ['py311'] include = '\.pyi?$' [tool.pytest.ini_options] -asyncio_default_fixture_loop_scope = "session" +asyncio_default_fixture_loop_scope = "function" asyncio_mode = "auto" [tool.ruff] From 22aec1f2b31c6fca4bc565ea462e36554d4190cc Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Mon, 21 Oct 2024 14:47:19 -0400 Subject: [PATCH 34/46] Update pytest fixtures --- src/nsls2api/tests/api/conftest.py | 13 ------------- src/nsls2api/tests/conftest.py | 9 ++++++--- 2 files changed, 6 insertions(+), 16 deletions(-) delete mode 100644 src/nsls2api/tests/api/conftest.py diff --git a/src/nsls2api/tests/api/conftest.py b/src/nsls2api/tests/api/conftest.py deleted file mode 100644 index 8d6405b5..00000000 --- a/src/nsls2api/tests/api/conftest.py +++ /dev/null @@ -1,13 +0,0 @@ -import pytest_asyncio -from asgi_lifespan import LifespanManager -from httpx import AsyncClient, ASGITransport - -from nsls2api.main import app - - -@pytest_asyncio.fixture(scope="function", autouse=True, loop_scope="function") -async def test_client(db): - async with LifespanManager(app, startup_timeout=100, shutdown_timeout=100): - server_name = "http://localhost" - async with AsyncClient(transport=ASGITransport(app=app), base_url=server_name, follow_redirects=True) as client: - yield client diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py index 3e803019..3a784123 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -1,3 +1,6 @@ +import asyncio +import datetime + import pytest_asyncio from nsls2api import models @@ -9,7 +12,7 @@ from nsls2api.models.proposal_types import ProposalType -@pytest_asyncio.fixture(scope="session", loop_scope="session") +@pytest_asyncio.fixture(scope="session", loop_scope="session", autouse=True) async def db(): settings = get_settings() await init_connection(settings.mongodb_dsn) @@ -35,8 +38,8 @@ async def db(): # Insert a cycle into the database cycle = Cycle(name="1999-1", facility="nsls2", year="1999", - start_date="1999-01-01T00:00:00.000+00:00", - end_date="1999-06-30T00:00:00.000+00:00", + 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" From c3f9210cda4693c62b33ba49ed2932b2e4bf416d Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Mon, 21 Oct 2024 14:47:41 -0400 Subject: [PATCH 35/46] Add tests for facility_service --- src/nsls2api/tests/services/__init__.py | 0 .../tests/services/test_facility_service.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 src/nsls2api/tests/services/__init__.py create mode 100644 src/nsls2api/tests/services/test_facility_service.py diff --git a/src/nsls2api/tests/services/__init__.py b/src/nsls2api/tests/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/nsls2api/tests/services/test_facility_service.py b/src/nsls2api/tests/services/test_facility_service.py new file mode 100644 index 00000000..3fa657a5 --- /dev/null +++ b/src/nsls2api/tests/services/test_facility_service.py @@ -0,0 +1,14 @@ +import pytest + +from nsls2api.services import facility_service + + +@pytest.mark.anyio +async def test_facilities_count(): + assert await facility_service.facilities_count() == 1 + + +@pytest.mark.anyio +async def test_all_facilities(): + facilities = await facility_service.all_facilities() + assert type(facilities) == list From 2f2e30503fdb5041423a6a05b867cc12d8af61d6 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Mon, 21 Oct 2024 14:47:57 -0400 Subject: [PATCH 36/46] Update test to be async --- src/nsls2api/tests/test_home.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/nsls2api/tests/test_home.py b/src/nsls2api/tests/test_home.py index 294e26ff..20091f3b 100644 --- a/src/nsls2api/tests/test_home.py +++ b/src/nsls2api/tests/test_home.py @@ -1,11 +1,14 @@ -from fastapi.testclient import TestClient +import pytest +from httpx import ASGITransport, AsyncClient from nsls2api.main import app -client = TestClient(app) - -def test_healthy_endpoint(): - response = client.get("/healthy") +@pytest.mark.anyio +async def test_healthy_endpoint(): + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: + response = await ac.get("/healthy") assert response.status_code == 200 assert response.text == "OK" From 0f058fc568d5bb85afd27953f9eca32ead0677f2 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Mon, 21 Oct 2024 14:48:35 -0400 Subject: [PATCH 37/46] Update test to be async --- src/nsls2api/tests/api/test_beamline_api.py | 43 ++++++++++++++------- src/nsls2api/tests/api/test_facility_api.py | 32 ++++++++++----- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/nsls2api/tests/api/test_beamline_api.py b/src/nsls2api/tests/api/test_beamline_api.py index f52f87d8..bb3a92f5 100644 --- a/src/nsls2api/tests/api/test_beamline_api.py +++ b/src/nsls2api/tests/api/test_beamline_api.py @@ -1,15 +1,19 @@ import pytest -from httpx import AsyncClient +from httpx import AsyncClient, ASGITransport +from nsls2api.main import app from nsls2api.models.beamlines import ServiceAccounts, Beamline -@pytest.mark.asyncio -async def test_get_beamline_service_accounts(test_client: AsyncClient): - response = await test_client.get("/v1/beamline/zzz/service-accounts") - response_json = response.json() +@pytest.mark.anyio +async def test_get_beamline_service_accounts(): + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: + response = await ac.get("/v1/beamline/zzz/service-accounts") assert response.status_code == 200 + response_json = response.json() # Make sure we can create a ServiceAccounts object from the response accounts = ServiceAccounts(**response_json) assert accounts.workflow == "testy-mctestface-workflow" @@ -20,9 +24,13 @@ async def test_get_beamline_service_accounts(test_client: AsyncClient): assert accounts.lsdc is None or accounts.lsdc == "" -@pytest.mark.asyncio -async def test_get_beamline_lowercase(test_client: AsyncClient): - response = await test_client.get("/v1/beamline/zzz") +@pytest.mark.anyio +async def test_get_beamline_lowercase(): + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: + response = await ac.get("/v1/beamline/zzz") + response_json = response.json() assert response.status_code == 200 @@ -31,9 +39,13 @@ async def test_get_beamline_lowercase(test_client: AsyncClient): assert beamline.name == "ZZZ" -@pytest.mark.asyncio -async def test_get_beamline_uppercase(test_client: AsyncClient): - response = await test_client.get("/v1/beamline/ZZZ") +@pytest.mark.anyio +async def test_get_beamline_uppercase(): + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: + response = await ac.get("/v1/beamline/ZZZ") + response_json = response.json() assert response.status_code == 200 @@ -42,8 +54,11 @@ async def test_get_beamline_uppercase(test_client: AsyncClient): assert beamline.name == "ZZZ" -@pytest.mark.asyncio -async def test_get_beamline_directory_skeleton(test_client: AsyncClient): - response = await test_client.get("/v1/beamline/zzz/directory-skeleton") +@pytest.mark.anyio +async def test_get_beamline_directory_skeleton(): + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: + response = await ac.get("/v1/beamline/zzz/directory-skeleton") response_json = response.json() assert response.status_code == 200 diff --git a/src/nsls2api/tests/api/test_facility_api.py b/src/nsls2api/tests/api/test_facility_api.py index ea9c1d6d..cbba261c 100644 --- a/src/nsls2api/tests/api/test_facility_api.py +++ b/src/nsls2api/tests/api/test_facility_api.py @@ -1,14 +1,19 @@ import pytest -from httpx import AsyncClient +from httpx import AsyncClient, ASGITransport from nsls2api.api.models.facility_model import FacilityCyclesResponseModel, FacilityCurrentOperatingCycleResponseModel from nsls2api.api.models.proposal_model import CycleProposalList +from nsls2api.main import app -@pytest.mark.asyncio -async def test_get_current_operating_cycle(test_client: AsyncClient): +@pytest.mark.anyio +async def test_get_current_operating_cycle(): facility_name = "nsls2" - response = await test_client.get(f"/v1/facility/{facility_name}/cycles/current") + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: + response = await ac.get(f"/v1/facility/{facility_name}/cycles/current") + response_json = response.json() assert response.status_code == 200 @@ -18,10 +23,14 @@ async def test_get_current_operating_cycle(test_client: AsyncClient): assert current_cycle.cycle == "1999-1" -@pytest.mark.asyncio -async def test_get_facility_cycles(test_client: AsyncClient): +@pytest.mark.anyio +async def test_get_facility_cycles(): facility_name = "nsls2" - response = await test_client.get(f"/v1/facility/{facility_name}/cycles") + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: + response = await ac.get(f"/v1/facility/{facility_name}/cycles") + response_json = response.json() assert response.status_code == 200 @@ -33,10 +42,15 @@ async def test_get_facility_cycles(test_client: AsyncClient): @pytest.mark.asyncio -async def test_get_proposals_for_cycle(test_client: AsyncClient): +async def test_get_proposals_for_cycle(): facility_name = "nsls2" cycle_name = "1999-1" - response = await test_client.get(f"/v1/facility/{facility_name}/cycle/{cycle_name}/proposals") + + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: + response = await ac.get(f"/v1/facility/{facility_name}/cycle/{cycle_name}/proposals") + response_json = response.json() assert response.status_code == 200 From 0a6eec75149e1f13d57cb56bb9e4e8330370cb1e Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Tue, 22 Oct 2024 20:26:28 -0400 Subject: [PATCH 38/46] Change to use fastapi status codes rather than ints --- src/nsls2api/api/v1/beamline_api.py | 53 +++++++++++------------------ 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/src/nsls2api/api/v1/beamline_api.py b/src/nsls2api/api/v1/beamline_api.py index 1a648a89..15389f69 100644 --- a/src/nsls2api/api/v1/beamline_api.py +++ b/src/nsls2api/api/v1/beamline_api.py @@ -1,16 +1,15 @@ import fastapi from fastapi import HTTPException, Depends, Request from fastapi.security.api_key import APIKey + from nsls2api.api.models.proposal_model import ( ProposalDirectoriesList, ) - from nsls2api.infrastructure.logging import logger from nsls2api.infrastructure.security import ( get_current_user, validate_admin_role, ) - from nsls2api.models.beamlines import ( Beamline, BeamlineService, @@ -18,7 +17,6 @@ DetectorList, DirectoryList, ) - from nsls2api.services import beamline_service router = fastapi.APIRouter() @@ -29,7 +27,7 @@ async def details(name: str): beamline = await beamline_service.beamline_by_name(name) if beamline is None: raise HTTPException( - status_code=451, detail=f"Beamline named {name} could not be found" + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"Beamline '{name}' does not exist" ) return beamline @@ -39,19 +37,19 @@ async def get_beamline_accounts(name: str, api_key: APIKey = Depends(get_current service_accounts = await beamline_service.service_accounts(name) if service_accounts is None: raise HTTPException( - status_code=404, detail=f"Beamline named {name} could not be found" + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"Beamline '{name}' does not exist" ) return service_accounts @router.get("/beamline/{name}/slack-channel-managers") async def get_beamline_slack_channel_managers( - name: str, api_key: APIKey = Depends(get_current_user) + name: str, api_key: APIKey = Depends(get_current_user) ): slack_channel_managers = await beamline_service.slack_channel_managers(name) if slack_channel_managers is None: raise HTTPException( - status_code=404, detail=f"Beamline named {name} could not be found" + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"Beamline named {name} could not be found" ) return slack_channel_managers @@ -63,7 +61,7 @@ async def get_beamline_detectors(name: str) -> DetectorList: detectors = await beamline_service.detectors(name) if detectors is None: raise HTTPException( - status_code=404, + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"No detectors for the {name} beamline could not be found.", ) @@ -84,19 +82,19 @@ async def get_beamline_detectors(name: str) -> DetectorList: dependencies=[Depends(validate_admin_role)], ) async def add_or_delete_detector( - request: Request, name: str, detector_name: str, detector: Detector | None = None + request: Request, name: str, detector_name: str, detector: Detector | None = None ): if request.method == "PUT": logger.info(f"Adding detector {detector_name} to beamline {name}") if not detector: raise HTTPException( - status_code=422, + status_code=fastapi.status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"No detector information supplied in request body.", ) if detector_name != detector.name: raise HTTPException( - status_code=400, + status_code=fastapi.status.HTTP_400_BAD_REQUEST, detail=f"Detector name in path '{detector_name}' does not match name in body '{detector.name}'.", ) @@ -111,7 +109,7 @@ async def add_or_delete_detector( if new_detector is None: raise HTTPException( - status_code=409, + status_code=fastapi.status.HTTP_409_CONFLICT, detail=f"Detector already exists in beamline {name} with either name '{detector.name}' or directory name '{detector.directory_name}'", ) @@ -126,7 +124,7 @@ async def add_or_delete_detector( if deleted_detector is None: raise HTTPException( - status_code=404, + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"Detector {detector_name} was not found for beamline {name}", ) @@ -140,19 +138,6 @@ async def add_or_delete_detector( response_model=ProposalDirectoriesList, deprecated=True, ) -async def get_beamline_proposal_directory_skeleton(name: str): - directory_skeleton = await beamline_service.directory_skeleton(name) - if directory_skeleton is None: - raise HTTPException( - status_code=404, - detail=f"No proposal directory skeleton for the {name} beamline could be generated.", - ) - response_model = ProposalDirectoriesList( - directory_count=len(directory_skeleton), directories=directory_skeleton - ) - return response_model - - @router.get( "/beamline/{name}/directory-skeleton", response_model=DirectoryList, @@ -161,7 +146,7 @@ async def get_beamline_directory_skeleton(name: str): directory_skeleton = await beamline_service.directory_skeleton(name) if directory_skeleton is None: raise HTTPException( - status_code=404, + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"No proposal directory skeleton for the {name} beamline could be generated.", ) response_model = ProposalDirectoriesList( @@ -181,7 +166,7 @@ async def get_beamline_workflow_username(name: str): workflow_user = await beamline_service.workflow_username(name) if workflow_user is None: raise HTTPException( - status_code=404, + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"No workflow user has been defined for the {name} beamline", ) return workflow_user @@ -194,7 +179,7 @@ async def get_beamline_ioc_username(name: str): ioc_user = await beamline_service.ioc_username(name) if ioc_user is None: raise HTTPException( - status_code=404, + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"No IOC user has been defined for the {name} beamline", ) return ioc_user @@ -207,7 +192,7 @@ async def get_beamline_bluesky_username(name: str): bluesky_user = await beamline_service.bluesky_username(name) if bluesky_user is None: raise HTTPException( - status_code=404, + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"No bluesky user has been defined for the {name} beamline", ) return bluesky_user @@ -222,7 +207,7 @@ async def get_beamline_epics_services_username(name: str): epics_user = await beamline_service.epics_services_username(name) if epics_user is None: raise HTTPException( - status_code=404, + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"No EPICS services user has been defined for the {name} beamline", ) return epics_user @@ -235,7 +220,7 @@ async def get_beamline_operator_username(name: str): operator_user = await beamline_service.operator_username(name) if operator_user is None: raise HTTPException( - status_code=404, + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"No operator user has been defined for the {name} beamline", ) return operator_user @@ -250,7 +235,7 @@ async def get_beamline_services(name: str): beamline_services = await beamline_service.all_services(name) if beamline_services is None: raise HTTPException( - status_code=404, detail=f"Beamline named {name} could not be found" + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"Beamline named {name} could not be found" ) return beamline_services @@ -275,7 +260,7 @@ async def add_beamline_service(name: str, service: BeamlineService): if new_service is None: raise HTTPException( - status_code=404, + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"Service {service.name} already exists in beamline {name}", ) From d632a998374247029b66df145e98df9b8ea708b8 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Tue, 22 Oct 2024 20:26:59 -0400 Subject: [PATCH 39/46] Add a check for beamline existence before dir skeleton --- src/nsls2api/services/beamline_service.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/nsls2api/services/beamline_service.py b/src/nsls2api/services/beamline_service.py index a6802b34..2276f077 100644 --- a/src/nsls2api/services/beamline_service.py +++ b/src/nsls2api/services/beamline_service.py @@ -89,12 +89,12 @@ async def detectors(name: str) -> Optional[list[Detector]]: async def add_detector( - beamline_name: str, - detector_name: str, - directory_name: str, - granularity: DirectoryGranularity, - description: str, - manufacturer: str, + beamline_name: str, + detector_name: str, + directory_name: str, + granularity: DirectoryGranularity, + description: str, + manufacturer: str, ) -> Optional[Detector]: """ Add a new detector to a beamline. @@ -143,8 +143,8 @@ async def add_detector( async def delete_detector( - beamline_name: str, - detector_name: str, + beamline_name: str, + detector_name: str, ) -> Optional[Detector]: """ Delete a detector from a beamline. @@ -320,6 +320,12 @@ async def update_data_admins(beamline_name: str, data_admins: list[str]): async def directory_skeleton(name: str): + # Let's make sure the beamline exists + beamline = await beamline_by_name(name) + # If there's no beamline then it's not possible to generate a directory skeleton + if beamline is None: + return None + detector_list = await detectors(name.upper()) directory_list = [] From 3270cd83da9dcb1a804b377d25f1fcc14fc8cdfc Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Tue, 22 Oct 2024 20:27:22 -0400 Subject: [PATCH 40/46] Add tests for non exsistent beamlines --- src/nsls2api/tests/api/test_beamline_api.py | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/nsls2api/tests/api/test_beamline_api.py b/src/nsls2api/tests/api/test_beamline_api.py index bb3a92f5..262df41b 100644 --- a/src/nsls2api/tests/api/test_beamline_api.py +++ b/src/nsls2api/tests/api/test_beamline_api.py @@ -62,3 +62,29 @@ async def test_get_beamline_directory_skeleton(): response = await ac.get("/v1/beamline/zzz/directory-skeleton") response_json = response.json() assert response.status_code == 200 + +@pytest.mark.anyio +async def test_get_nonexistent_beamline(): + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: + response = await ac.get("/v1/beamline/does-not-exist") + assert response.status_code == 404 + assert response.json() == {"detail": "Beamline 'does-not-exist' does not exist"} + +@pytest.mark.anyio +async def test_get_service_accounts_for_nonexistent_beamline(): + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: + response = await ac.get("/v1/beamline/does-not-exist/service-accounts") + assert response.status_code == 404 + assert response.json() == {"detail": "Beamline 'does-not-exist' does not exist"} + +@pytest.mark.anyio +async def test_get_directory_skeleton_for_nonexistent_beamline(): + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: + response = await ac.get("/v1/beamline/does-not-exist/directory-skeleton") + assert response.status_code == 404 \ No newline at end of file From 4081543ac53f490ed51003ced2e50e8e6bf0b7b9 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Fri, 25 Oct 2024 17:06:08 -0400 Subject: [PATCH 41/46] Update pyproject.toml to correct python versions --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 57e9d568..8341495d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "nsls2api" dynamic = ["version", "dependencies"] description = '' readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.11" license = "BSD-3-Clause" authors = [ { name = "Stuart Campbell", email = "scampbell@bnl.gov" }, @@ -17,9 +17,9 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", ] From ef243150ed29c247bb13eba2a392e486d33478b8 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Fri, 25 Oct 2024 17:08:07 -0400 Subject: [PATCH 42/46] Upgrade dependencies --- requirements-dev.txt | 26 +++++++++++++------------- requirements.txt | 26 ++++++++++++++------------ 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 04648910..9da6d866 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -45,7 +45,7 @@ click==8.1.7 # textual-dev configargparse==1.7 # via locust -coverage==7.6.3 +coverage==7.6.4 # via -r requirements-dev.in decorator==5.1.1 # via ipython @@ -62,11 +62,11 @@ flask-cors==5.0.0 # via locust flask-login==0.6.3 # via locust -frozenlist==1.4.1 +frozenlist==1.5.0 # via # aiohttp # aiosignal -gevent==24.10.2 +gevent==24.10.3 # via geventhttpclient geventhttpclient==2.3.1 # via locust @@ -79,7 +79,7 @@ idna==3.10 # yarl iniconfig==2.0.0 # via pytest -ipython==8.28.0 +ipython==8.29.0 # via -r requirements-dev.in itsdangerous==2.2.0 # via flask @@ -101,7 +101,7 @@ markdown-it-py==3.0.0 # mdit-py-plugins # rich # textual -markupsafe==3.0.1 +markupsafe==3.0.2 # via # jinja2 # werkzeug @@ -143,7 +143,7 @@ prompt-toolkit==3.0.48 # via ipython propcache==0.2.0 # via yarl -psutil==6.0.0 +psutil==6.1.0 # via locust ptyprocess==0.7.0 # via pexpect @@ -161,7 +161,7 @@ pygments==2.18.0 # rich pymongo==4.10.1 # via bunnet -pyright==1.1.385 +pyright==1.1.386 # via -r requirements-dev.in pytest==8.3.3 # via @@ -173,11 +173,11 @@ pyzmq==26.2.0 # via locust requests==2.32.3 # via locust -rich==13.9.2 +rich==13.9.3 # via # textual # textual-serve -ruff==0.6.9 +ruff==0.7.1 # via -r requirements-dev.in setuptools==75.2.0 # via @@ -191,7 +191,7 @@ sniffio==1.3.1 # asgi-lifespan stack-data==0.6.3 # via ipython -textual==0.83.0 +textual==0.85.0 # via # textual-dev # textual-serve @@ -220,14 +220,14 @@ urllib3==2.2.3 # requests wcwidth==0.2.13 # via prompt-toolkit -werkzeug==3.0.4 +werkzeug==3.0.6 # via # flask # flask-login # locust -yarl==1.15.3 +yarl==1.16.0 # via aiohttp zope-event==5.0 # via gevent -zope-interface==7.1.0 +zope-interface==7.1.1 # via gevent diff --git a/requirements.txt b/requirements.txt index 96d0ddea..199d3694 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ argon2-cffi==23.1.0 # via -r requirements.in argon2-cffi-bindings==21.2.0 # via argon2-cffi -asgi-correlation-id==4.3.3 +asgi-correlation-id==4.3.4 # via -r requirements.in async-timeout==4.0.3 # via httpx-socks @@ -33,9 +33,9 @@ decorator==5.1.1 # via gssapi dnspython==2.7.0 # via pymongo -faker==30.4.0 +faker==30.8.1 # via -r requirements.in -fastapi==0.115.2 +fastapi==0.115.3 # via -r requirements.in gssapi==1.9.0 # via n2snusertools @@ -76,7 +76,7 @@ markdown-it-py==3.0.0 # mdit-py-plugins # rich # textual -markupsafe==3.0.1 +markupsafe==3.0.2 # via # jinja2 # werkzeug @@ -89,7 +89,9 @@ motor==3.6.0 n2snusertools==0.3.7 # via -r requirements.in packaging==24.1 - # via gunicorn + # via + # asgi-correlation-id + # gunicorn passlib==1.7.4 # via -r requirements.in platformdirs==4.3.6 @@ -109,7 +111,7 @@ pydantic==2.9.2 # pydantic-settings pydantic-core==2.23.4 # via pydantic -pydantic-settings==2.5.2 +pydantic-settings==2.6.0 # via -r requirements.in pygments==2.18.0 # via rich @@ -125,7 +127,7 @@ python-socks==2.5.3 # via httpx-socks pyyaml==6.0.2 # via n2snusertools -rich==13.9.2 +rich==13.9.3 # via # -r requirements.in # textual @@ -134,9 +136,9 @@ shellingham==1.5.4 # via typer six==1.16.0 # via python-dateutil -slack-bolt==1.20.1 +slack-bolt==1.21.2 # via -r requirements.in -slack-sdk==3.33.1 +slack-sdk==3.33.2 # via # -r requirements.in # slack-bolt @@ -144,11 +146,11 @@ sniffio==1.3.1 # via # anyio # httpx -starlette==0.40.0 +starlette==0.41.0 # via # asgi-correlation-id # fastapi -textual==0.83.0 +textual==0.85.0 # via -r requirements.in toml==0.10.2 # via beanie @@ -171,5 +173,5 @@ uvicorn==0.32.0 # via -r requirements.in wcwidth==0.2.13 # via prettytable -werkzeug==3.0.4 +werkzeug==3.0.6 # via -r requirements.in From b2ae9749e7be88efbb2c712e6d1d80cc24f396f7 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Fri, 25 Oct 2024 17:12:27 -0400 Subject: [PATCH 43/46] Code Reformatting --- src/nsls2api/tests/api/test_beamline_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/nsls2api/tests/api/test_beamline_api.py b/src/nsls2api/tests/api/test_beamline_api.py index 262df41b..d663dfad 100644 --- a/src/nsls2api/tests/api/test_beamline_api.py +++ b/src/nsls2api/tests/api/test_beamline_api.py @@ -63,6 +63,7 @@ async def test_get_beamline_directory_skeleton(): response_json = response.json() assert response.status_code == 200 + @pytest.mark.anyio async def test_get_nonexistent_beamline(): async with AsyncClient( @@ -72,6 +73,7 @@ async def test_get_nonexistent_beamline(): assert response.status_code == 404 assert response.json() == {"detail": "Beamline 'does-not-exist' does not exist"} + @pytest.mark.anyio async def test_get_service_accounts_for_nonexistent_beamline(): async with AsyncClient( @@ -81,10 +83,11 @@ async def test_get_service_accounts_for_nonexistent_beamline(): assert response.status_code == 404 assert response.json() == {"detail": "Beamline 'does-not-exist' does not exist"} + @pytest.mark.anyio async def test_get_directory_skeleton_for_nonexistent_beamline(): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get("/v1/beamline/does-not-exist/directory-skeleton") - assert response.status_code == 404 \ No newline at end of file + assert response.status_code == 404 From 91d7c8189030a855f5d911ba4abef9215fc0433f Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Fri, 1 Nov 2024 15:11:09 -0400 Subject: [PATCH 44/46] Code Reformating --- src/nsls2api/tests/api/test_beamline_api.py | 18 ++--- src/nsls2api/tests/api/test_facility_api.py | 17 +++-- src/nsls2api/tests/conftest.py | 66 +++++++++++-------- .../infrastructure/test_mongodb_setup.py | 12 ++-- src/nsls2api/tests/test_home.py | 2 +- 5 files changed, 68 insertions(+), 47 deletions(-) diff --git a/src/nsls2api/tests/api/test_beamline_api.py b/src/nsls2api/tests/api/test_beamline_api.py index d663dfad..3a556c8f 100644 --- a/src/nsls2api/tests/api/test_beamline_api.py +++ b/src/nsls2api/tests/api/test_beamline_api.py @@ -1,14 +1,14 @@ import pytest -from httpx import AsyncClient, ASGITransport +from httpx import ASGITransport, AsyncClient from nsls2api.main import app -from nsls2api.models.beamlines import ServiceAccounts, Beamline +from nsls2api.models.beamlines import Beamline, ServiceAccounts @pytest.mark.anyio async def test_get_beamline_service_accounts(): async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" + transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get("/v1/beamline/zzz/service-accounts") assert response.status_code == 200 @@ -27,7 +27,7 @@ async def test_get_beamline_service_accounts(): @pytest.mark.anyio async def test_get_beamline_lowercase(): async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" + transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get("/v1/beamline/zzz") @@ -42,7 +42,7 @@ async def test_get_beamline_lowercase(): @pytest.mark.anyio async def test_get_beamline_uppercase(): async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" + transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get("/v1/beamline/ZZZ") @@ -57,7 +57,7 @@ async def test_get_beamline_uppercase(): @pytest.mark.anyio async def test_get_beamline_directory_skeleton(): async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" + transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get("/v1/beamline/zzz/directory-skeleton") response_json = response.json() @@ -67,7 +67,7 @@ async def test_get_beamline_directory_skeleton(): @pytest.mark.anyio async def test_get_nonexistent_beamline(): async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" + transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get("/v1/beamline/does-not-exist") assert response.status_code == 404 @@ -77,7 +77,7 @@ async def test_get_nonexistent_beamline(): @pytest.mark.anyio async def test_get_service_accounts_for_nonexistent_beamline(): async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" + transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get("/v1/beamline/does-not-exist/service-accounts") assert response.status_code == 404 @@ -87,7 +87,7 @@ async def test_get_service_accounts_for_nonexistent_beamline(): @pytest.mark.anyio async def test_get_directory_skeleton_for_nonexistent_beamline(): async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" + transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get("/v1/beamline/does-not-exist/directory-skeleton") assert response.status_code == 404 diff --git a/src/nsls2api/tests/api/test_facility_api.py b/src/nsls2api/tests/api/test_facility_api.py index cbba261c..6dcd9211 100644 --- a/src/nsls2api/tests/api/test_facility_api.py +++ b/src/nsls2api/tests/api/test_facility_api.py @@ -1,7 +1,10 @@ import pytest -from httpx import AsyncClient, ASGITransport +from httpx import ASGITransport, AsyncClient -from nsls2api.api.models.facility_model import FacilityCyclesResponseModel, FacilityCurrentOperatingCycleResponseModel +from nsls2api.api.models.facility_model import ( + FacilityCurrentOperatingCycleResponseModel, + FacilityCyclesResponseModel, +) from nsls2api.api.models.proposal_model import CycleProposalList from nsls2api.main import app @@ -10,7 +13,7 @@ async def test_get_current_operating_cycle(): facility_name = "nsls2" async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" + transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get(f"/v1/facility/{facility_name}/cycles/current") @@ -27,7 +30,7 @@ async def test_get_current_operating_cycle(): async def test_get_facility_cycles(): facility_name = "nsls2" async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" + transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get(f"/v1/facility/{facility_name}/cycles") @@ -47,9 +50,11 @@ async def test_get_proposals_for_cycle(): cycle_name = "1999-1" async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" + transport=ASGITransport(app=app), base_url="http://test" ) as ac: - response = await ac.get(f"/v1/facility/{facility_name}/cycle/{cycle_name}/proposals") + response = await ac.get( + f"/v1/facility/{facility_name}/cycle/{cycle_name}/proposals" + ) response_json = response.json() assert response.status_code == 200 diff --git a/src/nsls2api/tests/conftest.py b/src/nsls2api/tests/conftest.py index 3a784123..0e39f218 100644 --- a/src/nsls2api/tests/conftest.py +++ b/src/nsls2api/tests/conftest.py @@ -1,4 +1,3 @@ -import asyncio import datetime import pytest_asyncio @@ -18,40 +17,55 @@ async def db(): 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") - ) + 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") + facility = Facility( + name="NSLS-II", + facility_id="nsls2", + fullname="National Synchrotron Light Source II", + pass_facility_id="NSLS-II", + ) 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" - ) + 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", - ) + proposal_type = ProposalType( + code="X", + facility_id="nsls2", + description="Proposal Type X", + pass_id="999999", + pass_description="Proposal Type X", + ) await proposal_type.insert() yield diff --git a/src/nsls2api/tests/infrastructure/test_mongodb_setup.py b/src/nsls2api/tests/infrastructure/test_mongodb_setup.py index 7ddf157f..97a9e3b9 100644 --- a/src/nsls2api/tests/infrastructure/test_mongodb_setup.py +++ b/src/nsls2api/tests/infrastructure/test_mongodb_setup.py @@ -4,11 +4,13 @@ def test_create_connection_string(): - mongo_dsn = create_connection_string(host="testhost", - port=27017, - db_name="pytest", - username="testuser", - password="testpassword") + mongo_dsn = create_connection_string( + host="testhost", + port=27017, + db_name="pytest", + username="testuser", + password="testpassword", + ) assert mongo_dsn == MongoDsn.build( scheme="mongodb", host="testhost", diff --git a/src/nsls2api/tests/test_home.py b/src/nsls2api/tests/test_home.py index 20091f3b..f3647e0c 100644 --- a/src/nsls2api/tests/test_home.py +++ b/src/nsls2api/tests/test_home.py @@ -7,7 +7,7 @@ @pytest.mark.anyio async def test_healthy_endpoint(): async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" + transport=ASGITransport(app=app), base_url="http://test" ) as ac: response = await ac.get("/healthy") assert response.status_code == 200 From c41e4aa480a7f6c6e4938aafbef2c2d9a03c342d Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Fri, 1 Nov 2024 15:13:16 -0400 Subject: [PATCH 45/46] Code Reformating --- src/nsls2api/api/v1/beamline_api.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/nsls2api/api/v1/beamline_api.py b/src/nsls2api/api/v1/beamline_api.py index 15389f69..f0bf1959 100644 --- a/src/nsls2api/api/v1/beamline_api.py +++ b/src/nsls2api/api/v1/beamline_api.py @@ -1,5 +1,5 @@ import fastapi -from fastapi import HTTPException, Depends, Request +from fastapi import Depends, HTTPException, Request from fastapi.security.api_key import APIKey from nsls2api.api.models.proposal_model import ( @@ -27,7 +27,8 @@ async def details(name: str): beamline = await beamline_service.beamline_by_name(name) if beamline is None: raise HTTPException( - status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"Beamline '{name}' does not exist" + status_code=fastapi.status.HTTP_404_NOT_FOUND, + detail=f"Beamline '{name}' does not exist", ) return beamline @@ -37,19 +38,21 @@ async def get_beamline_accounts(name: str, api_key: APIKey = Depends(get_current service_accounts = await beamline_service.service_accounts(name) if service_accounts is None: raise HTTPException( - status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"Beamline '{name}' does not exist" + status_code=fastapi.status.HTTP_404_NOT_FOUND, + detail=f"Beamline '{name}' does not exist", ) return service_accounts @router.get("/beamline/{name}/slack-channel-managers") async def get_beamline_slack_channel_managers( - name: str, api_key: APIKey = Depends(get_current_user) + name: str, api_key: APIKey = Depends(get_current_user) ): slack_channel_managers = await beamline_service.slack_channel_managers(name) if slack_channel_managers is None: raise HTTPException( - status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"Beamline named {name} could not be found" + status_code=fastapi.status.HTTP_404_NOT_FOUND, + detail=f"Beamline named {name} could not be found", ) return slack_channel_managers @@ -82,7 +85,7 @@ async def get_beamline_detectors(name: str) -> DetectorList: dependencies=[Depends(validate_admin_role)], ) async def add_or_delete_detector( - request: Request, name: str, detector_name: str, detector: Detector | None = None + request: Request, name: str, detector_name: str, detector: Detector | None = None ): if request.method == "PUT": logger.info(f"Adding detector {detector_name} to beamline {name}") @@ -90,7 +93,7 @@ async def add_or_delete_detector( if not detector: raise HTTPException( status_code=fastapi.status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"No detector information supplied in request body.", + detail="No detector information supplied in request body.", ) if detector_name != detector.name: raise HTTPException( @@ -235,7 +238,8 @@ async def get_beamline_services(name: str): beamline_services = await beamline_service.all_services(name) if beamline_services is None: raise HTTPException( - status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"Beamline named {name} could not be found" + status_code=fastapi.status.HTTP_404_NOT_FOUND, + detail=f"Beamline named {name} could not be found", ) return beamline_services From 6358db64aa10f563c60b63a8a08952383707f6b5 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Fri, 1 Nov 2024 15:19:12 -0400 Subject: [PATCH 46/46] Update src/nsls2api/infrastructure/config.py Co-authored-by: Padraic Shafer <76011594+padraic-shafer@users.noreply.github.com> --- src/nsls2api/infrastructure/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nsls2api/infrastructure/config.py b/src/nsls2api/infrastructure/config.py index 510a846d..df5dae18 100644 --- a/src/nsls2api/infrastructure/config.py +++ b/src/nsls2api/infrastructure/config.py @@ -83,7 +83,9 @@ def get_settings() -> Settings: :returns: The dictionary of current settings. """ if os.environ.get("PYTEST_VERSION") is not None: - settings = Settings(_env_file=str(Path(__file__).parent.parent / "pytest.env")) + PROJ_SRC_PATH = Path(__file__).parent.parent + test_env_file = str(PROJ_SRC_PATH / "pytest.env") + settings = Settings(_env_file=test_env_file) else: settings = Settings()