Skip to content
Open
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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ ENV UV_PYTHON_INSTALL_DIR=/python

# Sync the project without its dev dependencies
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-editable --no-dev --managed-python
uv sync --locked --no-editable --no-dev --extra server --managed-python

RUN uv pip install debugpy

Expand Down
31 changes: 20 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,25 @@ classifiers = [
]
description = "Lightweight bluesky-as-a-service wrapper application. Also usable as a library."
dependencies = [
"tiled[client]>=0.2.4",
# Base install is the `blueapi` CLI/client (and usable as a library on its own).
# Add blueapi[server] to also run `blueapi serve`.
"bluesky[plotting]>=1.14.0", # plotting includes matplotlib, required for BestEffortCallback in run plans
"ophyd-async>=0.13.5",
"aioca",
"pydantic>=2.0",
"pydantic-settings",
"stomp-py",
"PyYAML>=6.0.2",
"click>=8.2.0",
"fastapi>=0.112.0",
"uvicorn>=0.52.1",
"click>=8.2.0", # need for blueapi serve and blueapi setup-scratch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

click is needed for the whole CLI isn't it?

Suggested change
"click>=8.2.0", # need for blueapi serve and blueapi setup-scratch
"click>=8.2.0",

"requests",
"GitPython>=3.1.58", #security base min https://github.com/advisories/GHSA-4gmw-gg2m-w46p
"GitPython>=3.1.58", #security base min https://github.com/advisories/GHSA-4gmw-gg2m-w46p - used by the server's /python_environment endpoint and the CLI's scratch setup
"event-model==1.24.0", # https://github.com/DiamondLightSource/blueapi/issues/684
"bluesky-stomp>=0.1.6",
"opentelemetry-distro>=0.48b0",
"opentelemetry-instrumentation-fastapi>=0.48b0",
"observability-utils>=0.1.4",
"pyjwt[crypto]",
"tomlkit",
"graypy>=2.1.0",
"httpx>=0.28.1",
"aiohttp>=3.13.5",
"websockets",
"tqdm",
"packaging",
]
dynamic = ["version"]
license.file = "LICENSE"
Expand All @@ -45,9 +41,22 @@ requires-python = ">=3.11"

[project.optional-dependencies]
demo = ["dls-dodal>=1.69.0", "ophyd-async[sim]"]
# Only needed to run `blueapi serve` (the FastAPI worker service).
server = [
"fastapi>=0.112.0",
"uvicorn>=0.52.1",
"aiohttp>=3.13.5",
"opentelemetry-instrumentation-fastapi>=0.48b0",
"graypy>=2.1.0",
"aioca",
"tiled[client]>=0.2.4",
"httpx>=0.28.1", # used by service/numtracker.py and service/authentication.py's TiledAuth
"tomlkit",
]

[dependency-groups]
dev = [
"blueapi[server]",
"ophyd_async[sim]",
"copier",
"dls-dodal>=1.69.0",
Expand Down
2 changes: 1 addition & 1 deletion src/blueapi/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@
UnauthorisedAccessError,
UnknownPlanError,
)
from blueapi.client.session import SessionCacheManager, SessionManager
from blueapi.config import (
ApplicationConfig,
ConfigLoader,
)
from blueapi.core import OTLP_EXPORT_ENABLED, DataEvent
from blueapi.log import set_up_logging
from blueapi.service.authentication import SessionCacheManager, SessionManager
from blueapi.service.model import DeviceResponse, PlanResponse, SourceInfo, TaskRequest
from blueapi.worker import ProgressEvent, WorkerEvent
from blueapi.worker.event import TaskError, TaskResult
Expand Down
91 changes: 0 additions & 91 deletions src/blueapi/cli/scratch.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import importlib.metadata
import logging
import os
import stat
Expand All @@ -8,10 +7,8 @@
from subprocess import Popen

from git import Repo
from tomlkit import parse

from blueapi.config import FORBIDDEN_OWN_REMOTE_URL, ScratchConfig
from blueapi.service.model import PackageInfo, PythonEnvironmentResponse, SourceInfo
from blueapi.utils import get_owner_gid, is_sgid_set

