Skip to content

Commit 937da77

Browse files
authored
Merge pull request #5 from als-computing/test_script
10 ot of 10, no notes, ship it, etc.
2 parents 55f2484 + f3fd0e5 commit 937da77

14 files changed

Lines changed: 5982 additions & 664 deletions

File tree

.github/workflows/test_build.yml

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,33 @@ jobs:
1414
runs-on: ubuntu-latest
1515
strategy:
1616
matrix:
17-
python-version: [3.9]
17+
python-version: [3.11, 3.12, 3.13]
1818
fail-fast: false
1919
steps:
2020

21-
- uses: actions/checkout@v2
21+
- uses: actions/checkout@v4
2222

2323
- name: Set up Python ${{ matrix.python-version }}
24-
uses: actions/setup-python@v2
24+
uses: actions/setup-python@v4
2525
with:
2626
python-version: ${{ matrix.python-version }}
2727

28-
- name: Install test requirements
28+
- name: Install uv
29+
uses: astral-sh/setup-uv@v2
30+
31+
- name: Install dependencies
2932
shell: bash -l {0}
3033
run: |
3134
set -vxeuo pipefail
32-
python -m pip install -r requirements.txt
33-
python -m pip install -r requirements-dev.txt
34-
python -m pip list
35+
uv sync --group dev
36+
uv pip list
3537
3638
- name: Test with pytest
3739
shell: bash -l {0}
3840
run: |
3941
set -vxeuo pipefail
40-
coverage run -m pytest -v
41-
coverage report
42+
uv run coverage run -m pytest -v
43+
uv run coverage report
4244
4345
build-and-push-image:
4446
name: Build docker image

Dockerfile

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8
1+
FROM python:3.12-slim
22

3+
# Install uv
4+
RUN pip install --no-cache-dir uv
35

4-
COPY ./requirements.txt /tmp/
5-
RUN pip install -U pip && pip install -r /tmp/requirements.txt
6-
COPY ./ /app
6+
# Set working directory
77
WORKDIR /app
8-
RUN pip install .
8+
9+
# Copy project files
10+
COPY . .
11+
12+
# Install dependencies with uv
13+
RUN uv sync --frozen --no-dev
14+
915
ENV APP_MODULE=splash_userservice.api:app
10-
# CMD ["uvicorn", "splash_userservice.api:app", "--host", "0.0.0.0", "--port", "80"]
16+
EXPOSE 80
17+
18+
CMD ["uv", "run", "uvicorn", "splash_userservice.api:app", "--host", "0.0.0.0", "--port", "80"]

README.md

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,67 @@ have a common way to access user and group information.
66

77
It is intended that the code in [models](./splash_userservice/models.py) and [api](./splash_userservice/api.py) would be the front-end interface, and facility-specific APIs would could then write specific code that maps to those model classes.
88

9-
A fastapi server is included just because it docuemnts APIs so well. You can start it up and browse to the OpenAPI page that it generates:
9+
## Installation
10+
11+
This project uses `uv` for dependency management. Install the project with:
12+
13+
uv sync # Install all dependencies
14+
uv sync --group dev # Install with development dependencies
15+
16+
Or in editable mode:
17+
18+
uv pip install -e .
19+
20+
## Running the API Server
21+
22+
A FastAPI server is included that documents the APIs with an interactive OpenAPI page:
1023

1124
pip install -e .
1225
uvicorn splash_userservice.api:app
1326

1427
Once started, you can navigate to the page at `http://localhost:8000/docs`
1528

