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
10 changes: 5 additions & 5 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ jobs:
name: Deploy to AWS
environment: deploy-to-aws
steps:
- uses: actions/checkout@v6.0.2
- uses: aws-actions/configure-aws-credentials@v6.1.3
- uses: actions/checkout@v7.0.1
- uses: aws-actions/configure-aws-credentials@v6.2.3
with:
aws-region: ${{ vars.AWS_REGION }}
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ vars.AWS_DEPLOYMENT_ROLE }}
- uses: aws-actions/amazon-ecr-login@v2.1.5
- uses: aws-actions/amazon-ecr-login@v2.1.6
id: log-into-ecr
- name: Build, tag, and push Docker image to Amazon ECR
id: build-tag-and-push-docker-image
Expand All @@ -37,13 +37,13 @@ jobs:
-e 's/AWS_EXECUTION_ROLE_PLACEHOLDER/${{ vars.AWS_EXECUTION_ROLE }}/g'
-e 's/AWS_REGION_PLACEHOLDER/${{ vars.AWS_REGION }}/g'
task-definition.json
- uses: aws-actions/amazon-ecs-render-task-definition@v1.8.5
- uses: aws-actions/amazon-ecs-render-task-definition@v1.9.0
id: render-task-definition
with:
container-name: atoti-session
image: ${{ steps.build-tag-and-push-docker-image.outputs.tag }}
task-definition: task-definition.json
- uses: aws-actions/amazon-ecs-deploy-task-definition@v2.6.2
- uses: aws-actions/amazon-ecs-deploy-task-definition@v2.6.3
with:
cluster: atoti-project-template
service: atoti-project-template
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ jobs:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: astral-sh/setup-uv@v8.1.0
- uses: actions/checkout@v7.0.1
- uses: astral-sh/setup-uv@v9.0.0
with:
enable-cache: true
# Keep in sync with pyproject.toml's `tool.uv.required-version`.
version: "0.11.17"
version: "0.11.32"
# Keep in sync with pyproject.toml's `project.requires-python`.
- run: uv python install 3.14
- run: uv sync --locked
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

This project is not intended to become a showcase of all Atoti's features.
It should have just enough structure to show how to start a project on sound and modular foundations, not more.
The right place to teach everything Atoti can do is https://docs.atoti.io.
The right place to teach everything Atoti can do is https://docs.activeviam.com/engine/python-sdk/6.2/index.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Keep Python version in sync with:
# - pyproject.toml's `project.requires-python`.
# - the main stage below.
FROM ghcr.io/astral-sh/uv:0.11.17-python3.14-trixie-slim AS builder
FROM ghcr.io/astral-sh/uv:0.11.32-python3.14-trixie-slim AS builder

ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy

Expand Down
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Atoti Project Template

This template can be used to start Atoti projects where the goal is to [go into production rather than prototyping in a notebook](https://docs.atoti.io/latest/deployment/going_from_a_notebook_to_an_app.html).
This template can be used to start Atoti projects where the goal is to [go into production rather than prototyping in a notebook](https://docs.activeviam.com/engine/python-sdk/6.2/getting_started/deployment/going_from_a_notebook_to_an_app).

On top of the `atoti` package, it comes with:

Expand All @@ -25,17 +25,19 @@ On top of the `atoti` package, it comes with:

### Commands

To start the app:
Start the app:

```bash
uv run python -m app
```
- with Docker and [Grafana OpenTelemetry LGTM](https://grafana.com/docs/opentelemetry/docker-lgtm):

To start the app in Docker with [Grafana OpenTelemetry LGTM](https://grafana.com/docs/opentelemetry/docker-lgtm):
```bash
docker compose up --build
```

- without them:

```bash
docker compose up --build
```
```bash
uv run python -m app
```

Other useful commands can be found in [`test.yml`](.github/workflows/test.yml).

Expand Down
20 changes: 11 additions & 9 deletions app/load_tables.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from asyncio import gather
from asyncio import gather, to_thread
from collections.abc import Iterable, Mapping
from pathlib import Path
from typing import Any, cast

import atoti as tt
import httpx
import httpx2
import pandas as pd
from pydantic import DirectoryPath, FilePath, HttpUrl

Expand All @@ -18,7 +18,7 @@
@span
async def read_station_information(
*,
http_client: httpx.AsyncClient,
http_client: httpx2.AsyncClient,
reverse_geocoding_path: HttpUrl | Path,
velib_data_base_path: HttpUrl | Path,
) -> pd.DataFrame:
Expand Down Expand Up @@ -77,7 +77,7 @@ async def read_station_status(
velib_data_base_path: HttpUrl | Path,
/,
*,
http_client: httpx.AsyncClient,
http_client: httpx2.AsyncClient,
) -> pd.DataFrame:
skeleton = Skeleton.tables.STATION_STATUS

Expand Down Expand Up @@ -112,7 +112,7 @@ async def load_tables(
/,
*,
config: Config,
http_client: httpx.AsyncClient,
http_client: httpx2.AsyncClient,
) -> None:
if config.data_refresh_period is None:
reverse_geocoding_path: HttpUrl | FilePath = (
Expand Down Expand Up @@ -144,10 +144,12 @@ async def load_tables(
session.tables.data_transaction(),
):
await gather(
session.tables[Skeleton.tables.STATION_INFORMATION.name].load_async(
station_information_df
to_thread(
session.tables[Skeleton.tables.STATION_INFORMATION.name].load,
station_information_df,
),
session.tables[Skeleton.tables.STATION_STATUS.name].load_async(
station_status_df
to_thread(
session.tables[Skeleton.tables.STATION_STATUS.name].load,
station_status_df,
),
)
8 changes: 4 additions & 4 deletions app/start_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from contextlib import asynccontextmanager, nullcontext

import atoti as tt
import httpx
from opentelemetry.instrumentation.httpx import AsyncOpenTelemetryTransport
import httpx2
from opentelemetry.instrumentation.httpx import AsyncOpenTelemetryTransportHttpx2

from .config import Config
from .load_tables import load_tables
Expand All @@ -13,9 +13,9 @@

@asynccontextmanager
async def start_app(*, config: Config) -> AsyncGenerator[tt.Session]:
httpx_transport = AsyncOpenTelemetryTransport(httpx.AsyncHTTPTransport())
http_transport = AsyncOpenTelemetryTransportHttpx2(httpx2.AsyncHTTPTransport())
async with (
httpx.AsyncClient(transport=httpx_transport) as http_client,
httpx2.AsyncClient(transport=http_transport) as http_client,
start_session(config=config, http_client=http_client) as session,
run_periodically(
lambda: load_tables(session, config=config, http_client=http_client),
Expand Down
4 changes: 2 additions & 2 deletions app/start_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from contextlib import asynccontextmanager

import atoti as tt
import httpx
import httpx2

from .config import Config
from .create_and_join_tables import create_and_join_tables
Expand All @@ -23,7 +23,7 @@ def _create_data_model(session: tt.Session, /) -> None:
async def start_session(
*,
config: Config,
http_client: httpx.AsyncClient,
http_client: httpx2.AsyncClient,
) -> AsyncGenerator[tt.Session]:
"""Start the session, declare the data model and load the initial data."""
session_config = tt.SessionConfig(
Expand Down
4 changes: 2 additions & 2 deletions app/util/read_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pathlib import Path

import anyio
import httpx
import httpx2
from pydantic import HttpUrl


Expand All @@ -12,7 +12,7 @@ async def read_json(
file_path: Path,
/,
*,
http_client: httpx.AsyncClient,
http_client: httpx2.AsyncClient,
) -> object:
if isinstance(base_path, Path):
json_bytes = await anyio.Path(base_path / file_path).read_bytes()
Expand Down
6 changes: 3 additions & 3 deletions app/util/reverse_geocode.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pathlib import Path
from typing import IO, Concatenate

import httpx
import httpx2
import pandas as pd
from pydantic import HttpUrl

Expand Down Expand Up @@ -62,7 +62,7 @@ async def _reverse_geocode(
coordinates: AbstractSet[_Coordinates],
/,
*,
http_client: httpx.AsyncClient,
http_client: httpx2.AsyncClient,
reverse_geocoding_path: HttpUrl | Path,
) -> _ReverseGeocodedCoordinates:
if not coordinates:
Expand Down Expand Up @@ -101,7 +101,7 @@ async def reverse_geocode(
coordinates: Iterable[_Coordinates],
/,
*,
http_client: httpx.AsyncClient,
http_client: httpx2.AsyncClient,
reverse_geocoding_path: HttpUrl | Path,
) -> pd.DataFrame:
data = await _reverse_geocode(
Expand Down
4 changes: 2 additions & 2 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ services:
OTEL_EXPORTER_OTLP_ENDPOINT: http://grafana-otel-lgtm:4318 # HTTP
healthcheck:
start_period: 30s
test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost/actuator/health/readiness').raise_for_status()"]
test: ["CMD", "python", "-c", "import httpx2; httpx2.get('http://localhost/actuator/health/readiness').raise_for_status()"]
ports:
- "127.0.0.1::80"

grafana-otel-lgtm:
image: grafana/otel-lgtm:0.27.0
image: grafana/otel-lgtm:0.29.2
ports:
- "127.0.0.1::3000" # UI
healthcheck:
Expand Down
15 changes: 4 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
[dependency-groups]
dev = [
"anyio",
"docker",
# Same bounds as `pandas` in `project.dependencies`.
"pandas-stubs>=2.2.3,<3.0.0.rc0",
"pytest",
"ruff",
"ty",
]
dev = ["anyio", "docker", "pandas-stubs", "pytest", "ruff", "ty"]

[project]
name = "app"
Expand All @@ -18,7 +10,7 @@ version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
"atoti[observability]",
"httpx",
"httpx2",
"opentelemetry-exporter-otlp-proto-http",
"opentelemetry-instrumentation-httpx",
"opentelemetry-sdk",
Expand All @@ -39,6 +31,7 @@ docstring-code-format = true
[tool.ruff.lint]
ignore = [
"COM812", # Covered by the formatter.
"CPY001",
"D100",
"D101",
"D102",
Expand Down Expand Up @@ -78,4 +71,4 @@ error-on-warning = true
# Keep in sync with:
# - .github/workflows/test.yml
# - Dockerfile`'s `builder`.
required-version = ">=0.11.17"
required-version = ">=0.11.32"
13 changes: 9 additions & 4 deletions skeleton/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ def _identifier(name: str, /) -> str:
return re.sub(r"\W|^(?=\d)", "_", name)


def _str(value: object, /) -> str:
assert isinstance(value, str)
return value


def _indent(code: str, /) -> str:
return f"{' ' * 4}{code}"

Expand Down Expand Up @@ -79,12 +84,12 @@ def _generate_class_name_and_lines[T](
case typed_dict if is_typeddict(typed_dict):
assert isinstance(skeleton, Mapping)
class_name_and_lines_from_attribute_name = {
attribute_name: _generate_class_name_and_lines(
skeleton[attribute_name],
annotation,
_str(attribute_name): _generate_class_name_and_lines(
attribute_value,
typed_dict.__annotations__[_str(attribute_name)],
path=path,
)
for attribute_name, annotation in typed_dict.__annotations__.items()
for attribute_name, attribute_value in skeleton.items()
}
case _:
raise TypeError(f"Unsupported skeleton type: {skeleton_type}.")
Expand Down
2 changes: 1 addition & 1 deletion task-definition.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"CMD",
"python",
"-c",
"import httpx; httpx.get('http://localhost/actuator/health/readiness').raise_for_status()"
"import httpx2; httpx2.get('http://localhost/actuator/health/readiness').raise_for_status()"
],
"startPeriod": 30
}
Expand Down
9 changes: 0 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
from collections.abc import AsyncGenerator
from pathlib import Path

import atoti as tt
import pytest

from app import Config, start_app

_TESTS_DIRECTORY = Path(__file__).parent
_PROJECT_DIRECTORY = _TESTS_DIRECTORY.parent


@pytest.fixture(name="project_name", scope="session")
def project_name_fixture() -> str:
return _PROJECT_DIRECTORY.name


@pytest.fixture(name="config", scope="session")
def config_fixture() -> Config:
Expand Down
27 changes: 0 additions & 27 deletions tests/docker/_docker_container.py

This file was deleted.

9 changes: 0 additions & 9 deletions tests/docker/_run_command.py

This file was deleted.

Loading