Skip to content

Commit 2de153e

Browse files
authored
Merge branch 'main' into pytest-timeout
2 parents bf71a35 + 28f6254 commit 2de153e

19 files changed

Lines changed: 1965 additions & 1677 deletions

File tree

.github/workflows/asyncapi.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ on:
99
tags:
1010
- '*'
1111
pull_request:
12+
merge_group:
13+
types: [checks_requested]
1214

1315
jobs:
1416
validate:

.github/workflows/backstage.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ on:
77
tags:
88
- '*'
99
pull_request:
10+
merge_group:
11+
types: [checks_requested]
1012

1113
jobs:
1214
validate:

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ on:
77
tags:
88
- "*"
99
pull_request:
10+
merge_group:
11+
types: [checks_requested]
1012

1113
jobs:
1214
lint:

.github/workflows/codeql.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ on:
1818
tags:
1919
- '*'
2020
pull_request:
21-
21+
merge_group:
22+
types: [checks_requested]
2223

2324
jobs:
2425
analyze:
@@ -61,7 +62,7 @@ jobs:
6162

6263
# Initializes the CodeQL tools for scanning.
6364
- name: Initialize CodeQL
64-
uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4
65+
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
6566
with:
6667
languages: ${{ matrix.language }}
6768
build-mode: ${{ matrix.build-mode }}
@@ -88,6 +89,6 @@ jobs:
8889
echo ' make release'
8990
exit 1
9091
- name: Perform CodeQL Analysis
91-
uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4
92+
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
9293
with:
9394
category: "/language:${{matrix.language}}"

.github/workflows/pr.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ name: PR Conventional Commit Validation
33
on:
44
pull_request:
55
types: [opened, synchronize, reopened, edited]
6+
merge_group:
7+
types: [checks_requested]
68

79
jobs:
810
validate-pr-title:
911
runs-on: ubuntu-latest
1012
steps:
1113
- name: PR Conventional Commit Validation
12-
uses: ytanikin/[email protected].1
14+
uses: ytanikin/[email protected].2
1315
with:
1416
task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert", "depr"]'
15-
add_label: 'false'
17+
add_label: "false"

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ COPY --from=build /python /python
6464

6565
# Copy the environment, but not the source code
6666
COPY --chown=1000:1000 --from=build /app/.venv /app/.venv
67-
RUN chmod o+wrX /app/.venv
67+
RUN chmod -R 777 /app
6868
ENV PATH=/app/.venv/bin:$PATH
6969

7070
# Add copy of blueapi source to container for debugging

docs/tutorials/scripting-plans.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ This will be one of `WorkerEvent`, `ProgressEvent` or `DataEvent`.
160160
An example that prints data for each point could be something like
161161