29+
## Testing
30+
31+
### Testing with test_user.py
32+
33+
The `scripts/test_user.py` script allows you to test the ALSHubService directly by querying user information:
34+
35+
python scripts/test_user.py <ORCID_OR_EMAIL> [OPTIONS]
36+
37+
**Options:**
38+
39+
- `--type {orcid,email}` or `-t {orcid,email}`: Specify identifier type (default: orcid)
40+
- `--no-groups`: Skip fetching groups, proposals, ESAFs, and beamline roles
41+
42+
**Examples:**
43+
44+
# Fetch user by ORCID
45+
python scripts/test_user.py 0000-0002-1539-0297
46+
47+
# Fetch user by email
48+
python scripts/test_user.py user@example.com --type email
49+
50+
# Fetch user without groups
51+
python scripts/test_user.py 0000-0002-1539-0297 --no-groups
52+
53+
# Get help
54+
python scripts/test_user.py --help
55+
56+
The script outputs the user information as JSON and logs all requests/responses to stderr. To see debug output (full URLs and response bodies), the logging is configured to show ALSHub service debug messages.
57+
58+
### Testing with test_user.sh (bash alternative)
59+
60+
A bash version is also available at `scripts/test_user.sh` for making direct HTTP requests:
61+
62+
./scripts/test_user.sh <ORCID> [LBNL_ID]
63+
64+
**Environment variables:**
65+
66+
- `ALSHUB_BASE`: ALSHub base URL (default: https://alsusweb.lbl.gov)
67+
- `ESAF_BASE`: ESAF base URL (default: https://als-esaf.als.lbl.gov)
68+
- `INSECURE_TLS`: Set to false for strict TLS verification (default: true for insecure)
69+
- `ORCID_ID`: Override ORCID (default: 0000-0000-0000-0000)
70+
- `SKIP_LBNL_REQUIRED_CALLS`: Skip proposals/ESAF calls if LBNLID not found
71+
1672
This project in a very early stage. Te [NSLS-II Scipy Cookiecutter](https://github.com/NSLS-II/scientific-python-cookiecutter) was used to start the project, but much is not yet being taken advantage of (especially documentation).

README.rst

Lines changed: 0 additions & 23 deletions
This file was deleted.

alshub/service.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@
2525

2626
logger = logging.getLogger("users.alshub")
2727

28-
context = ssl.create_default_context()
29-
context.load_verify_locations(cafile="./incommonrsaca.pem")
30-
3128

3229
def info(log, *args):
3330
if logger.isEnabledFor(logging.INFO):
@@ -77,16 +74,22 @@ async def get_user(self, id: str, id_type: IDType, fetch_groups=True) -> User:
7774

7875
user_lb_id = None
7976
groups = set()
80-
async with AsyncClient(base_url=ALSHUB_BASE, verify=context, timeout=10.0) as alsusweb_client:
77+
async with AsyncClient(base_url=ALSHUB_BASE, timeout=10.0) as alsusweb_client:
8178
# query for user information
8279
if id_type == IDType.email:
8380
q_param = "em"
8481
else:
8582
q_param = "or"
83+
url = f"{ALSHUB_PERSON}/?{q_param}={id}"
84+
full_url = f"{ALSHUB_BASE}/{url}"
8685
try:
87-
response = await alsusweb_client.get(f"{ALSHUB_PERSON}/?{q_param}={id}")
86+
debug('Requesting: %s', full_url)
87+
response = await alsusweb_client.get(url)
88+
debug('Response status: %s', response.status_code)
89+
if logger.isEnabledFor(logging.DEBUG) and response.content:
90+
debug('Response body: %s', response.json())
8891
except Exception as e:
89-
raise CommunicationError(f"exception talking to {ALSHUB_PERSON}/?{q_param}={id}") from e
92+
raise CommunicationError(f"exception talking to {url}") from e
9093

9194
if response.status_code == 404:
9295
raise UserNotFound(f'user {id} not found in ALSHub')
@@ -122,7 +125,7 @@ async def get_user(self, id: str, id_type: IDType, fetch_groups=True) -> User:
122125
if proposals:
123126
groups.update(proposals)
124127

125-
async with AsyncClient(base_url=ESAF_BASE, verify=context, timeout=10.0) as esaf_client:
128+
async with AsyncClient(base_url=ESAF_BASE, timeout=10.0) as esaf_client:
126129
esafs = await get_user_esafs(esaf_client, user_lb_id)
127130
if esafs:
128131
groups.update(esafs)
@@ -139,7 +142,11 @@ async def get_user(self, id: str, id_type: IDType, fetch_groups=True) -> User:
139142

140143

141144
async def get_user_proposals(client, lbl_id):
142-
response = await client.get(f"{ALSHUB_PROPOSALBY}/?lb={lbl_id}")
145+
url = f"{ALSHUB_PROPOSALBY}/?lb={lbl_id}"
146+
full_url = f"{ALSHUB_BASE}/{url}"
147+
debug('Requesting: %s', full_url)
148+
response = await client.get(url)
149+
debug('Response status: %s', response.status_code)
143150
if response.is_error:
144151
info('error getting user proposals: %s status code: %s message: %s',
145152
lbl_id,
@@ -148,6 +155,7 @@ async def get_user_proposals(client, lbl_id):
148155
return {}
149156
else:
150157
proposal_response_obj = response.json()
158+
debug('Response body: %s', proposal_response_obj)
151159
proposals = proposal_response_obj.get('Proposals')
152160
if not proposals:
153161
info('no proposals for lbnlid: %s', lbl_id)
@@ -161,14 +169,19 @@ async def get_user_proposals(client, lbl_id):
161169

162170

163171
async def get_user_esafs(client, lbl_id):
164-
response = await client.get(f"{ESAF_INFO}/?lb={lbl_id}")
172+
url = f"{ESAF_INFO}/?lb={lbl_id}"
173+
full_url = f"{ESAF_BASE}/{url}"
174+
debug('Requesting: %s', full_url)
175+
response = await client.get(url)
176+
debug('Response status: %s', response.status_code)
165177
if response.is_error:
166178
info('error getting user esafs: %s status code: %s message: %s',
167179
lbl_id,
168180
response.status_code,
169181
response.json())
170182
else:
171183
esafs = response.json()
184+
debug('Response body: %s', esafs)
172185
if not esafs or len(esafs) == 0:
173186
info('no proposals for lbnlid: %s', lbl_id)
174187
else:
@@ -180,7 +193,11 @@ async def get_user_esafs(client, lbl_id):
180193

181194

182195
async def get_staff_beamlines(ac: AsyncClient, orcid: str, email: str) -> List[str]:
183-
response = await ac.get(f"{ALSHUB_PERSON_ROLES}/?or={orcid}")
196+
url = f"{ALSHUB_PERSON_ROLES}/?or={orcid}"
197+
full_url = f"{ALSHUB_BASE}/{url}"
198+
debug('Requesting: %s', full_url)
199+
response = await ac.get(url)
200+
debug('Response status: %s', response.status_code)
184201
# ADMINS are a list maintained in a python to add users to groups even if they're not maintained in
185202
# ALSHub
186203
beamlines = set()
@@ -192,7 +209,9 @@ async def get_staff_beamlines(ac: AsyncClient, orcid: str, email: str) -> List[s
192209
info(f"error asking ALHub for staff roles {orcid}")
193210
return beamlines
194211
if response.content:
195-
beamline_roles = response.json().get("Beamline Roles")
212+
response_data = response.json()
213+
debug('Response body: %s', response_data)
214+
beamline_roles = response_data.get("Beamline Roles")
196215
if beamline_roles:
197216
alshub_beamlines = alshub_roles_to_beamline_groups(
198217
beamline_roles,

pyproject.toml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "splash-userservice"
7+
version = "0.0.1.dev0"
8+
description = "API for defining users and groups at scientific user facilities"
9+
readme = "README.md"
10+
requires-python = ">=3.7"
11+
license = {text = "BSD-3-Clause"}
12+
authors = [
13+
{name = "Dylan McReynolds", email = "dmcreynolds@lbl.gov"}
14+
]
15+
classifiers = [
16+
"Development Status :: 2 - Pre-Alpha",
17+
"Natural Language :: English",
18+
"Programming Language :: Python :: 3",
19+
"Programming Language :: Python :: 3.7",
20+
"Programming Language :: Python :: 3.8",
21+
"Programming Language :: Python :: 3.9",
22+
"Programming Language :: Python :: 3.10",
23+
"Programming Language :: Python :: 3.11",
24+
]
25+
26+
dependencies = [
27+
"fastapi",
28+
"uvicorn",
29+
"certifi",
30+
"httpx>=0.24",
31+
"pydantic",
32+
"starlette",
33+
"typer",
34+
]
35+
36+
dynamic = []
37+
38+
[project.optional-dependencies]
39+
dev = [
40+
"codecov",
41+
"coverage",
42+
"flake8",
43+
"pytest",
44+
"sphinx",
45+
"ipython",
46+
"matplotlib",
47+
"numpydoc",
48+
"sphinx-copybutton",
49+
"sphinx_rtd_theme",
50+
]
51+
52+
[dependency-groups]
53+
dev = [
54+
"codecov",
55+
"coverage",
56+
"flake8",
57+
"pytest",
58+
"sphinx",
59+
"ipython",
60+
"matplotlib",
61+
"numpydoc",
62+
"sphinx-copybutton",
63+
"sphinx_rtd_theme",
64+
]
65+
66+
[project.urls]
67+
Repository = "https://github.com/als-computing/splash-userservice"
68+
Documentation = "https://splash-userservice.readthedocs.io"
69+
70+
[tool.hatch.build.targets.wheel]
71+
packages = ["splash_userservice", "alshub"]
72+
73+
[tool.pytest.ini_options]
74+
testpaths = ["splash_userservice/tests", "alshub/tests"]
75+
addopts = "-v"
76+
77+
[tool.coverage.run]
78+
source = ["splash_userservice", "alshub"]
79+
omit = [
80+
"*/tests/*",
81+
"*/conftest.py",
82+
]
83+
84+
[tool.flake8]
85+
max-line-length = 120
86+
exclude = [".git", "__pycache__", "build", "dist", ".venv"]
87+
ignore = ["E203", "W503"]

requirements-dev.txt

Lines changed: 0 additions & 13 deletions
This file was deleted.

requirements.txt

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)