diff --git a/AGENTS.md b/AGENTS.md index 2eda999b..e6047ca1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,6 +76,9 @@ cd src/typescript && npm run build # Python cd src/python && poetry build +# Python - after any dependency change, regenerate requirements.txt for Cloud Run Buildpack: +cd src/python && poetry export --without-hashes --without dev -f requirements.txt -o requirements.txt + # Go cd src/go && make build diff --git a/src/python/README.md b/src/python/README.md index 6b69a625..ff30fea0 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -218,6 +218,18 @@ gcloud run deploy lamp-control-api \ --allow-unauthenticated ``` +### Managing `requirements.txt` + +Cloud Run uses the Google Cloud Buildpack, which relies on `requirements.txt` for dependency installation. This file must be kept in sync with `pyproject.toml` and `poetry.lock`. + +**Regenerate after any dependency change:** + +```bash +poetry export --without-hashes --without dev -f requirements.txt -o requirements.txt +``` + +> **Important:** Always commit the updated `requirements.txt` alongside any changes to `pyproject.toml` or `poetry.lock`. Failing to do so will cause Cloud Run deployments to use stale dependencies. + ## Tests ### Running Unit Tests diff --git a/src/python/poetry.lock b/src/python/poetry.lock index 545edac2..046b4e43 100644 --- a/src/python/poetry.lock +++ b/src/python/poetry.lock @@ -38,7 +38,7 @@ version = "4.9.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, @@ -189,7 +189,7 @@ version = "2025.4.26" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, @@ -653,7 +653,7 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -665,7 +665,7 @@ version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -740,7 +740,7 @@ version = "0.27.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main"] files = [ {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, @@ -1770,7 +1770,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -2117,4 +2117,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.14" -content-hash = "02da47be0f0ba8445d9a4348d3260d3035686ba861ffb0d8289f82f3519cd965" +content-hash = "c9738d97c1292874111be8eab623cda8fd4e83f3de35e6a00fa1f490e9359272" diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml index f81f042c..77afec7f 100644 --- a/src/python/pyproject.toml +++ b/src/python/pyproject.toml @@ -37,12 +37,12 @@ opentelemetry-instrumentation-sqlalchemy = "^0.50b0" opentelemetry-instrumentation-httpx = "^0.50b0" opentelemetry-instrumentation-logging = "^0.50b0" python-json-logger = "^2.0" +httpx = "^0.27.2" [tool.poetry.group.dev.dependencies] pytest = "^8.3.5" pytest-asyncio = "^0.26.0" pytest-cov = "^6.1.1" -httpx = "^0.27.2" black = "^26.3.1" ruff = "^0.15.6" mypy = "^1.15.0" diff --git a/src/python/requirements.txt b/src/python/requirements.txt new file mode 100644 index 00000000..b0020351 --- /dev/null +++ b/src/python/requirements.txt @@ -0,0 +1,49 @@ +alembic==1.18.0 ; python_version >= "3.14" and python_version < "4.0" +annotated-types==0.7.0 ; python_version >= "3.14" and python_version < "4.0" +anyio==4.9.0 ; python_version >= "3.14" and python_version < "4.0" +asgiref==3.11.1 ; python_version >= "3.14" and python_version < "4.0" +asyncpg==0.30.0 ; python_version >= "3.14" and python_version < "4.0" +certifi==2025.4.26 ; python_version >= "3.14" and python_version < "4.0" +click==8.1.8 ; python_version >= "3.14" and python_version < "4.0" +colorama==0.4.6 ; python_version >= "3.14" and python_version < "4.0" and platform_system == "Windows" +deprecated==1.3.1 ; python_version >= "3.14" and python_version < "4.0" +fastapi==0.115.12 ; python_version >= "3.14" and python_version < "4.0" +googleapis-common-protos==1.73.0 ; python_version >= "3.14" and python_version < "4.0" +greenlet==3.3.1 ; python_version >= "3.14" and python_version < "4.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") +grpcio==1.78.0 ; python_version >= "3.14" and python_version < "4.0" +h11==0.16.0 ; python_version >= "3.14" and python_version < "4.0" +httpcore==1.0.9 ; python_version >= "3.14" and python_version < "4.0" +httptools==0.7.1 ; python_version >= "3.14" and python_version < "4.0" +httpx==0.27.2 ; python_version >= "3.14" and python_version < "4.0" +idna==3.10 ; python_version >= "3.14" and python_version < "4.0" +importlib-metadata==8.5.0 ; python_version >= "3.14" and python_version < "4.0" +mako==1.3.10 ; python_version >= "3.14" and python_version < "4.0" +markupsafe==3.0.2 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-api==1.29.0 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-exporter-otlp-proto-common==1.29.0 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-exporter-otlp-proto-grpc==1.29.0 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-instrumentation-asgi==0.50b0 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-instrumentation-fastapi==0.50b0 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-instrumentation-httpx==0.50b0 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-instrumentation-logging==0.50b0 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-instrumentation-sqlalchemy==0.50b0 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-instrumentation==0.50b0 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-proto==1.29.0 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-sdk==1.29.0 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-semantic-conventions==0.50b0 ; python_version >= "3.14" and python_version < "4.0" +opentelemetry-util-http==0.50b0 ; python_version >= "3.14" and python_version < "4.0" +packaging==25.0 ; python_version >= "3.14" and python_version < "4.0" +protobuf==5.29.6 ; python_version >= "3.14" and python_version < "4.0" +pydantic-core==2.41.5 ; python_version >= "3.14" and python_version < "4.0" +pydantic-settings==2.12.0 ; python_version >= "3.14" and python_version < "4.0" +pydantic==2.12.5 ; python_version >= "3.14" and python_version < "4.0" +python-dotenv==1.1.0 ; python_version >= "3.14" and python_version < "4.0" +python-json-logger==2.0.7 ; python_version >= "3.14" and python_version < "4.0" +sniffio==1.3.1 ; python_version >= "3.14" and python_version < "4.0" +sqlalchemy==2.0.45 ; python_version >= "3.14" and python_version < "4.0" +starlette==0.46.2 ; python_version >= "3.14" and python_version < "4.0" +typing-extensions==4.15.0 ; python_version >= "3.14" and python_version < "4.0" +typing-inspection==0.4.2 ; python_version >= "3.14" and python_version < "4.0" +uvicorn==0.27.1 ; python_version >= "3.14" and python_version < "4.0" +wrapt==1.17.3 ; python_version >= "3.14" and python_version < "4.0" +zipp==3.23.0 ; python_version >= "3.14" and python_version < "4.0" diff --git a/src/python/src/openapi_server/main.py b/src/python/src/openapi_server/main.py index 3d175cd5..9249d279 100644 --- a/src/python/src/openapi_server/main.py +++ b/src/python/src/openapi_server/main.py @@ -31,8 +31,6 @@ async def lifespan(app: FastAPI): and properly closes it on shutdown. """ # Startup - if configure_telemetry(): - FastAPIInstrumentor.instrument_app(app) initialize_database() yield # Shutdown @@ -47,6 +45,10 @@ async def lifespan(app: FastAPI): lifespan=lifespan, ) +# Middleware must be added before the app starts — cannot be done inside lifespan +if configure_telemetry(): + FastAPIInstrumentor.instrument_app(app) + @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError):