Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions .github/workflows/test_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,33 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]
python-version: [3.11, 3.12, 3.13]
fail-fast: false
steps:

- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install test requirements
- name: Install uv
uses: astral-sh/setup-uv@v2

- name: Install dependencies
shell: bash -l {0}
run: |
set -vxeuo pipefail
python -m pip install -r requirements.txt
python -m pip install -r requirements-dev.txt
python -m pip list
uv sync --group dev
uv pip list

- name: Test with pytest
shell: bash -l {0}
run: |
set -vxeuo pipefail
coverage run -m pytest -v
coverage report
uv run coverage run -m pytest -v
uv run coverage report

build-and-push-image:
name: Build docker image
Expand Down
20 changes: 14 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8
FROM python:3.12-slim

# Install uv
RUN pip install --no-cache-dir uv

COPY ./requirements.txt /tmp/
RUN pip install -U pip && pip install -r /tmp/requirements.txt
COPY ./ /app
# Set working directory
WORKDIR /app
RUN pip install .

# Copy project files
COPY . .

# Install dependencies with uv
RUN uv sync --frozen --no-dev

ENV APP_MODULE=splash_userservice.api:app
# CMD ["uvicorn", "splash_userservice.api:app", "--host", "0.0.0.0", "--port", "80"]
EXPOSE 80

CMD ["uv", "run", "uvicorn", "splash_userservice.api:app", "--host", "0.0.0.0", "--port", "80"]
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,67 @@ have a common way to access user and group information.

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.

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:
## Installation

This project uses `uv` for dependency management. Install the project with:

uv sync # Install all dependencies
uv sync --group dev # Install with development dependencies

Or in editable mode:

uv pip install -e .

## Running the API Server

A FastAPI server is included that documents the APIs with an interactive OpenAPI page:

pip install -e .
uvicorn splash_userservice.api:app

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

## Testing

### Testing with test_user.py

The `scripts/test_user.py` script allows you to test the ALSHubService directly by querying user information:

python scripts/test_user.py <ORCID_OR_EMAIL> [OPTIONS]

**Options:**

- `--type {orcid,email}` or `-t {orcid,email}`: Specify identifier type (default: orcid)
- `--no-groups`: Skip fetching groups, proposals, ESAFs, and beamline roles

**Examples:**

# Fetch user by ORCID
python scripts/test_user.py 0000-0002-1539-0297

# Fetch user by email
python scripts/test_user.py user@example.com --type email

# Fetch user without groups
python scripts/test_user.py 0000-0002-1539-0297 --no-groups

# Get help
python scripts/test_user.py --help

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.

### Testing with test_user.sh (bash alternative)

A bash version is also available at `scripts/test_user.sh` for making direct HTTP requests:

./scripts/test_user.sh <ORCID> [LBNL_ID]

**Environment variables:**