162162
```python
163+
from blueapi.core.bluesky_types import DataEvent
164+
163165
def feedback(evt):
164166
match evt:
165167
case DataEvent(name="start"):

helm/blueapi/templates/statefulset.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ spec:
8383
{{- if .Values.initContainer.enabled }}
8484
initContainers:
8585
- name: setup-scratch
86+
{{- with .Values.securityContext }}
87+
securityContext:
88+
{{- toYaml . | nindent 10 }}
89+
{{- end }}
8690
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
8791
imagePullPolicy: {{ .Values.image.pullPolicy }}
8892
resources:

src/blueapi/cli/cli.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from blueapi.client.rest import (
2727
BlueskyRemoteControlError,
2828
InvalidParametersError,
29+
NonJsonResponseError,
2930
ServiceUnavailableError,
3031
UnauthorisedAccessError,
3132
UnknownPlanError,
@@ -83,8 +84,24 @@ def is_str_dict(val: Any) -> TypeGuard[TaskParameters]:
8384
@click.option(
8485
"-c", "--config", type=Path, help="Path to configuration YAML file", multiple=True
8586
)
87+
@click.option(
88+
"-v",
89+
"--verbose",
90+
"log_level",
91+
flag_value="DEBUG",
92+
help="Include DEBUG level logging output",
93+
)
94+
@click.option(
95+
"-q",
96+
"--quiet",
97+
"log_level",
98+
flag_value="ERROR",
99+
help="Reduce logging noise to only show errors",
100+
)
86101
@click.pass_context
87-
def main(ctx: click.Context, config: tuple[Path, ...]) -> None:
102+
def main(
103+
ctx: click.Context, config: tuple[Path, ...], log_level: str | None = None
104+
) -> None:
88105
# if no command is supplied, run with the options passed
89106

90107
# Set umask to DLS standard
@@ -96,6 +113,9 @@ def main(ctx: click.Context, config: tuple[Path, ...]) -> None:
96113
except FileNotFoundError as fnfe:
97114
raise ClickException(f"Config file not found: {fnfe.filename}") from fnfe
98115

116+
if log_level:
117+
config_loader.use_values({"logging": {"level": log_level}})
118+
99119
loaded_config: ApplicationConfig = config_loader.load()
100120

101121
set_up_logging(loaded_config.logging)
@@ -494,13 +514,16 @@ def login(obj: dict) -> None:
494514
print("Logged in")
495515
except Exception:
496516
client = BlueapiClient.from_config(config)
497-
if oidc := client.oidc_config:
498-
auth = SessionManager(
499-
oidc, cache_manager=SessionCacheManager(config.auth_token_path)
500-
)
501-
auth.start_device_flow()
502-
else:
503-
print("Server is not configured to use authentication!")
517+
try:
518+
if oidc := client.oidc_config:
519+
auth = SessionManager(
520+
oidc, cache_manager=SessionCacheManager(config.auth_token_path)
521+
)
522+
auth.start_device_flow()
523+
else:
524+
print("Server is not configured to use authentication!")
525+
except NonJsonResponseError as e:
526+
print(str(e))
504527

505528

506529
@main.command(name="logout")

src/blueapi/client/rest.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
import logging
13
from collections.abc import Callable, Mapping
24
from typing import Any, Literal, TypeVar
35

@@ -10,6 +12,7 @@
1012
)
1113
from pydantic import BaseModel, TypeAdapter, ValidationError
1214

15+
from blueapi import __version__
1316
from blueapi.config import RestConfig
1417
from blueapi.service.authentication import JWTAuth, SessionManager
1518
from blueapi.service.model import (
@@ -32,6 +35,8 @@
3235

3336
TRACER = get_tracer("rest")
3437

38+
LOGGER = logging.getLogger(__name__)
39+
3540

3641
class UnauthorisedAccessError(Exception):
3742
pass
@@ -41,6 +46,10 @@ class BlueskyRemoteControlError(Exception):
4146
pass
4247

4348

49+
class NonJsonResponseError(Exception):
50+
pass
51+
52+
4453
class BlueskyRequestError(Exception):
4554
def __init__(self, code: int, message: str) -> None:
4655
super().__init__(message, code)
@@ -105,7 +114,7 @@ def _exception(response: requests.Response) -> Exception | None:
105114
if code < 400:
106115
return None
107116
elif code == 404:
108-
return KeyError(str(response.json()))
117+
return KeyError(str(_response_json(response)))
109118
else:
110119
return BlueskyRemoteControlError(code, str(response))
111120

@@ -120,7 +129,7 @@ def _create_task_exceptions(response: requests.Response) -> Exception | None:
120129
return UnknownPlanError()
121130
elif code == 422:
122131
try:
123-
content = response.json()
132+
content = _response_json(response)
124133
return InvalidParametersError(
125134
TypeAdapter(list[ParameterError]).validate_python(
126135
content.get("detail", [])
@@ -134,6 +143,18 @@ def _create_task_exceptions(response: requests.Response) -> Exception | None:
134143
return BlueskyRequestError(code, response.text)
135144

136145

146+
def _response_json(response: requests.Response) -> Any:
147+
try:
148+
return response.json()
149+
except json.decoder.JSONDecodeError as exc:
150+
LOGGER.debug(
151+
f"Invalid json response from <{response.request.url}>: <{response.content}>"
152+
)
153+
raise NonJsonResponseError(
154+
"Response does not contain a valid JSON object"
155+
) from exc
156+
157+
137158
class BlueapiRestClient:
138159
_config: RestConfig
139160
_session_manager: SessionManager | None
@@ -271,7 +292,20 @@ def _request_and_deserialize(
271292
raise exception
272293
if response.status_code == status.HTTP_204_NO_CONTENT:
273294
raise NoContentError(target_type)
274-
deserialized = TypeAdapter(target_type).validate_python(response.json())
295+
if (server_version := response.headers.get("x-blueapi-version")) is not None:
296+
from packaging.version import Version
297+
298+
if (server_version := Version(server_version).base_version) != (
299+
client_version := Version(__version__).base_version
300+
):
301+
LOGGER.warning(
302+
f"Version mismatch: Blueapi server version is {server_version} "
303+
f"but client version is {client_version}. "
304+
f"Some features may not work as expected."
305+
)
306+
deserialized = TypeAdapter(target_type).validate_python(
307+
_response_json(response)
308+
)
275309
return deserialized
276310

277311

0 commit comments

Comments
 (0)