diff --git a/performance-tests/locustfile.py b/performance-tests/locustfile.py index 67532d57..62ea2cf5 100644 --- a/performance-tests/locustfile.py +++ b/performance-tests/locustfile.py @@ -1,18 +1,20 @@ import locust + class ApiTest(locust.FastHttpUser): host = "http://127.0.0.1:8080" # The time a user 'waits' between requests - wait_time = locust.between(5,30) + wait_time = locust.between(5, 30) @locust.task(weight=1) def home_page(self): - self.client.get('/') + self.client.get("/") + @locust.task(weight=5) def stats(self): - self.client.get('/stats') + self.client.get("/stats") @locust.task(weight=15) def recent(self): - self.client.get('/proposals/recent/5') \ No newline at end of file + self.client.get("/proposals/recent/5") diff --git a/pyproject.toml b/pyproject.toml index 8341495d..9972dd4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,11 @@ hooks.vcs.version-file = "src/nsls2api/_version.py" [tool.hatch.build.targets.sdist] exclude = [ "/.github", + "/.gitignore", + "/.git", + "/.vscode", + "/.pytest_cache", + "/.venv", ] #[tool.hatch.metadata.hooks.requirements_txt.optional-dependencies] @@ -56,5 +61,5 @@ asyncio_default_fixture_loop_scope = "function" asyncio_mode = "auto" [tool.ruff] -target-version = "py311" +target-version = "py312" diff --git a/requirements-dev.in b/requirements-dev.in index ede68e72..6abb7d15 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -3,6 +3,10 @@ asyncer black bunnet coverage +hatch +hatchling +hatch-requirements-txt +hatch-vcs ipython locust pyright @@ -10,3 +14,4 @@ pytest pytest-asyncio ruff textual-dev +uv diff --git a/requirements-dev.txt b/requirements-dev.txt index 9da6d866..478e062f 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.10 +aiohttp==3.10.11 # via # aiohttp-jinja2 # textual-dev @@ -14,7 +14,9 @@ aiosignal==1.3.1 annotated-types==0.7.0 # via pydantic anyio==4.6.2.post1 - # via asyncer + # via + # asyncer + # httpx asgi-lifespan==2.1.0 # via -r requirements-dev.in asttokens==2.4.1 @@ -25,7 +27,7 @@ attrs==24.2.0 # via aiohttp black==24.10.0 # via -r requirements-dev.in -blinker==1.8.2 +blinker==1.9.0 # via flask brotli==1.1.0 # via geventhttpclient @@ -34,7 +36,11 @@ bunnet==1.3.0 certifi==2024.8.30 # via # geventhttpclient + # httpcore + # httpx # requests +cffi==1.17.1 + # via cryptography charset-normalizer==3.4.0 # via requests click==8.1.7 @@ -42,17 +48,25 @@ click==8.1.7 # black # bunnet # flask + # hatch # textual-dev + # userpath configargparse==1.7 # via locust coverage==7.6.4 # via -r requirements-dev.in +cryptography==43.0.3 + # via secretstorage decorator==5.1.1 # via ipython +distlib==0.3.9 + # via virtualenv dnspython==2.7.0 # via pymongo executing==2.1.0 # via stack-data +filelock==3.16.1 + # via virtualenv flask==3.0.3 # via # flask-cors @@ -66,15 +80,37 @@ frozenlist==1.5.0 # via # aiohttp # aiosignal -gevent==24.10.3 +gevent==24.11.1 # via geventhttpclient geventhttpclient==2.3.1 # via locust greenlet==3.1.1 # via gevent +h11==0.14.0 + # via httpcore +hatch==1.13.0 + # via -r requirements-dev.in +hatch-requirements-txt==0.4.1 + # via -r requirements-dev.in +hatch-vcs==0.4.0 + # via -r requirements-dev.in +hatchling==1.26.3 + # via + # -r requirements-dev.in + # hatch + # hatch-requirements-txt + # hatch-vcs +httpcore==1.0.6 + # via httpx +httpx==0.27.2 + # via hatch +hyperlink==21.0.0 + # via hatch idna==3.10 # via # anyio + # httpx + # hyperlink # requests # yarl iniconfig==2.0.0 @@ -83,20 +119,32 @@ ipython==8.29.0 # via -r requirements-dev.in itsdangerous==2.2.0 # via flask -jedi==0.19.1 +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.0.1 + # via keyring +jaraco-functools==4.1.0 + # via keyring +jedi==0.19.2 # via ipython +jeepney==0.8.0 + # via + # keyring + # secretstorage jinja2==3.1.4 # via # aiohttp-jinja2 # flask # textual-serve +keyring==25.5.0 + # via hatch lazy-model==0.2.0 # via bunnet linkify-it-py==2.0.3 # via markdown-it-py -locust==2.32.0 +locust==2.32.2 # via -r requirements-dev.in -markdown-it-py==3.0.0 +markdown-it-py[linkify,plugins]==3.0.0 # via # mdit-py-plugins # rich @@ -111,6 +159,10 @@ mdit-py-plugins==0.4.2 # via markdown-it-py mdurl==0.1.2 # via markdown-it-py +more-itertools==10.5.0 + # via + # jaraco-classes + # jaraco-functools msgpack==1.1.0 # via # locust @@ -123,22 +175,34 @@ mypy-extensions==1.0.0 # via black nodeenv==1.9.1 # via pyright -packaging==24.1 +packaging==24.2 # via # black + # hatch + # hatch-requirements-txt + # hatchling # pytest + # setuptools-scm parso==0.8.4 # via jedi pathspec==0.12.1 - # via black + # via + # black + # hatchling pexpect==4.9.0 - # via ipython + # via + # hatch + # ipython platformdirs==4.3.6 # via # black + # hatch # textual + # virtualenv pluggy==1.5.0 - # via pytest + # via + # hatchling + # pytest prompt-toolkit==3.0.48 # via ipython propcache==0.2.0 @@ -149,6 +213,8 @@ ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 # via stack-data +pycparser==2.22 + # via cffi pydantic==2.9.2 # via # bunnet @@ -161,7 +227,7 @@ pygments==2.18.0 # rich pymongo==4.10.1 # via bunnet -pyright==1.1.386 +pyright==1.1.388 # via -r requirements-dev.in pytest==8.3.3 # via @@ -173,25 +239,29 @@ pyzmq==26.2.0 # via locust requests==2.32.3 # via locust -rich==13.9.3 +rich==13.9.4 # via + # hatch # textual # textual-serve -ruff==0.7.1 +ruff==0.7.3 # via -r requirements-dev.in -setuptools==75.2.0 - # via - # zope-event - # zope-interface +secretstorage==3.3.3 + # via keyring +setuptools-scm==8.1.0 + # via hatch-vcs +shellingham==1.5.4 + # via hatch six==1.16.0 # via asttokens sniffio==1.3.1 # via # anyio # asgi-lifespan + # httpx stack-data==0.6.3 # via ipython -textual==0.85.0 +textual==0.85.2 # via # textual-dev # textual-serve @@ -201,10 +271,16 @@ textual-serve==1.1.1 # via textual-dev toml==0.10.2 # via bunnet +tomli-w==1.1.0 + # via hatch +tomlkit==0.13.2 + # via hatch traitlets==5.14.3 # via # ipython # matplotlib-inline +trove-classifiers==2024.10.21.16 + # via hatchling typing-extensions==4.12.2 # via # pydantic @@ -218,16 +294,26 @@ urllib3==2.2.3 # via # geventhttpclient # requests +userpath==1.9.2 + # via hatch +uv==0.5.1 + # via + # -r requirements-dev.in + # hatch +virtualenv==20.27.1 + # via hatch wcwidth==0.2.13 # via prompt-toolkit -werkzeug==3.0.6 +werkzeug==3.1.3 # via # flask # flask-login # locust -yarl==1.16.0 +yarl==1.17.1 # via aiohttp zope-event==5.0 # via gevent zope-interface==7.1.1 # via gevent +zstandard==0.23.0 + # via hatch diff --git a/requirements.txt b/requirements.txt index 199d3694..29130d33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ argon2-cffi-bindings==21.2.0 # via argon2-cffi asgi-correlation-id==4.3.4 # via -r requirements.in -async-timeout==4.0.3 +async-timeout==5.0.1 # via httpx-socks beanie==1.27.0 # via -r requirements.in @@ -33,9 +33,9 @@ decorator==5.1.1 # via gssapi dnspython==2.7.0 # via pymongo -faker==30.8.1 +faker==32.1.0 # via -r requirements.in -fastapi==0.115.3 +fastapi==0.115.5 # via -r requirements.in gssapi==1.9.0 # via n2snusertools @@ -53,7 +53,7 @@ httpx==0.27.2 # via # -r requirements.in # httpx-socks -httpx-socks==0.9.1 +httpx-socks[asyncio]==0.9.2 # via -r requirements.in idna==3.10 # via @@ -71,7 +71,7 @@ ldap3==2.9.1 # via n2snusertools linkify-it-py==2.0.3 # via markdown-it-py -markdown-it-py==3.0.0 +markdown-it-py[linkify,plugins]==3.0.0 # via # mdit-py-plugins # rich @@ -88,7 +88,7 @@ motor==3.6.0 # via beanie n2snusertools==0.3.7 # via -r requirements.in -packaging==24.1 +packaging==24.2 # via # asgi-correlation-id # gunicorn @@ -96,7 +96,7 @@ passlib==1.7.4 # via -r requirements.in platformdirs==4.3.6 # via textual -prettytable==3.11.0 +prettytable==3.12.0 # via n2snusertools pyasn1==0.6.1 # via ldap3 @@ -111,7 +111,7 @@ pydantic==2.9.2 # pydantic-settings pydantic-core==2.23.4 # via pydantic -pydantic-settings==2.6.0 +pydantic-settings==2.6.1 # via -r requirements.in pygments==2.18.0 # via rich @@ -121,13 +121,13 @@ python-dateutil==2.9.0.post0 # via faker python-dotenv==1.0.1 # via pydantic-settings -python-multipart==0.0.12 +python-multipart==0.0.18 # via -r requirements.in python-socks==2.5.3 # via httpx-socks pyyaml==6.0.2 # via n2snusertools -rich==13.9.3 +rich==13.9.4 # via # -r requirements.in # textual @@ -138,7 +138,7 @@ six==1.16.0 # via python-dateutil slack-bolt==1.21.2 # via -r requirements.in -slack-sdk==3.33.2 +slack-sdk==3.33.3 # via # -r requirements.in # slack-bolt @@ -146,15 +146,15 @@ sniffio==1.3.1 # via # anyio # httpx -starlette==0.41.0 +starlette==0.41.2 # via # asgi-correlation-id # fastapi -textual==0.85.0 +textual==0.85.2 # via -r requirements.in toml==0.10.2 # via beanie -typer==0.12.5 +typer==0.13.0 # via -r requirements.in typing-extensions==4.12.2 # via @@ -173,5 +173,5 @@ uvicorn==0.32.0 # via -r requirements.in wcwidth==0.2.13 # via prettytable -werkzeug==3.0.6 +werkzeug==3.1.3 # via -r requirements.in diff --git a/src/nsls2api/api/models/facility_model.py b/src/nsls2api/api/models/facility_model.py index 69e90362..cf0a60f5 100644 --- a/src/nsls2api/api/models/facility_model.py +++ b/src/nsls2api/api/models/facility_model.py @@ -1,15 +1,18 @@ from enum import StrEnum import pydantic + class FacilityName(StrEnum): nsls2 = "nsls2" lbms = "lbms" cfn = "cfn" + class FacilityCyclesResponseModel(pydantic.BaseModel): facility: str cycles: list[str] + class FacilityCurrentOperatingCycleResponseModel(pydantic.BaseModel): facility: str - cycle: str \ No newline at end of file + cycle: str diff --git a/src/nsls2api/api/v1/admin_api.py b/src/nsls2api/api/v1/admin_api.py index 31d1bd81..253c845a 100644 --- a/src/nsls2api/api/v1/admin_api.py +++ b/src/nsls2api/api/v1/admin_api.py @@ -11,7 +11,12 @@ validate_admin_role, generate_api_key, ) -from nsls2api.models.apikeys import ApiUser, ApiUserRole, ApiUserResponseModel, ApiUserType +from nsls2api.models.apikeys import ( + ApiUser, + ApiUserRole, + ApiUserResponseModel, + ApiUserType, +) from nsls2api.models.slack_models import SlackChannelCreationResponseModel from nsls2api.services import beamline_service, proposal_service, slack_service @@ -28,7 +33,7 @@ async def info(settings: Annotated[config.Settings, Depends(config.get_settings) @router.get("/admin/validate", response_model=str) async def check_admin_validation( - admin_user: Annotated[ApiUser, Depends(validate_admin_role)] = None, + admin_user: Annotated[ApiUser, Depends(validate_admin_role)] = None, ): """ :return: str - The username of the validated admin user. @@ -57,7 +62,7 @@ async def generate_user_apikey(username: str, usertype: ApiUserType = ApiUserTyp @router.post("/admin/proposal/generate-test") async def generate_fake_proposal( - add_specific_user: str | None = None, + add_specific_user: str | None = None, ) -> Optional[SingleProposal]: proposal = await proposal_service.generate_fake_test_proposal( FacilityName.nsls2, add_specific_user @@ -78,14 +83,17 @@ async def create_slack_channel(proposal_id: str) -> SlackChannelCreationResponse if proposal is None: raise HTTPException( - status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=f"Proposal {proposal_id} not found" + status_code=fastapi.status.HTTP_404_NOT_FOUND, + detail=f"Proposal {proposal_id} not found", ) channel_name = proposal_service.slack_channel_name_for_proposal(proposal_id) if channel_name is None: - raise HTTPException(status_code=fastapi.status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Slack channel name could not be generated for the proposal {proposal_id}") + raise HTTPException( + status_code=fastapi.status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Slack channel name could not be generated for the proposal {proposal_id}", + ) channel_id = await slack_service.create_channel( channel_name, @@ -95,7 +103,7 @@ async def create_slack_channel(proposal_id: str) -> SlackChannelCreationResponse if channel_id is None: raise HTTPException( status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Slack channel could not be created for the proposal {proposal_id}" + detail=f"Slack channel could not be created for the proposal {proposal_id}", ) logger.info(f"Created slack channel '{channel_name}' for proposal {proposal_id}.") diff --git a/src/nsls2api/api/v1/jobs_api.py b/src/nsls2api/api/v1/jobs_api.py index 511587ce..86bab76a 100644 --- a/src/nsls2api/api/v1/jobs_api.py +++ b/src/nsls2api/api/v1/jobs_api.py @@ -37,11 +37,17 @@ async def check_job_status(request: Request, job_id: str): return job.processing_status -@router.get("/sync/dataadmins", dependencies=[Depends(get_current_user)], include_in_schema=SYNC_ROUTES_IN_SCHEMA, tags=["sync"]) +@router.get( + "/sync/dataadmins", + dependencies=[Depends(get_current_user)], + include_in_schema=SYNC_ROUTES_IN_SCHEMA, + tags=["sync"], +) async def sync_dataadmins(request: Request) -> BackgroundJob: job = await background_service.create_background_job(JobActions.synchronize_admins) return job + @router.get( "/sync/proposal/{proposal_id}", dependencies=[Depends(get_current_user)], @@ -57,7 +63,11 @@ async def sync_proposal(request: Request, proposal_id: str) -> BackgroundJob: return job -@router.get("/sync/proposal/types/{facility}", include_in_schema=SYNC_ROUTES_IN_SCHEMA, tags=["sync"]) +@router.get( + "/sync/proposal/types/{facility}", + include_in_schema=SYNC_ROUTES_IN_SCHEMA, + tags=["sync"], +) async def sync_proposal_types(facility: FacilityName = FacilityName.nsls2): sync_params = JobSyncParameters(facility=facility) job = await background_service.create_background_job( @@ -83,7 +93,9 @@ async def sync_proposals_for_cycle(request: Request, cycle: str, return job -@router.get("/sync/cycles/{facility}", include_in_schema=SYNC_ROUTES_IN_SCHEMA, tags=["sync"]) +@router.get( + "/sync/cycles/{facility}", include_in_schema=SYNC_ROUTES_IN_SCHEMA, tags=["sync"] +) async def sync_cycles(facility: FacilityName = FacilityName.nsls2): sync_params = JobSyncParameters(facility=facility) job = await background_service.create_background_job( @@ -93,15 +105,17 @@ async def sync_cycles(facility: FacilityName = FacilityName.nsls2): return job -@router.get("/sync/update-cycles/{facility}", include_in_schema=SYNC_ROUTES_IN_SCHEMA, tags=["sync"]) +@router.get( + "/sync/update-cycles/{facility}", + include_in_schema=SYNC_ROUTES_IN_SCHEMA, + tags=["sync"], +) async def sync_update_cycles( request: fastapi.Request, facility: FacilityName = FacilityName.nsls2, cycle: Optional[str] = None, ): - sync_params = JobSyncParameters( - facility=facility, sync_source=JobSyncSource.PASS - ) + sync_params = JobSyncParameters(facility=facility, sync_source=JobSyncSource.PASS) job = await background_service.create_background_job( JobActions.update_cycle_information, diff --git a/src/nsls2api/api/v1/proposal_api.py b/src/nsls2api/api/v1/proposal_api.py index f5152abd..8ffbb0ae 100644 --- a/src/nsls2api/api/v1/proposal_api.py +++ b/src/nsls2api/api/v1/proposal_api.py @@ -47,12 +47,19 @@ async def get_commissioning_proposals(beamline: str | None = None): try: proposals = await proposal_service.commissioning_proposals(beamline=beamline) if proposals is None: - raise HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND, - detail="No commissioning proposals found") + raise HTTPException( + status_code=fastapi.status.HTTP_404_NOT_FOUND, + detail="No commissioning proposals found", + ) except LookupError as e: - raise HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=e.args[0]) + raise HTTPException( + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=e.args[0] + ) except Exception as e: - raise HTTPException(status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"An error occurred: {e}") + raise HTTPException( + status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"An error occurred: {e}", + ) model = CommissioningProposalsList( count=len(proposals), commissioning_proposals=proposals @@ -60,15 +67,19 @@ async def get_commissioning_proposals(beamline: str | None = None): return model -@router.get("/proposals/", response_model=ProposalFullDetailsList, description="Not fully functional yet.") +@router.get( + "/proposals/", + response_model=ProposalFullDetailsList, + description="Not fully functional yet.", +) async def get_proposals( - proposal_id: Annotated[list[str], Query()] = [], - beamline: Annotated[list[str], Query()] = [], - cycle: Annotated[list[str], Query()] = [], - facility: Annotated[list[FacilityName], Query()] = [FacilityName.nsls2], - page_size: int = 10, - page: int = 1, - include_directories: bool = False, + proposal_id: Annotated[list[str], Query()] = [], + beamline: Annotated[list[str], Query()] = [], + cycle: Annotated[list[str], Query()] = [], + facility: Annotated[list[FacilityName], Query()] = [FacilityName.nsls2], + page_size: int = 10, + page: int = 1, + include_directories: bool = False, ): proposal_list = await proposal_service.fetch_proposals( proposal_id=proposal_id, @@ -95,12 +106,19 @@ async def get_proposal(proposal_id: str): try: proposal = await proposal_service.proposal_by_id(proposal_id) if proposal is None: - raise HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND, - detail=f"Proposal {proposal_id} not found") + raise HTTPException( + status_code=fastapi.status.HTTP_404_NOT_FOUND, + detail=f"Proposal {proposal_id} not found", + ) except LookupError as e: - raise HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=e.args[0]) + raise HTTPException( + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=e.args[0] + ) except Exception as e: - raise HTTPException(status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"An error occurred: {e}") + raise HTTPException( + status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"An error occurred: {e}", + ) response_model = SingleProposal(proposal=proposal) return response_model @@ -111,7 +129,10 @@ async def get_proposals_users(proposal_id: str): try: users = await proposal_service.fetch_users_on_proposal(proposal_id) if users is None: - raise HTTPException(fastapi.status.HTTP_404_NOT_FOUND, detail=f"Users not found for proposal {proposal_id}") + raise HTTPException( + fastapi.status.HTTP_404_NOT_FOUND, + detail=f"Users not found for proposal {proposal_id}", + ) except LookupError as e: raise HTTPException(status_code=404, detail=e.args[0]) except Exception as e: @@ -131,16 +152,25 @@ async def get_proposal_principal_investigator(proposal_id: str): principal_investigator = await proposal_service.pi_from_proposal(proposal_id) if len(principal_investigator) == 0: # We need a PI for every proposal - raise HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND, - detail=f"PI not found for proposal {proposal_id}") + raise HTTPException( + status_code=fastapi.status.HTTP_404_NOT_FOUND, + detail=f"PI not found for proposal {proposal_id}", + ) elif len(principal_investigator) > 1: # We should only have 1 PI - raise HTTPException(status_code=fastapi.status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Proposal {proposal_id} contains more than one PI") + raise HTTPException( + status_code=fastapi.status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Proposal {proposal_id} contains more than one PI", + ) except LookupError as e: - raise HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=e.args[0]) + raise HTTPException( + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=e.args[0] + ) except Exception as e: - raise HTTPException(status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"An error occurred: {e}") + raise HTTPException( + status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"An error occurred: {e}", + ) response_model = ProposalUser( proposal_id=str(proposal_id), user=principal_investigator[0] @@ -153,18 +183,27 @@ async def get_proposal_usernames(proposal_id: str): try: # Check to see if proposal exists if not await proposal_service.exists(proposal_id): - raise HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND, - detail=f"Proposal {proposal_id} not found") + raise HTTPException( + status_code=fastapi.status.HTTP_404_NOT_FOUND, + detail=f"Proposal {proposal_id} not found", + ) except LookupError as e: - raise HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=e.args[0]) + raise HTTPException( + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=e.args[0] + ) except Exception as e: - raise HTTPException(status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"An error occurred: {e}") + raise HTTPException( + status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"An error occurred: {e}", + ) proposal_usernames = await proposal_service.fetch_usernames_from_proposal( proposal_id ) - proposal_groupname = proposal_service.generate_data_session_for_proposal(proposal_id) + proposal_groupname = proposal_service.generate_data_session_for_proposal( + proposal_id + ) response_model = UsernamesList( usernames=proposal_usernames, @@ -180,12 +219,19 @@ async def get_proposal_directories(proposal_id: str) -> ProposalDirectoriesList: try: directories = await proposal_service.directories(proposal_id) if directories is None: - raise HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND, - detail=f"Directories not found for proposal {proposal_id}") + raise HTTPException( + status_code=fastapi.status.HTTP_404_NOT_FOUND, + detail=f"Directories not found for proposal {proposal_id}", + ) except LookupError as e: - raise HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=e.args[0]) + raise HTTPException( + status_code=fastapi.status.HTTP_404_NOT_FOUND, detail=e.args[0] + ) except Exception as e: - raise HTTPException(status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"An error occurred: {e}") + raise HTTPException( + status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"An error occurred: {e}", + ) response_model = ProposalDirectoriesList( directories=directories, diff --git a/src/nsls2api/api/v1/stats_api.py b/src/nsls2api/api/v1/stats_api.py index 8c5a4175..490e9c84 100644 --- a/src/nsls2api/api/v1/stats_api.py +++ b/src/nsls2api/api/v1/stats_api.py @@ -1,5 +1,4 @@ import fastapi -from pydantic_core import ValidationError from nsls2api._version import version as api_version from nsls2api.api.models.stats_model import ( @@ -7,7 +6,6 @@ StatsModel, ProposalsPerCycleModel, ) -from nsls2api.infrastructure.logging import logger from nsls2api.services import ( beamline_service, facility_service, diff --git a/src/nsls2api/api/v1/user_api.py b/src/nsls2api/api/v1/user_api.py index 0f6651a8..df7a57fd 100644 --- a/src/nsls2api/api/v1/user_api.py +++ b/src/nsls2api/api/v1/user_api.py @@ -29,8 +29,11 @@ async def get_person_from_username(username: str): username=bnl_person.ActiveDirectoryName, cyber_agreement_signed=bnl_person.CyberAgreementSigned, ) - # If the person is an Employee then set their institution to BNL - if bnl_person.EmployeeStatus == "Active" and bnl_person.EmployeeType == "Employee": + # If the person is an Employee then set their institution to BNL + if ( + bnl_person.EmployeeStatus == "Active" + and bnl_person.EmployeeType == "Employee" + ): person.bnl_employee = True person.institution = "Brookhaven National Laboratory" return person @@ -61,6 +64,7 @@ async def get_person_from_email(email: str): status_code=404, ) + # TODO: Add back into schema if we decide to use this endpoint. @router.get("/person/department/{department}", include_in_schema=False) async def get_person_by_department(department_code: str = "PS"): @@ -68,6 +72,7 @@ async def get_person_by_department(department_code: str = "PS"): if bnl_people: return bnl_people + # TODO: Add back into schema if we decide to use this endpoint. @router.get("/person/me", response_model=str, include_in_schema=False) async def get_myself(current_user: Annotated[Person, Depends(get_current_user)]): diff --git a/src/nsls2api/cli/auth.py b/src/nsls2api/cli/auth.py index 5482fa34..50f1a2d5 100644 --- a/src/nsls2api/cli/auth.py +++ b/src/nsls2api/cli/auth.py @@ -3,7 +3,6 @@ from typing import Optional import httpx -import rich.emoji import typer import getpass import configparser @@ -58,7 +57,3 @@ def status(): raise print(f"Authenticated as {logged_in_username}") - - - - diff --git a/src/nsls2api/cli/beamline.py b/src/nsls2api/cli/beamline.py index 9c080a09..529f3d7e 100644 --- a/src/nsls2api/cli/beamline.py +++ b/src/nsls2api/cli/beamline.py @@ -10,4 +10,4 @@ def view(beamline: str): @app.command("list") def list_beamlines(): - print(f"Listing beamlines...") + print("Listing beamlines...") diff --git a/src/nsls2api/exception_handlers.py b/src/nsls2api/exception_handlers.py index be33d00c..6b0e7e9a 100644 --- a/src/nsls2api/exception_handlers.py +++ b/src/nsls2api/exception_handlers.py @@ -5,7 +5,7 @@ from fastapi.responses import JSONResponse -# This is to make sure we add the request ID to the response headers for the case +# This is to make sure we add the request ID to the response headers for the case # of unhandled server errors. @app.exception_handler(Exception) async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: diff --git a/src/nsls2api/infrastructure/app_setup.py b/src/nsls2api/infrastructure/app_setup.py index e244ef6d..b971421b 100644 --- a/src/nsls2api/infrastructure/app_setup.py +++ b/src/nsls2api/infrastructure/app_setup.py @@ -1,5 +1,4 @@ import asyncio -import os from contextlib import asynccontextmanager from nsls2api.infrastructure import mongodb_setup @@ -11,9 +10,9 @@ local_development_mode = False + @asynccontextmanager async def app_lifespan(_): - # Initialize the MongoDB connection await mongodb_setup.init_connection(settings.mongodb_dsn) diff --git a/src/nsls2api/infrastructure/config.py b/src/nsls2api/infrastructure/config.py index df5dae18..80f33b31 100644 --- a/src/nsls2api/infrastructure/config.py +++ b/src/nsls2api/infrastructure/config.py @@ -65,13 +65,15 @@ class Settings(BaseSettings): 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_url: HttpUrl = ( + "https://ups.servicenowservices.com/api" + ) universal_proposal_system_api_user: str | None = "" - universal_proposal_system_api_password : str | None = "" - + universal_proposal_system_api_password: str | None = "" model_config = SettingsConfigDict( - env_file=str(Path(__file__).parent.parent / ".env"), extra='ignore', + env_file=str(Path(__file__).parent.parent / ".env"), + extra="ignore", ) @@ -89,4 +91,4 @@ def get_settings() -> Settings: else: settings = Settings() - return settings \ No newline at end of file + return settings diff --git a/src/nsls2api/infrastructure/mongodb_setup.py b/src/nsls2api/infrastructure/mongodb_setup.py index 1fb95e11..66b9b38b 100644 --- a/src/nsls2api/infrastructure/mongodb_setup.py +++ b/src/nsls2api/infrastructure/mongodb_setup.py @@ -10,7 +10,7 @@ def create_connection_string( - host: str, port: int, db_name: str, username: str, password: str + host: str, port: int, db_name: str, username: str, password: str ) -> MongoDsn: return MongoDsn.build( scheme="mongodb", @@ -37,4 +37,6 @@ async def init_connection(mongodb_dsn: MongoDsn): document_models=models.all_models, ) - logger.info(f"Connected to {click.style(client.get_default_database().name, fg='green')} database.") + logger.info( + f"Connected to {click.style(client.get_default_database().name, fg='green')} database." + ) diff --git a/src/nsls2api/infrastructure/security.py b/src/nsls2api/infrastructure/security.py index 816f110c..b56ebaae 100644 --- a/src/nsls2api/infrastructure/security.py +++ b/src/nsls2api/infrastructure/security.py @@ -176,7 +176,7 @@ async def validate_admin_role( return key.user else: return None - except LookupError as lookup_err: + except LookupError: return None else: raise HTTPException( diff --git a/src/nsls2api/local_cli.py b/src/nsls2api/local_cli.py index 33d3187b..da460062 100644 --- a/src/nsls2api/local_cli.py +++ b/src/nsls2api/local_cli.py @@ -2,6 +2,7 @@ This is a VERY rough local cli app that needs completely replacing. It is just here for now to test some functionality before I add the API and use a better CLI framework. """ + import asyncio from nsls2api.infrastructure import mongodb_setup diff --git a/src/nsls2api/models/apikeys.py b/src/nsls2api/models/apikeys.py index 890224f9..01ec08f5 100644 --- a/src/nsls2api/models/apikeys.py +++ b/src/nsls2api/models/apikeys.py @@ -1,6 +1,6 @@ import datetime from enum import StrEnum -from typing import Optional, List +from typing import Optional from uuid import UUID, uuid4 import beanie @@ -20,6 +20,7 @@ class ApiUserRole(StrEnum): staff = "staff" admin = "admin" + class ApiUserResponseModel(pydantic.BaseModel): id: UUID username: str diff --git a/src/nsls2api/services/background_service.py b/src/nsls2api/services/background_service.py index 2eb80c6a..e131a627 100644 --- a/src/nsls2api/services/background_service.py +++ b/src/nsls2api/services/background_service.py @@ -11,7 +11,7 @@ async def create_background_job( - action: JobActions, sync_parameters: JobSyncParameters = None + action: JobActions, sync_parameters: JobSyncParameters = None ) -> BackgroundJob: job = BackgroundJob(action=action, sync_parameters=sync_parameters) await job.save() @@ -50,7 +50,7 @@ async def start_job(job_id: bson.ObjectId) -> Optional[BackgroundJob]: async def complete_job( - job_id: bson.ObjectId, processing_status: JobStatus, log_message: str = None + job_id: bson.ObjectId, processing_status: JobStatus, log_message: str = None ) -> Optional[BackgroundJob]: job = await job_by_id(job_id) if not job: diff --git a/src/nsls2api/services/beamline_service.py b/src/nsls2api/services/beamline_service.py index 2276f077..a578ac3e 100644 --- a/src/nsls2api/services/beamline_service.py +++ b/src/nsls2api/services/beamline_service.py @@ -10,7 +10,6 @@ from nsls2api.infrastructure.logging import logger from nsls2api.models.beamlines import ( Beamline, - BeamlineService, Detector, DetectorView, ServicesOnly, @@ -89,12 +88,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 +142,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. diff --git a/src/nsls2api/services/facility_service.py b/src/nsls2api/services/facility_service.py index ef165a52..5052050c 100644 --- a/src/nsls2api/services/facility_service.py +++ b/src/nsls2api/services/facility_service.py @@ -110,7 +110,12 @@ async def update_data_admins(facility_id: str, data_admins: list[str]): data_admins (list[str]): A list of usernames to set as data admins for the facility. """ await Facility.find_one(Facility.facility_id == facility_id.lower()).update( - Set({Facility.data_admins: data_admins, Facility.last_updated: datetime.datetime.now(), }) + Set( + { + Facility.data_admins: data_admins, + Facility.last_updated: datetime.datetime.now(), + } + ) ) @@ -135,7 +140,7 @@ async def current_operating_cycle(facility: str) -> Optional[str]: async def cycle_year( - cycle_name: str, facility_name: FacilityName = FacilityName.nsls2 + cycle_name: str, facility_name: FacilityName = FacilityName.nsls2 ) -> Optional[str]: """ Cycle Year diff --git a/src/nsls2api/services/helpers.py b/src/nsls2api/services/helpers.py index bdabe276..ce5eca79 100644 --- a/src/nsls2api/services/helpers.py +++ b/src/nsls2api/services/helpers.py @@ -13,13 +13,19 @@ class HTTPXClientWrapper: async_client = None def start(self): - timeouts = httpx.Timeout(None, connect=30.0) # 30s timeout on connect, no other timeouts. + timeouts = httpx.Timeout( + None, connect=30.0 + ) # 30s timeout on connect, no other timeouts. limits = httpx.Limits(max_keepalive_connections=5, max_connections=10) self.async_client = httpx.AsyncClient(limits=limits, timeout=timeouts) - logger.info(f"HTTPXClientWrapper [{click.style(str(id(self.async_client)), fg='cyan')}] started.") + logger.info( + f"HTTPXClientWrapper [{click.style(str(id(self.async_client)), fg='cyan')}] started." + ) async def stop(self): - logger.info(f"HTTPXClientWrapper [{click.style(str(id(self.async_client)), fg='cyan')}] stopped.") + logger.info( + f"HTTPXClientWrapper [{click.style(str(id(self.async_client)), fg='cyan')}] stopped." + ) await self.async_client.aclose() self.async_client = None @@ -50,6 +56,7 @@ async def _call_async_webservice( results = resp.json() return results + async def _call_async_webservice_with_client( url: str, headers: dict = None, client: httpx.AsyncClient = None ) -> Response: @@ -62,4 +69,5 @@ async def _call_async_webservice_with_client( results = resp.json() return results + httpx_client_wrapper = HTTPXClientWrapper() diff --git a/src/nsls2api/services/n2sn_service.py b/src/nsls2api/services/n2sn_service.py index d21c97e1..bac08eae 100644 --- a/src/nsls2api/services/n2sn_service.py +++ b/src/nsls2api/services/n2sn_service.py @@ -18,11 +18,11 @@ async def get_groups_by_username(username: str) -> Optional[ActiveDirectoryUserG """ with ADObjects( - settings.active_directory_server, - user_search=settings.n2sn_user_search, - group_search=settings.n2sn_group_search, - authenticate=False, - ca_certs_file=settings.bnlroot_ca_certs_file, + settings.active_directory_server, + user_search=settings.n2sn_user_search, + group_search=settings.n2sn_group_search, + authenticate=False, + ca_certs_file=settings.bnlroot_ca_certs_file, ) as ad: user_details = ad.get_group_by_samaccountname(username) if len(user_details) == 0 or len(user_details) > 1: @@ -42,11 +42,11 @@ async def get_user_by_username(username: str) -> Optional[ActiveDirectoryUser]: print(settings.pass_api_key) with ADObjects( - settings.active_directory_server, - user_search=settings.n2sn_user_search, - group_search=settings.n2sn_group_search, - authenticate=False, - ca_certs_file=settings.bnlroot_ca_certs_file, + settings.active_directory_server, + user_search=settings.n2sn_user_search, + group_search=settings.n2sn_group_search, + authenticate=False, + ca_certs_file=settings.bnlroot_ca_certs_file, ) as ad: user_details = ad.get_user_by_samaccountname(username) if len(user_details) == 0 or len(user_details) > 1: @@ -61,11 +61,11 @@ async def get_user_by_id(bnl_id: str) -> Optional[ActiveDirectoryUser]: """ with ADObjects( - settings.active_directory_server, - user_search=settings.n2sn_user_search, - group_search=settings.n2sn_group_search, - authenticate=False, - ca_certs_file=settings.bnlroot_ca_certs_file, + settings.active_directory_server, + user_search=settings.n2sn_user_search, + group_search=settings.n2sn_group_search, + authenticate=False, + ca_certs_file=settings.bnlroot_ca_certs_file, ) as ad: user_details = ad.get_user_by_id(bnl_id) if len(user_details) == 0 or len(user_details) > 1: @@ -88,11 +88,11 @@ async def get_users_in_group(group: str) -> list[ActiveDirectoryUser]: """ with ADObjects( - settings.active_directory_server, - user_search=settings.n2sn_user_search, - group_search=settings.n2sn_group_search, - authenticate=False, - ca_certs_file=settings.bnlroot_ca_certs_file, + settings.active_directory_server, + user_search=settings.n2sn_user_search, + group_search=settings.n2sn_group_search, + authenticate=False, + ca_certs_file=settings.bnlroot_ca_certs_file, ) as ad: users = ad.get_group_members(group) return users diff --git a/src/nsls2api/services/pass_service.py b/src/nsls2api/services/pass_service.py index ecead96c..9ceac027 100644 --- a/src/nsls2api/services/pass_service.py +++ b/src/nsls2api/services/pass_service.py @@ -128,7 +128,7 @@ async def get_commissioning_proposals_by_year(year: str, facility_name: Facility error_message: str = f"Facility {facility_name} does not have a PASS ID." logger.error(error_message) raise PassException(error_message) - + # The PASS ID for commissioning proposals is 300005 url = f"{base_url}/Proposal/GetProposalsByType/{api_key}/{pass_facility}/{year}/300005/NULL" @@ -137,7 +137,9 @@ async def get_commissioning_proposals_by_year(year: str, facility_name: Facility commissioning_proposal_list = [] if pass_commissioning_proposals and len(pass_commissioning_proposals) > 0: for commissioning_proposal in pass_commissioning_proposals: - commissioning_proposal_list.append(PassProposal(**commissioning_proposal)) + commissioning_proposal_list.append( + PassProposal(**commissioning_proposal) + ) except ValidationError as error: error_message = f"Error validating commissioning proposal data received from PASS for year {str(year)} at {facility_name} facility." logger.error(error_message) diff --git a/src/nsls2api/services/slack_service.py b/src/nsls2api/services/slack_service.py index 2b61fcf7..66039f44 100644 --- a/src/nsls2api/services/slack_service.py +++ b/src/nsls2api/services/slack_service.py @@ -14,12 +14,14 @@ # 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: """ Retrieves the details of the Slack bot. @@ -113,8 +115,8 @@ async def create_channel( is_private: bool = True, ) -> str | None: """ - Creates a new Slack channel with the given name and privacy settings. If the channel - already exists, it will convert the channel to the desired privacy setting and invite + Creates a new Slack channel with the given name and privacy settings. If the channel + already exists, it will convert the channel to the desired privacy setting and invite the necessary bot user to the channel. Args: @@ -217,7 +219,9 @@ def rename_channel(name: str, new_name: str) -> str | None: if channel_id is None: raise Exception(f"Channel {name} not found.") - response = get_boring_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}") diff --git a/src/nsls2api/services/sync_service.py b/src/nsls2api/services/sync_service.py index 9432d937..fb8fc0a8 100644 --- a/src/nsls2api/services/sync_service.py +++ b/src/nsls2api/services/sync_service.py @@ -8,8 +8,14 @@ from nsls2api.api.models.person_model import ActiveDirectoryUser from nsls2api.models.beamlines import Beamline from nsls2api.models.pass_models import PassCycle, PassProposalType -from nsls2api.services import beamline_service, bnlpeople_service, facility_service, n2sn_service, pass_service, \ - proposal_service +from nsls2api.services import ( + beamline_service, + bnlpeople_service, + facility_service, + n2sn_service, + pass_service, + proposal_service, +) from nsls2api.infrastructure.logging import logger from nsls2api.models.cycles import Cycle @@ -29,33 +35,52 @@ async def worker_synchronize_dataadmins(skip_beamlines=False) -> None: facility_list = await facility_service.all_facilities() for facility in facility_list: logger.info(f"Synchronizing data admins for facility {facility.facility_id}.") - data_admin_group_name = await facility_service.data_admin_group(facility.facility_id) + data_admin_group_name = await facility_service.data_admin_group( + facility.facility_id + ) if data_admin_group_name: - ad_users: list[ActiveDirectoryUser] = await n2sn_service.get_users_in_group(data_admin_group_name) - username_list = [u['sAMAccountName'] for u in ad_users if u['sAMAccountName'] is not None] + ad_users: list[ActiveDirectoryUser] = await n2sn_service.get_users_in_group( + data_admin_group_name + ) + username_list = [ + u["sAMAccountName"] for u in ad_users if u["sAMAccountName"] is not None + ] logger.info(f"Setting user list = {username_list} ") - await facility_service.update_data_admins(facility.facility_id, username_list) + await facility_service.update_data_admins( + facility.facility_id, username_list + ) else: logger.warning( - f"There is no 'data_admin_group' for facility_id={facility.facility_id} defined in the database.") + f"There is no 'data_admin_group' for facility_id={facility.facility_id} defined in the database." + ) time_taken = datetime.datetime.now() - start_time - logger.info(f"Facility Data Admin permissions synchronized in {time_taken.total_seconds():,.2f} seconds") + logger.info( + f"Facility Data Admin permissions synchronized in {time_taken.total_seconds():,.2f} seconds" + ) if skip_beamlines is False: beamline_list: list[Beamline] = await beamline_service.all_beamlines() for beamline in beamline_list: logger.info(f"Synchronizing data admins for beamline {beamline.name}.") - data_admin_group_name = await beamline_service.data_admin_group(beamline.name) - ad_users: list[ActiveDirectoryUser] = await n2sn_service.get_users_in_group(data_admin_group_name) - username_list = [u['sAMAccountName'] for u in ad_users if u['sAMAccountName'] is not None] + data_admin_group_name = await beamline_service.data_admin_group( + beamline.name + ) + ad_users: list[ActiveDirectoryUser] = await n2sn_service.get_users_in_group( + data_admin_group_name + ) + username_list = [ + u["sAMAccountName"] for u in ad_users if u["sAMAccountName"] is not None + ] await beamline_service.update_data_admins(beamline.name, username_list) time_taken = datetime.datetime.now() - start_time - logger.info(f"Beamline Data Admin permissions synchronized in {time_taken.total_seconds():,.2f} seconds") + logger.info( + f"Beamline Data Admin permissions synchronized in {time_taken.total_seconds():,.2f} seconds" + ) async def worker_synchronize_cycles_from_pass( - facility_name: FacilityName = FacilityName.nsls2, + facility_name: FacilityName = FacilityName.nsls2, ) -> None: """ This method synchronizes the cycles for a facility from PASS. @@ -122,14 +147,14 @@ async def worker_synchronize_cycles_from_pass( async def worker_synchronize_proposal_types_from_pass( - facility_name: FacilityName = FacilityName.nsls2, + facility_name: FacilityName = FacilityName.nsls2, ) -> None: start_time = datetime.datetime.now() try: - pass_proposal_types: list[PassProposalType] = await pass_service.get_proposal_types( - facility_name - ) + pass_proposal_types: list[ + PassProposalType + ] = await pass_service.get_proposal_types(facility_name) except pass_service.PassException as error: error_message = ( f"Error retrieving proposal types from PASS for {facility_name} facility." @@ -196,7 +221,9 @@ async def synchronize_proposal_from_pass(proposal_id: str) -> None: saf_list.append( SafetyForm( - saf_id=str(saf.SAF_ID), status=saf.Status, instruments=set(saf_beamline_list) + saf_id=str(saf.SAF_ID), + status=saf.Status, + instruments=set(saf_beamline_list), ) ) @@ -221,7 +248,9 @@ async def synchronize_proposal_from_pass(proposal_id: str) -> None: user_is_pi = True pi_found_in_experimenters = True try: - logger.debug(f"Looking up username for employee/life number = {user.BNL_ID}") + logger.debug( + f"Looking up username for employee/life number = {user.BNL_ID}" + ) bnl_username = await bnlpeople_service.get_username_by_id(user.BNL_ID) logger.debug(f" ---> {bnl_username}") except HTTPStatusError as error: @@ -356,8 +385,8 @@ async def worker_synchronize_proposals_for_cycle_from_pass(cycle: str, async def worker_update_proposal_to_cycle_mapping( - facility: FacilityName = FacilityName.nsls2, - sync_source: JobSyncSource = JobSyncSource.PASS, + facility: FacilityName = FacilityName.nsls2, + sync_source: JobSyncSource = JobSyncSource.PASS, ) -> None: start_time = datetime.datetime.now() diff --git a/src/nsls2api/tests/services/test_facility_service.py b/src/nsls2api/tests/services/test_facility_service.py index 3fa657a5..0d9c35ec 100644 --- a/src/nsls2api/tests/services/test_facility_service.py +++ b/src/nsls2api/tests/services/test_facility_service.py @@ -11,4 +11,4 @@ async def test_facilities_count(): @pytest.mark.anyio async def test_all_facilities(): facilities = await facility_service.all_facilities() - assert type(facilities) == list + assert type(facilities) is list diff --git a/src/nsls2api/viewmodels/proposals/search_viewmodel.py b/src/nsls2api/viewmodels/proposals/search_viewmodel.py index 112ea74f..ba1f523c 100644 --- a/src/nsls2api/viewmodels/proposals/search_viewmodel.py +++ b/src/nsls2api/viewmodels/proposals/search_viewmodel.py @@ -23,7 +23,6 @@ def __init__(self, request: Request): except KeyError: self.search_text = "" - async def load(self): print(f"Searching for {self.search_text}") self.proposals = await proposal_service.search_proposals(self.search_text)