_DEFAULT_INSTALL_TIMEOUT: float = 300.0
Expand Down Expand Up @@ -178,91 +175,3 @@ def _validate_directory(path: Path) -> None:
raise KeyError(f"{path}: No such file or directory")
elif path.is_file():
raise KeyError(f"{path}: Is a file, not a directory")


def _get_project_name_from_pyproject(path: Path) -> str:
pyproject_path = path / "pyproject.toml"
if pyproject_path.exists():
with pyproject_path.open("r", encoding="utf-8") as file:
toml_data = parse(file.read())
return toml_data.get("project", {}).get("name", "")
return ""


def _fetch_installed_packages_details() -> list[PackageInfo]:
installed_packages = importlib.metadata.distributions()
return [
PackageInfo(
name=dist.metadata["Name"],
version=dist.version,
location=str(dist.locate_file("")),
is_dirty=False,
)
for dist in installed_packages
]


def get_python_environment(
config: ScratchConfig | None,
name: str | None = None,
source: SourceInfo | None = None,
) -> PythonEnvironmentResponse:
"""
Get the Python environment. This includes all installed packages and
the scratch packages.
"""
scratch_packages = {}
packages = []

if config is None:
python_env_response = PythonEnvironmentResponse(scratch_enabled=False)
else:
python_env_response = PythonEnvironmentResponse(scratch_enabled=True)
_validate_directory(config.root)
for repo in config.repositories:
local_directory = config.root / repo.name
repo = Repo(local_directory)
try:
branch = repo.active_branch.name
except TypeError:
branch = repo.head.commit.hexsha

is_dirty = repo.is_dirty()

version = (
f"{repo.remotes[0].url} @{branch}"
if repo.remotes
else f"UNKNOWN REMOTE @{branch}"
)
package_name = _get_project_name_from_pyproject(local_directory)
package_location = ""

packages.append(
PackageInfo(
name=package_name,
version=version,
location=package_location,
source=SourceInfo.SCRATCH,
is_dirty=is_dirty,
)
)
scratch_packages = {p.name: p for p in packages}

for pkg in _fetch_installed_packages_details():
if pkg.name not in scratch_packages:
packages.append(pkg)
else:
scratch_packages[pkg.name].location += f"{pkg.location} &&"

python_env_response.installed_packages = sorted(
packages, key=lambda pkg: pkg.name.lower()
)
if name:
python_env_response.installed_packages = [
p for p in python_env_response.installed_packages if p.name == name
]
if source:
python_env_response.installed_packages = [
p for p in python_env_response.installed_packages if p.source == source
]
return python_env_response
2 changes: 1 addition & 1 deletion src/blueapi/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
start_as_current_span,
)

from blueapi.client.session import SessionCacheManager, SessionManager
from blueapi.config import (
ApplicationConfig,
ConfigLoader,
MissingStompConfigurationError,
)
from blueapi.core.bluesky_types import DataEvent
from blueapi.service.authentication import SessionCacheManager, SessionManager
from blueapi.service.model import (
DeviceModel,
DeviceResponse,
Expand Down
6 changes: 3 additions & 3 deletions src/blueapi/client/rest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import json
import logging
from collections.abc import Callable, Iterable, Mapping
from http import HTTPStatus
from typing import Any, Literal, TypeVar

import requests
from fastapi import status
from observability_utils.tracing import (
get_context_propagator,
get_tracer,
Expand All @@ -17,9 +17,9 @@

from blueapi import __version__
from blueapi.client import client
from blueapi.client.session import JWTAuth, SessionManager
from blueapi.config import RestConfig
from blueapi.core.bluesky_types import DataEvent
from blueapi.service.authentication import JWTAuth, SessionManager
from blueapi.service.model import (
DeviceModel,
DeviceResponse,
Expand Down Expand Up @@ -338,7 +338,7 @@ def _request_and_deserialize(
exception = get_exception(response)
if exception is not None:
raise exception
if response.status_code == status.HTTP_204_NO_CONTENT:
if response.status_code == HTTPStatus.NO_CONTENT:
raise NoContentError(target_type)
if (server_version := response.headers.get("x-blueapi-version")) is not None:
from packaging.version import Version
Expand Down
Loading