- `ALSHUB_BASE`: ALSHub base URL (default: https://alsusweb.lbl.gov)
- `ESAF_BASE`: ESAF base URL (default: https://als-esaf.als.lbl.gov)
- `INSECURE_TLS`: Set to false for strict TLS verification (default: true for insecure)
- `ORCID_ID`: Override ORCID (default: 0000-0000-0000-0000)
- `SKIP_LBNL_REQUIRED_CALLS`: Skip proposals/ESAF calls if LBNLID not found

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).
23 changes: 0 additions & 23 deletions README.rst

This file was deleted.

41 changes: 30 additions & 11 deletions alshub/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@

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

context = ssl.create_default_context()
context.load_verify_locations(cafile="./incommonrsaca.pem")


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

user_lb_id = None
groups = set()
async with AsyncClient(base_url=ALSHUB_BASE, verify=context, timeout=10.0) as alsusweb_client:
async with AsyncClient(base_url=ALSHUB_BASE, timeout=10.0) as alsusweb_client:
# query for user information
if id_type == IDType.email:
q_param = "em"
else:
q_param = "or"
url = f"{ALSHUB_PERSON}/?{q_param}={id}"
full_url = f"{ALSHUB_BASE}/{url}"
try:
response = await alsusweb_client.get(f"{ALSHUB_PERSON}/?{q_param}={id}")
debug('Requesting: %s', full_url)
response = await alsusweb_client.get(url)
debug('Response status: %s', response.status_code)
if logger.isEnabledFor(logging.DEBUG) and response.content:
debug('Response body: %s', response.json())
except Exception as e:
raise CommunicationError(f"exception talking to {ALSHUB_PERSON}/?{q_param}={id}") from e
raise CommunicationError(f"exception talking to {url}") from e

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

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


async def get_user_proposals(client, lbl_id):
response = await client.get(f"{ALSHUB_PROPOSALBY}/?lb={lbl_id}")
url = f"{ALSHUB_PROPOSALBY}/?lb={lbl_id}"
full_url = f"{ALSHUB_BASE}/{url}"
debug('Requesting: %s', full_url)
response = await client.get(url)
debug('Response status: %s', response.status_code)
if response.is_error:
info('error getting user proposals: %s status code: %s message: %s',
lbl_id,
Expand All @@ -148,6 +155,7 @@ async def get_user_proposals(client, lbl_id):
return {}
else:
proposal_response_obj = response.json()
debug('Response body: %s', proposal_response_obj)
proposals = proposal_response_obj.get('Proposals')
if not proposals:
info('no proposals for lbnlid: %s', lbl_id)
Expand All @@ -161,14 +169,19 @@ async def get_user_proposals(client, lbl_id):


async def get_user_esafs(client, lbl_id):
response = await client.get(f"{ESAF_INFO}/?lb={lbl_id}")
url = f"{ESAF_INFO}/?lb={lbl_id}"
full_url = f"{ESAF_BASE}/{url}"
debug('Requesting: %s', full_url)
response = await client.get(url)
debug('Response status: %s', response.status_code)
if response.is_error:
info('error getting user esafs: %s status code: %s message: %s',
lbl_id,
response.status_code,
response.json())
else:
esafs = response.json()
debug('Response body: %s', esafs)
if not esafs or len(esafs) == 0:
info('no proposals for lbnlid: %s', lbl_id)
else:
Expand All @@ -180,7 +193,11 @@ async def get_user_esafs(client, lbl_id):


async def get_staff_beamlines(ac: AsyncClient, orcid: str, email: str) -> List[str]:
response = await ac.get(f"{ALSHUB_PERSON_ROLES}/?or={orcid}")
url = f"{ALSHUB_PERSON_ROLES}/?or={orcid}"
full_url = f"{ALSHUB_BASE}/{url}"
debug('Requesting: %s', full_url)
response = await ac.get(url)
debug('Response status: %s', response.status_code)
# ADMINS are a list maintained in a python to add users to groups even if they're not maintained in
# ALSHub
beamlines = set()
Expand All @@ -192,7 +209,9 @@ async def get_staff_beamlines(ac: AsyncClient, orcid: str, email: str) -> List[s
info(f"error asking ALHub for staff roles {orcid}")
return beamlines
if response.content:
beamline_roles = response.json().get("Beamline Roles")
response_data = response.json()
debug('Response body: %s', response_data)
beamline_roles = response_data.get("Beamline Roles")
if beamline_roles:
alshub_beamlines = alshub_roles_to_beamline_groups(
beamline_roles,
Expand Down
87 changes: 87 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "splash-userservice"
version = "0.0.1.dev0"
description = "API for defining users and groups at scientific user facilities"
readme = "README.md"
requires-python = ">=3.7"
license = {text = "BSD-3-Clause"}
authors = [
{name = "Dylan McReynolds", email = "dmcreynolds@lbl.gov"}
]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Natural Language :: English",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]

dependencies = [
"fastapi",
"uvicorn",
"certifi",
"httpx>=0.24",
"pydantic",
"starlette",
"typer",
]

dynamic = []

[project.optional-dependencies]
dev = [
"codecov",
"coverage",
"flake8",
"pytest",
"sphinx",
"ipython",
"matplotlib",
"numpydoc",
"sphinx-copybutton",
"sphinx_rtd_theme",
]

[dependency-groups]
dev = [
"codecov",
"coverage",
"flake8",
"pytest",
"sphinx",
"ipython",
"matplotlib",
"numpydoc",
"sphinx-copybutton",
"sphinx_rtd_theme",
]

[project.urls]
Repository = "https://github.com/als-computing/splash-userservice"
Documentation = "https://splash-userservice.readthedocs.io"

[tool.hatch.build.targets.wheel]
packages = ["splash_userservice", "alshub"]

[tool.pytest.ini_options]
testpaths = ["splash_userservice/tests", "alshub/tests"]
addopts = "-v"

[tool.coverage.run]
source = ["splash_userservice", "alshub"]
omit = [
"*/tests/*",
"*/conftest.py",
]

[tool.flake8]
max-line-length = 120
exclude = [".git", "__pycache__", "build", "dist", ".venv"]
ignore = ["E203", "W503"]
13 changes: 0 additions & 13 deletions requirements-dev.txt

This file was deleted.

5 changes: 0 additions & 5 deletions requirements.txt

This file was deleted.

Loading
Loading