Skip to content

Commit 8c1fc15

Browse files
refactor(utils): introduce library mode
refactor(utils): don't initialize sentry, logfire, ssl trust store or certifi in library mode chore(deps): bump refactor(utils): lower log level of boot message
1 parent e11b2a6 commit 8c1fc15

File tree

9 files changed

+1083
-1014
lines changed

9 files changed

+1083
-1014
lines changed

pyproject.toml

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ requires-python = ">=3.11, <3.14"
7474

7575
dependencies = [
7676
# From Template
77-
"fastapi[standard,all]>=0.120.4,<1",
77+
"fastapi[standard,all]>=0.121.1,<1",
7878
"humanize>=4.14.0,<5",
7979
"logfire[system-metrics]>=4.14.2,<5",
8080
"nicegui[native]>=3.1.0,<3.2.0", # Regression in 3.2.0
@@ -88,31 +88,32 @@ dependencies = [
8888
"opentelemetry-instrumentation-urllib3>=0.53b0,<1",
8989
"packaging>=25.0,<26",
9090
"platformdirs>=4.5.0,<5",
91-
"psutil>=7.1.2,<8",
92-
"pydantic-settings>=2.11.0,<3",
91+
"psutil>=7.1.3,<8",
92+
"pydantic-settings>=2.12.0,<3",
9393
"pywin32>=310,<311 ; sys_platform == 'win32'",
9494
"pyyaml>=6.0.3,<7",
95-
"sentry-sdk>=2.43.0,<3",
95+
"sentry-sdk>=2.44.0,<3",
9696
"typer>=0.20.0,<1",
9797
"uptime>=3.0.1,<4",
9898
# Custom
9999
"aiopath>=0.6.11,<1",
100-
"boto3>=1.40.64,<2",
101-
"certifi>=2025.10.5,<2026",
100+
"boto3>=1.40.61,<2",
101+
"certifi>=2025.11.12,<2026",
102102
"defusedxml>=0.7.1",
103103
"dicom-validator>=0.7.3,<1",
104104
"dicomweb-client[gcp]>=0.59.3,<1",
105105
"duckdb>=0.10.0,<=1.4.1",
106106
"fastparquet>=2024.11.0,<2025",
107-
"google-cloud-storage>=3.4.1,<4",
107+
"google-cloud-storage>=3.5.0,<4",
108108
"google-crc32c>=1.7.1,<2",
109109
"highdicom>=0.26.1,<1",
110110
"html-sanitizer>=2.6.0,<3",
111111
"httpx>=0.28.1,<1",
112-
"idc-index-data==22.0.2",
112+
"idc-index-data==22.1.2",
113113
"ijson>=3.4.0.post0,<4",
114114
"jsf>=0.11.2,<1",
115115
"jsonschema[format-nongpl]>=4.25.1,<5",
116+
"loguru>=0.7.3",
116117
"openslide-bin>=4.0.0.8,<5",
117118
"openslide-python>=1.4.2,<2",
118119
"pandas>=2.3.3,<3",
@@ -138,8 +139,8 @@ pyinstaller = ["pyinstaller>=6.14.0,<7"]
138139
jupyter = ["jupyter>=1.1.1,<2"]
139140
marimo = [
140141
"cloudpathlib>=0.23.0,<1",
141-
"ipython>=9.6.0,<10",
142-
"marimo>=0.17.2,<1",
142+
"ipython>=9.7.0,<10",
143+
"marimo>=0.17.7,<1",
143144
"matplotlib>=3.10.7,<4",
144145
"shapely>=2.1.0,<3",
145146
]
@@ -154,15 +155,15 @@ dev = [
154155
"furo>=2025.9.25,<2026",
155156
"git-cliff>=2.10.1,<3",
156157
"mypy>=1.18.2,<2",
157-
"nox[uv]>=2025.10.16,<2026",
158+
"nox[uv]>=2025.11.12,<2026",
158159
"pip-audit>=2.9.0,<3",
159160
"pip-licenses @ git+https://github.com/neXenio/pip-licenses.git@master", # https://github.com/raimon49/pip-licenses/pull/224
160-
"pre-commit>=4.3.0,<5",
161+
"pre-commit>=4.4.0,<5",
161162
"pyright>=1.1.406,<1.1.407", # Regression in 1.1.407, see https://github.com/microsoft/pyright/issues/11060
162163
"pytest>=8.4.2,<9",
163-
"pytest-asyncio>=1.2.0,<2",
164+
"pytest-asyncio>=1.3.0,<2",
164165
"pytest-cov>=7.0.0,<8",
165-
"pytest-docker>=3.2.3,<4",
166+
"pytest-docker>=3.2.5,<4",
166167
"pytest-durations>=1.6.1,<2",
167168
"pytest-env>=1.2.0,<2",
168169
"pytest-md-report>=0.7.0,<1",
@@ -173,7 +174,7 @@ dev = [
173174
"pytest-timeout>=2.4.0,<3",
174175
"pytest-watcher>=0.4.3,<1",
175176
"pytest-xdist[psutil]>=3.8.0,<4",
176-
"ruff>=0.14.3,<1",
177+
"ruff>=0.14.4,<1",
177178
"scalene>=1.5.55,<2",
178179
"sphinx>=8.2.3,<9",
179180
"sphinx-autobuild>=2025.8.25,<2026",

src/aignostics/utils/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
__env__,
1212
__env_file__,
1313
__is_development_mode__,
14+
__is_library_mode__,
1415
__is_running_in_container__,
1516
__is_running_in_read_only_environment__,
17+
__is_testing_mode__,
1618
__project_name__,
1719
__project_path__,
1820
__repository_url__,
@@ -45,8 +47,10 @@
4547
"__env__",
4648
"__env_file__",
4749
"__is_development_mode__",
50+
"__is_library_mode__",
4851
"__is_running_in_container__",
4952
"__is_running_in_read_only_environment__",
53+
"__is_testing_mode__",
5054
"__project_name__",
5155
"__project_path__",
5256
"__repository_url__",

src/aignostics/utils/_constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
__is_development_mode__ = "uvx" not in sys.argv[0].lower()
2020
__is_running_in_container__ = os.getenv(f"{__project_name__.upper()}_RUNNING_IN_CONTAINER")
2121

22+
# Detect if we're runnning as CLI (not during doc generation or testing)
23+
# Check if sys.argv[0] ends with project name (the actual CLI executable)
24+
__is_cli_execution__ = sys.argv[0].endswith(__project_name__) or (len(sys.argv) > 1 and sys.argv[1] == __project_name__)
25+
__is_library_mode__ = not __is_cli_execution__ and not os.getenv("PYTEST_RUNNING_AIGNOSTICS")
26+
__is_testing_mode__ = "pytest" in sys.modules and os.getenv("PYTEST_RUNNING_AIGNOSTICS")
27+
2228
# Determine if we're running in a read-only runtime environment
2329
READ_ONLY_ENV_INDICATORS = [
2430
f"{__project_name__.upper()}_RUNNING_IN_CONTAINER",

src/aignostics/utils/_logfire.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pydantic import BeforeValidator, Field, PlainSerializer, SecretStr
88
from pydantic_settings import SettingsConfigDict
99

10-
from ._constants import __env__, __env_file__, __project_name__, __repository_url__, __version__
10+
from ._constants import __env__, __env_file__, __is_library_mode__, __project_name__, __repository_url__, __version__
1111
from ._settings import OpaqueSettings, load_settings, strip_to_none_before_validator
1212

1313

@@ -51,6 +51,9 @@ def logfire_initialize(modules: list["str"]) -> bool:
5151
Returns:
5252
bool: True if initialized successfully False otherwise
5353
"""
54+
if __is_library_mode__:
55+
return False
56+
5457
settings = load_settings(LogfireSettings)
5558

5659
if not find_spec("logfire") or not settings.enabled or settings.token is None:

src/aignostics/utils/_sentry.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pydantic import AfterValidator, BeforeValidator, Field, PlainSerializer, SecretStr
99
from pydantic_settings import SettingsConfigDict
1010

11-
from ._constants import __env__, __env_file__, __project_name__, __version__
11+
from ._constants import __env__, __env_file__, __is_library_mode__, __project_name__, __version__
1212
from ._settings import OpaqueSettings, load_settings, strip_to_none_before_validator
1313

1414
_ERR_MSG_MISSING_SCHEME = "Sentry DSN is missing URL scheme (protocol)"
@@ -189,6 +189,9 @@ def sentry_initialize() -> bool:
189189
Returns:
190190
bool: True if initialized successfully, False otherwise
191191
"""
192+
if __is_library_mode__:
193+
return False
194+
192195
settings = load_settings(SentrySettings)
193196

194197
if not find_spec("sentry_sdk") or not settings.enabled or settings.dsn is None:

src/aignostics/utils/boot.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,18 @@
55
import sys
66
from pathlib import Path
77

8-
import certifi
9-
import truststore
10-
8+
# Optional SSL certificate modules - gracefully degrade if not available
9+
try:
10+
import certifi
11+
except ImportError:
12+
certifi = None # type: ignore[assignment]
13+
14+
try:
15+
import truststore # pyright: ignore[reportMissingImports]
16+
except ImportError:
17+
truststore = None # type: ignore[assignment]
18+
19+
from ._constants import __is_library_mode__
1120
from ._log import logging_initialize
1221

1322
# Import third party dependencies.
@@ -60,9 +69,6 @@ def _parse_env_args() -> None:
6069
6170
- Last but not least removes those args so typer does not complain about them.
6271
"""
63-
logger = get_logger(__name__)
64-
logger.debug("_parse_env_args called with sys.argv: %s", sys.argv)
65-
6672
i = 1 # Start after script name
6773
to_remove = []
6874
prefix = f"{__project_name__.upper()}_"
@@ -89,6 +95,8 @@ def _parse_env_args() -> None:
8995

9096

9197
def _amend_ssl_trust_chain() -> None:
98+
if __is_library_mode__:
99+
return
92100
truststore.inject_into_ssl()
93101

94102
if ssl.get_default_verify_paths().cafile is None and os.environ.get("SSL_CERT_FILE") is None:
@@ -99,12 +107,14 @@ def _log_boot_message() -> None:
99107
"""Log boot message with version and process information."""
100108
logger = get_logger(__name__)
101109
process_info = get_process_info()
102-
logger.info(
103-
"⭐ Booting %s v%s (project root %s, pid %s), parent '%s' (pid %s)",
110+
mode_suffix = ", library-mode" if __is_library_mode__ else ""
111+
logger.debug(
112+
"⭐ Booting %s v%s (project root %s, pid %s), parent '%s' (pid %s)%s",
104113
__project_name__,
105114
__version__,
106115
process_info.project_root,
107116
process_info.pid,
108117
process_info.parent.name,
109118
process_info.parent.pid,
119+
mode_suffix,
110120
)

tests/conftest.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
from __future__ import annotations
44

5-
import logging
65
import os
6+
7+
from .constants_test import TEST_SUITE
8+
9+
os.environ[f"PYTEST_RUNNING_{TEST_SUITE}"] = "1" # Doing this at the top ensures aignostics src code sees it early
10+
11+
import logging
712
from asyncio import sleep
813
from importlib.util import find_spec
914
from pathlib import Path

tests/constants_test.py

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,59 +6,37 @@
66

77
import os
88

9+
TEST_SUITE = "AIGNOSTICS"
10+
911
SPOT_0_GS_URL = "gs://platform-api-application-test-data/heta/slides/8fafc17d-a5cc-4e9d-a982-030b1486ca88.tiff"
1012
SPOT_0_FILENAME = "8fafc17d-a5cc-4e9d-a982-030b1486ca88.tiff"
11-
SPOT_0_FILESIZE = 10562338
12-
SPOT_0_EXPECTED_RESULT_FILES = [
13-
("tissue_qc_segmentation_map_image.tiff", 1698570, 10),
14-
("tissue_qc_geojson_polygons.json", 315019, 10),
15-
("tissue_segmentation_geojson_polygons.json", 927599, 10),
16-
("readout_generation_slide_readouts.csv", 299865, 10),
17-
("readout_generation_cell_readouts.csv", 1470036, 10),
18-
("cell_classification_geojson_polygons.json", 9915953, 10),
19-
("tissue_segmentation_segmentation_map_image.tiff", 2989980, 10),
20-
("tissue_segmentation_csv_class_information.csv", 361, 10),
21-
("tissue_qc_csv_class_information.csv", 236, 10),
22-
]
23-
SPOT_0_EXPECTED_CELLS_CLASSIFIED = (35160, 10)
2413
SPOT_0_CRC32C = "5onqtA=="
14+
SPOT_0_FILESIZE = 10562338
2515
SPOT_0_RESOLUTION_MPP = 0.26268186053789266
2616
SPOT_0_WIDTH = 7447
2717
SPOT_0_HEIGHT = 7196
2818

2919
SPOT_1_GS_URL = "gs://aignx-storage-service-dev/sample_data_formatted/9375e3ed-28d2-4cf3-9fb9-8df9d11a6627.tiff"
3020
SPOT_1_FILENAME = "9375e3ed-28d2-4cf3-9fb9-8df9d11a6627.tiff"
31-
SPOT_1_FILESIZE = 14681750
32-
SPOT_1_EXPECTED_RESULT_FILES = [
33-
("tissue_qc_segmentation_map_image.tiff", 464908, 10),
34-
("tissue_qc_geojson_polygons.json", 180522, 10),
35-
("tissue_segmentation_geojson_polygons.json", 270931, 10),
36-
("readout_generation_slide_readouts.csv", 295268, 10),
37-
("readout_generation_cell_readouts.csv", 2228907, 10),
38-
("cell_classification_geojson_polygons.json", 16054058, 10),
39-
("tissue_segmentation_segmentation_map_image.tiff", 581258, 10),
40-
("tissue_segmentation_csv_class_information.csv", 342, 10),
41-
("tissue_qc_csv_class_information.csv", 232, 10),
42-
]
4321
SPOT_1_CRC32C = "9l3NNQ=="
22+
SPOT_1_FILESIZE = 14681750
23+
SPOT_1_RESOLUTION_MPP = 0.46499982
4424
SPOT_1_WIDTH = 3728
4525
SPOT_1_HEIGHT = 3640
46-
SPOT_1_RESOLUTION_MPP = 0.46499982
4726

4827
SPOT_2_GS_URL = "gs://aignx-storage-service-dev/sample_data_formatted/8c7b079e-8b8a-4036-bfde-5818352b503a.tiff"
4928
SPOT_2_FILENAME = "8c7b079e-8b8a-4036-bfde-5818352b503a.tiff"
50-
SPOT_2_FILESIZE = 20153772
5129
SPOT_2_CRC32C = "w+ud3g=="
30+
SPOT_2_RESOLUTION_MPP = 0.46499982
5231
SPOT_2_WIDTH = 3616
5332
SPOT_2_HEIGHT = 3400
54-
SPOT_2_RESOLUTION_MPP = 0.46499982
5533

5634
SPOT_3_GS_URL = "gs://aignx-storage-service-dev/sample_data_formatted/1f4f366f-a2c5-4407-9f5e-23400b22d50e.tiff"
5735
SPOT_3_FILENAME = "1f4f366f-a2c5-4407-9f5e-23400b22d50e.tiff"
5836
SPOT_3_CRC32C = "Zmx0wA=="
37+
SPOT_3_RESOLUTION_MPP = 0.46499982
5938
SPOT_3_WIDTH = 4016
6039
SPOT_3_HEIGHT = 3952
61-
SPOT_3_RESOLUTION_MPP = 0.46499982
6240

6341
match os.getenv("AIGNOSTICS_PLATFORM_ENVIRONMENT", "production"):
6442
case "production":
@@ -67,12 +45,64 @@
6745

6846
HETA_APPLICATION_ID = "he-tme"
6947
HETA_APPLICATION_VERSION = "1.0.0-beta.8"
48+
49+
SPOT_0_EXPECTED_RESULT_FILES = [
50+
("tissue_qc_segmentation_map_image.tiff", 1698570, 10),
51+
("tissue_qc_geojson_polygons.json", 315019, 10),
52+
("tissue_segmentation_geojson_polygons.json", 927599, 10),
53+
("readout_generation_slide_readouts.csv", 299865, 10),
54+
("readout_generation_cell_readouts.csv", 1470036, 10),
55+
("cell_classification_geojson_polygons.json", 9915953, 10),
56+
("tissue_segmentation_segmentation_map_image.tiff", 2989980, 10),
57+
("tissue_segmentation_csv_class_information.csv", 361, 10),
58+
("tissue_qc_csv_class_information.csv", 236, 10),
59+
]
60+
SPOT_0_EXPECTED_CELLS_CLASSIFIED = (35160, 10)
61+
62+
SPOT_1_EXPECTED_RESULT_FILES = [
63+
("tissue_qc_segmentation_map_image.tiff", 464908, 10),
64+
("tissue_qc_geojson_polygons.json", 180522, 10),
65+
("tissue_segmentation_geojson_polygons.json", 270931, 10),
66+
("readout_generation_slide_readouts.csv", 295268, 10),
67+
("readout_generation_cell_readouts.csv", 2228907, 10),
68+
("cell_classification_geojson_polygons.json", 16054058, 10),
69+
("tissue_segmentation_segmentation_map_image.tiff", 581258, 10),
70+
("tissue_segmentation_csv_class_information.csv", 342, 10),
71+
("tissue_qc_csv_class_information.csv", 232, 10),
72+
]
73+
7074
case "staging":
7175
TEST_APPLICATION_ID = "test-app"
7276
TEST_APPLICATION_VERSION = "0.0.5"
7377

7478
HETA_APPLICATION_ID = "he-tme"
7579
HETA_APPLICATION_VERSION = "1.0.0-sl+4"
80+
81+
SPOT_0_EXPECTED_RESULT_FILES = [
82+
("tissue_qc_segmentation_map_image.tiff", 1698570, 10),
83+
("tissue_qc_geojson_polygons.json", 315019, 10),
84+
("tissue_segmentation_geojson_polygons.json", 927599, 10),
85+
("readout_generation_slide_readouts.csv", 299865, 10),
86+
("readout_generation_cell_readouts.csv", 1470036, 10),
87+
("cell_classification_geojson_polygons.json", 9915953, 10),
88+
("tissue_segmentation_segmentation_map_image.tiff", 2989980, 10),
89+
("tissue_segmentation_csv_class_information.csv", 361, 10),
90+
("tissue_qc_csv_class_information.csv", 236, 10),
91+
]
92+
SPOT_0_EXPECTED_CELLS_CLASSIFIED = (35160, 10)
93+
94+
SPOT_1_EXPECTED_RESULT_FILES = [
95+
("tissue_qc_segmentation_map_image.tiff", 440122, 10),
96+
("tissue_qc_geojson_polygons.json", 139943, 10),
97+
("tissue_segmentation_geojson_polygons.json", 270931, 10),
98+
("readout_generation_slide_readouts.csv", 295268, 10),
99+
("readout_generation_cell_readouts.csv", 300408, 10),
100+
("cell_classification_geojson_polygons.json", 16384866, 10),
101+
("tissue_segmentation_segmentation_map_image.tiff", 508552, 10),
102+
("tissue_segmentation_csv_class_information.csv", 443, 10),
103+
("tissue_qc_csv_class_information.csv", 284, 10),
104+
]
105+
76106
case _:
77107
message = f"Unsupported AIGNOSTICS_PLATFORM_ENVIRONMENT value: {os.getenv('AIGNOSTICS_PLATFORM_ENVIRONMENT')}"
78108
raise ValueError(message)

0 commit comments

Comments
 (0)