Compare commits

...

34 commits

Author SHA1 Message Date
7d0d6011d7 CI: switch to Woodpecker 2025-06-27 18:06:27 +02:00
ec4cc9a58a CI: fix container version
Some checks failed
/ test (push) Has been cancelled
/ build (push) Has been cancelled
2025-03-16 14:21:50 +01:00
7105edecd1 CI: build container from plain Python image
All checks were successful
/ test (push) Successful in 25s
/ build (push) Successful in 27s
2025-03-16 14:14:39 +01:00
ea7e3087cd Use dynamic versioning
Some checks failed
/ test (push) Successful in 26s
/ build (push) Failing after 8s
2025-03-15 17:43:07 +01:00
c7deb34bae Add annotations to kube 2025-03-15 16:30:07 +01:00
3745cfe3f0 Add kube yaml
All checks were successful
/ test (push) Successful in 25s
2024-12-08 06:12:18 +01:00
8bfb410ee1 CI: set db host for tests
All checks were successful
/ test (push) Successful in 25s
/ build (push) Successful in 6s
2024-12-08 05:57:04 +01:00
4a54a815a0 Config: set default db host to localhost
Some checks failed
/ test (push) Failing after 25s
/ build (push) Has been skipped
2024-12-08 05:32:00 +01:00
727698527e CI: split test and build workflows, run build only on git push tag
All checks were successful
/ test (push) Successful in 25s
/ build (push) Successful in 6s
2024-12-08 02:46:31 +01:00
d0c7e4614a Fix loose config with environment variables
Some checks failed
/ test (push) Successful in 24s
/ build (push) Failing after 6s
2024-12-08 02:23:30 +01:00
53f935b916 CI: build container with version and latest tags
All checks were successful
/ test (push) Successful in 25s
/ build (push) Successful in 7s
2024-12-06 20:14:42 +01:00
ed6c70e1a9 CI: publish container with tag latest along with the version
Some checks failed
/ test (push) Successful in 24s
/ build (push) Failing after 5s
2024-12-06 20:07:13 +01:00
6601a6ced3 CI: fix add Python package build
All checks were successful
/ test (push) Successful in 25s
/ build (push) Successful in 6s
2024-12-06 05:08:05 +01:00
ffd3e99ecc CI: add Python package build
Some checks failed
/ test (push) Successful in 25s
/ build (push) Failing after 4s
2024-12-06 05:04:39 +01:00
48507cc01c Add database container image build 2024-12-06 05:03:55 +01:00
425aaf9dc0 Playbook: fix build and publish ci image
Some checks failed
/ test (push) Successful in 47s
/ build (push) Failing after 1s
2024-12-06 03:35:08 +01:00
c97e3123c9 Playbook: build and publish ci image 2024-12-06 03:34:01 +01:00
a1a499b370 Containers: use public registry
Some checks failed
/ test (push) Failing after 1s
/ build (push) Has been skipped
2024-12-06 03:16:21 +01:00
23afaa7c82 CI: build python package
Some checks failed
/ test (push) Failing after 1s
/ build (push) Has been skipped
2024-12-06 03:05:43 +01:00
5c27a26e78 Update registry and containers for CI
All checks were successful
/ test (push) Successful in 25s
/ build (push) Successful in 15s
2024-12-05 18:59:55 +01:00
259f881815 CI: full image name for the database service 2
Some checks failed
/ test (push) Successful in 25s
/ build (push) Failing after 2s
2024-11-04 23:15:01 +01:00
6abdb5e0d6 CI: full image name for the database service
All checks were successful
/ test (push) Successful in 25s
/ build (push) Successful in 1s
2024-11-04 13:39:38 +01:00
e77bb9eb54 CI: fix app version fix
All checks were successful
/ test (push) Successful in 17s
/ build (push) Successful in 11s
2024-11-04 04:39:06 +01:00
a23674ff63 CI: fix app version 3
All checks were successful
/ test (push) Successful in 17s
/ build (push) Successful in 1s
2024-11-04 04:36:12 +01:00
c1ffb17162 CI: fix app version 2
All checks were successful
/ test (push) Successful in 17s
/ build (push) Successful in 1m29s
2024-11-04 04:17:55 +01:00
d1b7357de4 CI: fix app version
All checks were successful
/ test (push) Successful in 17s
/ build (push) Successful in 11s
2024-11-04 04:10:09 +01:00
1693662e75 Version file added dynamically (workaround as uv does not have this feature yet)
All checks were successful
/ test (push) Successful in 17s
/ build (push) Successful in 19s
Tag container image with playbook
Importable module with python -m treetrail
2024-11-04 03:58:58 +01:00
2d0b788728 Cleanup
All checks were successful
/ test (push) Successful in 17s
/ build (push) Successful in 2s
2024-11-03 19:44:10 +01:00
5a9f28dfb4 CI: default not to build container image manually
All checks were successful
/ test (push) Successful in 16s
/ build (push) Successful in 11s
2024-11-03 19:34:33 +01:00
5ef71e1f17 CI: fix file name
All checks were successful
/ test (push) Successful in 17s
/ build (push) Successful in 2s
2024-11-03 19:32:05 +01:00
93d414673c CI: build only if the test pass 2024-11-03 19:26:43 +01:00
b523ead55f CI: build container when the version is a clean tag
All checks were successful
/ build (push) Successful in 10s
/ test (push) Successful in 17s
2024-11-03 19:12:28 +01:00
cc4584ae97 CI: build container image on request only
All checks were successful
/ test (push) Successful in 18s
2024-11-03 04:57:44 +01:00
6e5711bfd1 CI: fix build 4
All checks were successful
/ build (push) Successful in 18s
/ test (push) Successful in 17s
2024-11-03 04:51:32 +01:00
26 changed files with 466 additions and 271 deletions

View file

@ -1,5 +1,4 @@
.venv .venv
.git
dist dist
.pytest_cache .pytest_cache
.forgejo .forgejo

View file

@ -1,60 +0,0 @@
on:
push:
workflow_dispatch:
inputs:
verbose:
description: "Verbose"
required: false
default: false
type: boolean
jobs:
build:
runs-on: container
container:
image: tiptop:5000/treetrail-backend-ci
volumes:
- "uv_cache:/root/.cache/uv"
- "ca-cert:/etc/containers/certs.d"
steps:
- uses: actions/checkout@v4
- name: Git unshallow - get all history from Git to get the tag for the computation of the version
run: git pull --unshallow
- name: Get the version from git
id: version
run: echo "version=$(git describe --dirty --tags)" >> $GITHUB_OUTPUT
- name: Install app with 'uv pip install'
run: uv pip install --python=$UV_PROJECT_ENVIRONMENT --no-deps .
- name: Workaround for bug of podman-login
run: |
mkdir -p $HOME/.docker
echo "{ \"auths\": {} }" > $HOME/.docker/config.json
- name: Log in to the container registry (with another workaround)
uses: actions/podman-login@v1
with:
registry: ${{ vars.REGISTRY }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
auth_file_path: /tmp/auth.json
- name: Build the container image
uses: actions/buildah-build@v1
with:
image: treetrail-backend
oci: true
labels: treetrail-backend
tags: ${{ steps.version.outputs.version }}
containerfiles: |
./Containerfile
- name: Push the image to the registry
uses: actions/push-to-registry@v2
with:
registry: "docker://${{ vars.REGISTRY }}"
image: treetrail-backend
tags: ${{ steps.version.outputs.version }}

View file

@ -1,34 +0,0 @@
on:
push:
workflow_dispatch:
inputs:
verbose:
description: "Verbose"
required: false
default: false
type: boolean
jobs:
test:
runs-on: container
container:
image: tiptop:5000/treetrail-backend-ci
volumes:
- "uv_cache:/root/.cache/uv"
- "ca-cert:/etc/containers/certs.d"
services:
treetrail-database:
image: treetrail-database
steps:
- name: Echo env
if: ${{ inputs.verbose }}
run: |
echo '${{ toJSON(env) }}'
- uses: actions/checkout@v4
- name: Install app with 'uv pip install'
run: uv pip install --python=$UV_PROJECT_ENVIRONMENT --no-deps .
- name: Run tests (API call)
run: pytest -s tests/basic.py

70
.woodpecker/build.yaml Normal file
View file

@ -0,0 +1,70 @@
when:
- event: manual
- event: tag
depends_on:
- test
steps:
python_sync:
image: code.philo.ydns.eu/philorg/uv
volumes:
- uv-cache:/uv-cache
environment:
UV_CACHE_DIR: /uv-cache
UV_LINK_MODE: copy
commands:
- uv sync
python_build:
image: code.philo.ydns.eu/philorg/uv
volumes:
- uv-cache:/uv-cache
environment:
UV_CACHE_DIR: /uv-cache
UV_LINK_MODE: copy
commands:
- uv build --wheel
- uv cache prune --ci
python_publish:
image: code.philo.ydns.eu/philorg/uv
volumes:
- uv-cache:/uv-cache
environment:
UV_CACHE_DIR: /uv-cache
UV_LINK_MODE: copy
environment:
OWNER: philorg
REGISTRY_URL: https://code.philo.ydns.eu
REGISTRY_TOKEN:
from_secret: registry_token
commands:
- uv publish --publish-url $REGISTRY_URL/api/packages/$OWNER/pypi --token $REGISTRY_TOKEN dist/*.whl
failure: ignore
container_build_publish:
image: quay.io/podman/stable:latest
# Caution: This image is built daily. It might fill up your image store quickly.
#pull: true
volumes:
- containers:/var/lib/containers
- uv-cache:/uv-cache
# Fill in the trusted checkbox in Woodpecker's settings as well
privileged: true
environment:
UV_CACHE_DIR: /uv-cache
UV_LINK_MODE: copy
registry: code.philo.ydns.eu
org: philorg
container_name: treetrail-backend
registry_token:
from_secret: registry_token
commands:
# Login at the registry
- podman login -u __token__ --password $registry_token $registry
# Build the container image
- podman build --volume=/var/lib/containers:/var/lib/containers --tag $registry/$org/$container_name:latest --tag $registry/$org/$container_name:$CI_COMMIT_TAG .
# Push the image
- podman push $registry/$org/$container_name:latest
- podman push $registry/$org/$container_name:$CI_COMMIT_TAG

21
.woodpecker/test.yaml Normal file
View file

@ -0,0 +1,21 @@
when:
- event: push
branch: main
- event: manual
- event: tag
steps:
sync:
image: code.philo.ydns.eu/philorg/uv
volumes:
- uv-cache:/uv-cache
environment:
UV_CACHE_DIR: /uv-cache
UV_LINK_MODE: copy
commands:
- uv sync
test:
image: code.philo.ydns.eu/philorg/uv
commands:
- .venv/bin/pytest -s tests/basic.py

View file

@ -1,19 +1,13 @@
# Build: podman build -t treetrail-backend-base -f Containerfile.base FROM docker.io/library/python:latest
FROM tiptop:5000/treetrail-backend-deps COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
ENV PYTHONPATH $UV_PROJECT_ENVIRONMENT/lib/python3.12/site-packages COPY . /app
COPY . /src # Sync the project into a new environment, using the frozen lockfile
WORKDIR /app
#RUN --mount=type=cache,target=/root/.cache \ RUN uv pip install --system .
# cd /src && \
# uv sync --locked --no-dev --no-editable
RUN uv pip install \
--python=$UV_PROJECT_ENVIRONMENT \
--no-deps \
/src
CMD [ \ CMD [ \
"uvicorn", "treetrail.application:app", \ "uvicorn", "treetrail.application:app", \

View file

@ -1,6 +1,6 @@
# Build: podman build -t treetrail-backend-ci -f Containerfile.ci # Build: podman build -t code.philo.ydns.eu/philorg/treetrail-backend-ci -f Containerfile.ci
FROM tiptop:5000/python-ci FROM code.philo.ydns.eu/philorg/python-ci
COPY ./pyproject.toml ./README.md ./uv.lock /_lock/ COPY ./pyproject.toml ./README.md ./uv.lock /_lock/

4
Containerfile.database Normal file
View file

@ -0,0 +1,4 @@
FROM docker.io/postgis/postgis:17-3.5-alpine
ENV POSTGRES_USER treetrail
ENV POSTGRES_PASSWORD treetrail

View file

@ -1,6 +1,6 @@
# Build: podman build -t treetrail-backend-deps -f Containerfile.deps # Build: podman build -t code.philo.ydns.eu/philorg/treetrail-backend-deps -f Containerfile.deps
FROM tiptop:5000/trixie_python FROM code.philo.ydns.eu/philorg/trixie_python
COPY ./pyproject.toml ./README.md ./uv.lock /_lock/ COPY ./pyproject.toml ./README.md ./uv.lock /_lock/

View file

@ -1,15 +0,0 @@
# FIXME: do not use hardcoded registry
FROM tiptop:5000/trixie_python
WORKDIR /app
ENV PYTHONPATH $UV_PROJECT_ENVIRONMENT/lib/python3.12/site-packages
COPY --from=tiptop:5000/treetrail_backend_deps /app /app
#COPY --from=tiptop:5000/treetrail_backend_deps /var/lib/treetrail/ /var/lib/treetrail
COPY ./treetrail ./pyproject.toml ./README.md ./
# Instances should override the prod.yaml file
#COPY ./prod.yaml /etc/treetrail/prod.yaml
CMD ["uvicorn", "treetrail.application:app", "--port", "8081", "--log-config", "logging.yaml", "--host", "0.0.0.0"]

View file

@ -1,4 +1,5 @@
FROM localhost/trixie_python # Build: podman build -t code.philo.ydns.eu/philorg/treetrail-backend-full -f Containerfile.full_copy
FROM code.philo.ydns.eu/philorg/trixie_python
ENV PYTHONPATH $UV_PROJECT_ENVIRONMENT/lib/python3.12/site-packages ENV PYTHONPATH $UV_PROJECT_ENVIRONMENT/lib/python3.12/site-packages
ENV PATH=/app/bin:$PATH ENV PATH=/app/bin:$PATH

View file

@ -1,6 +1,6 @@
# Build: podman build -t trixie_python -f Containerfile.trixie_python # Build: podman build -t code.philo.ydns.eu/philorg/trixie_python -f Containerfile.trixie_python
FROM debian:trixie-slim FROM docker.io/library/debian:trixie-slim
RUN <<EOT RUN <<EOT
apt-get update -qy apt-get update -qy
@ -15,9 +15,9 @@ EOT
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
ENV UV_LINK_MODE=copy \ ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \ UV_COMPILE_BYTECODE=1 \
UV_PYTHON_DOWNLOADS=never \ UV_PYTHON_DOWNLOADS=never \
#UV_PYTHON=python3.12 \ #UV_PYTHON=python3.12 \
UV_PROJECT_ENVIRONMENT=/app \ UV_PROJECT_ENVIRONMENT=/app \
PYTHONPATH=/app/lib/python3.12/site-packages \ PYTHONPATH=/app/lib/python3.12/site-packages \
PATH=/app/bin:$PATH PATH=/app/bin:$PATH

View file

@ -1,3 +1,6 @@
*Tree Trail* is a fun and pedagogic tool to discover the trails and trees around. *Tree Trail* is a fun and pedagogic tool to discover the trails and trees around.
This is the server (back-end), written in Python. This is the server (back-end), written in Python.
[![status-badge](https://code.philo.ydns.eu/woodpecker/api/badges/20/status.svg)](https://code.philo.ydns.eu/woodpecker/repos/20)

View file

@ -1,11 +1,11 @@
- name: Build containers - name: Build containers images
hosts: localhost hosts: localhost
gather_facts: false gather_facts: false
vars: vars:
force: false
force_rm: false force_rm: false
cache: false cache: false
repository: tiptop:5000 repository: code.philo.ydns.eu
organisation: philorg
tasks: tasks:
#- name: Read conf #- name: Read conf
@ -13,13 +13,56 @@
# file: prod.yaml # file: prod.yaml
# name: conf # name: conf
- name: Build the database image
tags: db
containers.podman.podman_image:
name: treetrail-database
state: build
path: "{{ playbook_dir }}"
build:
format: oci
force_rm: "{{ force_rm }}"
cache: "{{ cache }}"
file: Containerfile.database
push: true
push_args:
dest: "{{ repository }}/{{ organisation }}"
- name: Using the variables - name: Using the variables
ansible.builtin.debug: ansible.builtin.debug:
var: force_rm var: force_rm
- name: Get the version from git
tags:
- ci
- deps
- backend
command: git describe --dirty --tags
register: version
args:
chdir: "{{ playbook_dir }}"
- name: Build the base CI image
tags: ci
containers.podman.podman_image:
name: treetrail-backend-ci
tag: "{{ version.stdout }}"
state: build
path: "{{ playbook_dir }}"
build:
format: oci
force_rm: "{{ force_rm }}"
cache: "{{ cache }}"
file: Containerfile.ci
push: true
push_args:
dest: "{{ repository }}/{{ organisation }}"
- name: Build the base image, only with python dependencies - name: Build the base image, only with python dependencies
tags: deps
containers.podman.podman_image: containers.podman.podman_image:
name: treetrail-backend-deps name: treetrail-backend-deps
tag: "{{ version.stdout }}"
state: build state: build
path: "{{ playbook_dir }}" path: "{{ playbook_dir }}"
build: build:
@ -29,11 +72,13 @@
file: Containerfile.deps file: Containerfile.deps
push: true push: true
push_args: push_args:
dest: "{{ repository }}/treetrail-backend-deps" dest: "{{ repository }}/{{ organisation }}"
- name: Build the backend container image - name: Build the backend container image
tags: backend
containers.podman.podman_image: containers.podman.podman_image:
name: treetrail-backend name: treetrail-backend
tag: "{{ version.stdout }}"
state: build state: build
force: true force: true
path: "{{ playbook_dir }}" path: "{{ playbook_dir }}"
@ -42,6 +87,7 @@
force_rm: "{{ force_rm }}" force_rm: "{{ force_rm }}"
cache: "{{ cache }}" cache: "{{ cache }}"
file: Containerfile file: Containerfile
extra_args: "--build-arg APP_VERSION={{ version.stdout }}"
push: true push: true
push_args: push_args:
dest: "{{ repository }}/treetrail-backend" dest: "{{ repository }}/{{ organisation }}"

View file

@ -1,78 +1,80 @@
[project] [project]
name = "treetrail-backend" name = "treetrail-backend"
version = "0.3.0" dynamic = ["version"]
#dynamic = ["version"]
#dynamic = ["version"]
description = "A fun and pedagogic tool to discover the trails and trees around" description = "A fun and pedagogic tool to discover the trails and trees around"
authors = [ authors = [{ name = "Philippe May", email = "phil.treetrail@philome.mooo.com" }]
{ name = "Philippe May", email = "phil.treetrail@philome.mooo.com" }
]
dependencies = [ dependencies = [
"aiofiles", "aiofiles",
"aiohttp-client-cache", "aiohttp-client-cache",
"aiosqlite", "aiosqlite",
"asyncpg", "asyncpg",
"fastapi", "fastapi",
"geoalchemy2", "geoalchemy2",
"geopandas", "geopandas",
"httptools>=0.6.1", "httptools>=0.6.1",
"orjson", "orjson",
"pandas", "pandas",
"passlib[bcrypt]", "passlib[bcrypt]",
"pillow", "pillow",
"psycopg2-binary", "psycopg2-binary",
"pyarrow", "pyarrow>=19.0.1",
"pydantic-settings", "pydantic-settings",
"python-jose[cryptography]", "python-jose[cryptography]",
"python-multipart", "python-multipart",
"requests", "requests",
"sqlalchemy[asyncio]", "sqlalchemy[asyncio]",
"sqlmodel", "sqlmodel",
"uvicorn[standard]", "uvicorn[standard]",
"uvloop", "uvloop",
] ]
requires-python = ">=3.11" requires-python = ">=3.11"
readme = "README.md" readme = "README.md"
license = {text = "MIT"} license = { text = "MIT" }
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Framework :: FastAPI", "Framework :: FastAPI",
"Environment :: Web Environment", "Environment :: Web Environment",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License (GPL)", "License :: OSI Approved :: GNU General Public License (GPL)",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Operating System :: MacOS :: MacOS X", "Operating System :: MacOS :: MacOS X",
"Operating System :: POSIX", "Operating System :: POSIX",
"Programming Language :: Python", "Programming Language :: Python",
] ]
#[project.scripts] #[project.scripts]
#treetrail-backend = "treetrail_backend:main" #treetrail-backend = "treetrail_backend:main"
[dependency-groups]
dev = ["dunamai>=1.23.0", "ipdb>=0.13.13"]
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling", "uv-dynamic-versioning"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.hatch.version]
source = "uv-dynamic-versioning"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/treetrail"] packages = ["src/treetrail"]
[tool.uv-dynamic-versioning]
style = "semver"
[tool.uv] [tool.uv]
package = true package = true
dev-dependencies = [ dev-dependencies = [
"httpx", "httpx",
"ipdb", "ipdb",
"pandas-stubs", "pandas-stubs",
"pytest", "pytest",
"types-Pillow", "types-Pillow",
"types-PyYAML", "types-PyYAML",
"types-aiofiles", "types-aiofiles",
"types-passlib", "types-passlib",
"types-python-jose", "types-python-jose",
"types-requests", "types-requests",
] ]
#[tool.pdm.version] [tool.black]
#source = "scm" line-length = 98
#write_to = "treetrail/_version.py"
#write_template = "__version__ = '{}'"
#

View file

@ -0,0 +1,8 @@
import importlib.metadata
try:
from dunamai import Version, Style
__version__ = Version.from_git().serialize(style=Style.SemVer, dirty=True)
except (ImportError, RuntimeError):
__version__ = importlib.metadata.version(__name__)

View file

@ -0,0 +1,3 @@
from .application import _main
_main()

View file

@ -1 +0,0 @@
__version__ = '0.3.0'

View file

@ -36,7 +36,8 @@ from treetrail.models import (BaseMapStyles, User, Role, Bootstrap,
MapStyle, Tree, Trail, MapStyle, Tree, Trail,
TreeTrail, POI, UserWithRoles, Zone, TreeTrail, POI, UserWithRoles, Zone,
VersionedComponent) VersionedComponent)
from treetrail.config import conf, get_cache_dir, __version__ from treetrail.config import conf, get_cache_dir
from treetrail import __version__
from treetrail.plantekey import get_local_details from treetrail.plantekey import get_local_details
from treetrail.tiles import registry as tilesRegistry from treetrail.tiles import registry as tilesRegistry

View file

@ -51,22 +51,7 @@ app.mount(
def _main(argv=None): def _main(argv=None):
from argparse import ArgumentParser from argparse import ArgumentParser
arg_parser = ArgumentParser( arg_parser = ArgumentParser(
description="fastapi Application server", description="Treetrail backend / server",
prog="fastapi"
)
arg_parser.add_argument(
'--path',
help='Path of socket file',
)
arg_parser.add_argument(
"-H", "--hostname",
help="TCP/IP hostname to serve on (default: %(default)r)",
default="localhost"
)
arg_parser.add_argument(
"-P", "--port",
help="TCP/IP port to serve on",
type=int,
) )
arg_parser.add_argument( arg_parser.add_argument(
"-c", "--create-db", "-c", "--create-db",
@ -154,6 +139,11 @@ def _main(argv=None):
help="Set debug logging", help="Set debug logging",
action="store_true" action="store_true"
) )
arg_parser.add_argument(
"--version",
help="Print version and exit",
action="store_true"
)
args = arg_parser.parse_args() args = arg_parser.parse_args()
if args.debug: if args.debug:
@ -161,6 +151,11 @@ def _main(argv=None):
## For ipdb: ## For ipdb:
logging.getLogger('parso').setLevel(logging.WARNING) logging.getLogger('parso').setLevel(logging.WARNING)
if args.version:
import treetrail
print(treetrail.__version__)
sys.exit(0)
if args.create_db: if args.create_db:
from treetrail.database import create_db from treetrail.database import create_db
import asyncio import asyncio
@ -233,7 +228,8 @@ def _main(argv=None):
sys.exit(0) sys.exit(0)
print( print(
'This application needs to be run with an asgi server like uvicorn.', 'No CLI option was given, try --help',
'To run the server, use an asgi server like uvicorn.',
'For example:', 'For example:',
'uvicorn application:app', 'uvicorn application:app',
'or:', 'or:',

View file

@ -12,8 +12,6 @@ from pydantic_settings import (
) )
from pydantic.v1.utils import deep_update from pydantic.v1.utils import deep_update
from treetrail._version import __version__
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ENV = environ.get("env", "prod") ENV = environ.get("env", "prod")
@ -66,16 +64,10 @@ def get_cache_dir() -> Path:
return Path(conf.storage.root_cache_path) return Path(conf.storage.root_cache_path)
class MyBaseSettings(BaseSettings): class DB(BaseSettings):
model_config = SettingsConfigDict( model_config = SettingsConfigDict(env_prefix="treetrail_db_")
env_prefix="treetrail_",
env_nested_delimiter="_",
)
class DB(MyBaseSettings):
# uri: str # uri: str
host: str = "treetrail-database" host: str = "localhost"
port: int = 5432 port: int = 5432
user: str = "treetrail" user: str = "treetrail"
db: str = "treetrail" db: str = "treetrail"
@ -93,16 +85,19 @@ class DB(MyBaseSettings):
return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.db}" return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.db}"
class App(MyBaseSettings): class App(BaseSettings):
model_config = SettingsConfigDict(env_prefix="treetrail_app_")
title: str = "Tree Trail" title: str = "Tree Trail"
class Storage(MyBaseSettings): class Storage(BaseSettings):
model_config = SettingsConfigDict(env_prefix="treetrail_storage_")
root_attachment_path: str = "/var/lib/treetrail/attachments" root_attachment_path: str = "/var/lib/treetrail/attachments"
root_cache_path: str = "/var/lib/treetrail/cache" root_cache_path: str = "/var/lib/treetrail/cache"
class Tiles(MyBaseSettings): class Tiles(BaseSettings):
model_config = SettingsConfigDict(env_prefix="treetrail_tiles_")
baseDir: str = "/var/lib/treetrail/mbtiles_files" baseDir: str = "/var/lib/treetrail/mbtiles_files"
useRequestUrl: bool = True useRequestUrl: bool = True
spriteBaseDir: str = "/var/lib/treetrail/mbtiles_sprites" spriteBaseDir: str = "/var/lib/treetrail/mbtiles_sprites"
@ -111,7 +106,8 @@ class Tiles(MyBaseSettings):
osmBaseDir: str = "/var/lib/treetrail/osm" osmBaseDir: str = "/var/lib/treetrail/osm"
class Map(MyBaseSettings): class Map(BaseSettings):
model_config = SettingsConfigDict(env_prefix="treetrail_map_")
zoom: float = 14.0 zoom: float = 14.0
pitch: float = 0.0 pitch: float = 0.0
lat: float = 45.8822 lat: float = 45.8822
@ -120,12 +116,14 @@ class Map(MyBaseSettings):
background: str = "OpenFreeMap" background: str = "OpenFreeMap"
class Geo(MyBaseSettings): class Geo(BaseSettings):
model_config = SettingsConfigDict(env_prefix="treetrail_geo_")
simplify_geom_factor: int = 10000000 simplify_geom_factor: int = 10000000
simplify_preserve_topology: bool = False simplify_preserve_topology: bool = False
class Security(MyBaseSettings): class Security(BaseSettings):
model_config = SettingsConfigDict(env_prefix="treetrail_security_")
""" """
JWT security configuration JWT security configuration
""" """
@ -135,12 +133,14 @@ class Security(MyBaseSettings):
access_token_expire_minutes: float = 30 access_token_expire_minutes: float = 30
class ExternalMapStyle(MyBaseSettings): class ExternalMapStyle(BaseSettings):
model_config = SettingsConfigDict(env_prefix="treetrail_external_map_style_")
name: str name: str
url: str url: str
class Config(MyBaseSettings): class Config(BaseSettings):
model_config = SettingsConfigDict(env_prefix="treetrail_")
@classmethod @classmethod
def settings_customise_sources( def settings_customise_sources(
@ -163,9 +163,9 @@ class Config(MyBaseSettings):
tiles: Tiles = Tiles() tiles: Tiles = Tiles()
security: Security = Security() security: Security = Security()
geo: Geo = Geo() geo: Geo = Geo()
version: str version: str = "-"
db: DB = DB() db: DB = DB()
base_href: str = "/treetrail" base_href: str = "/treetrail"
conf = Config(version=__version__) # type: ignore conf = Config()

View file

@ -14,7 +14,7 @@ from treetrail.config import conf
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CREATE_DB_TIMEOUT = 30 CREATE_DB_TIMEOUT = 10
engine = create_async_engine( engine = create_async_engine(
conf.db.get_sqla_url(), conf.db.get_sqla_url(),
@ -33,7 +33,7 @@ async def create_db(drop=False):
await conn.run_sync(SQLModel.metadata.drop_all) await conn.run_sync(SQLModel.metadata.drop_all)
await conn.run_sync(SQLModel.metadata.create_all) await conn.run_sync(SQLModel.metadata.create_all)
logger.debug(f'Connect to database with config: {conf.db}') logger.debug(f"Connect to database with config: {conf.db}")
while attempts > 0: while attempts > 0:
try: try:
await try_once() await try_once()
@ -68,6 +68,7 @@ async def populate_init_db():
"""Populate the database for a fresh install""" """Populate the database for a fresh install"""
from sqlalchemy import text from sqlalchemy import text
from treetrail.security import create_user, add_role, add_user_role from treetrail.security import create_user, add_role, add_user_role
logger.info("Populating initial database") logger.info("Populating initial database")
user = await create_user(username="admin", password="admin") user = await create_user(username="admin", password="admin")
@ -76,7 +77,7 @@ async def populate_init_db():
async with db_session() as session: async with db_session() as session:
for initial in initials: for initial in initials:
await session.execute(text(initial)) await session.execute(text(initial))
logger.debug(f'Added map style {initial}') logger.debug(f"Added map style {initial}")
await session.commit() await session.commit()

View file

@ -7,19 +7,19 @@ import pandas as pd
from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.engine.row import Row from sqlalchemy.engine.row import Row
from sqlalchemy.sql.selectable import Select from sqlalchemy.sql.selectable import Select
import geopandas as gpd # type: ignore import geopandas as gpd # type: ignore
from treetrail.config import conf from treetrail.config import conf
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AlchemyEncoder(json.JSONEncoder): class AlchemyEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
if isinstance(obj.__class__, DeclarativeMeta): if isinstance(obj.__class__, DeclarativeMeta):
# an SQLAlchemy class # an SQLAlchemy class
fields = {} fields = {}
for field in [x for x in dir(obj) for field in [x for x in dir(obj) if not x.startswith("_") and x != "metadata"]:
if not x.startswith('_') and x != 'metadata']:
data = obj.__getattribute__(field) data = obj.__getattribute__(field)
try: try:
# this will fail on non-encodable values, like other classes # this will fail on non-encodable values, like other classes
@ -50,24 +50,30 @@ def get_attachment_root(type: str):
def get_attachment_tree_root(): def get_attachment_tree_root():
return get_attachment_root('tree') return get_attachment_root("tree")
def get_attachment_trail_root(): def get_attachment_trail_root():
return get_attachment_root('trail') return get_attachment_root("trail")
def get_attachment_poi_root(): def get_attachment_poi_root():
return get_attachment_root('poi') return get_attachment_root("poi")
def pandas_query(session, query): def pandas_query(session, query):
return pd.read_sql_query(query, session.connection()) return pd.read_sql_query(query, session.connection())
def geopandas_query(session, query: Select, model, *,
# simplify_tolerance: float|None=None, def geopandas_query(
crs=None, cast=True, session,
): query: Select,
model,
*,
# simplify_tolerance: float|None=None,
crs=None,
cast=True,
):
## XXX: I could not get the add_columns work without creating a subquery, ## XXX: I could not get the add_columns work without creating a subquery,
## so moving the simplification to geopandas - see in _get_df ## so moving the simplification to geopandas - see in _get_df
# if simplify_tolerance is not None: # if simplify_tolerance is not None:
@ -78,9 +84,11 @@ def geopandas_query(session, query: Select, model, *,
# query = query.add_columns(new_column) # query = query.add_columns(new_column)
return gpd.GeoDataFrame.from_postgis(query, session.connection(), crs=crs) return gpd.GeoDataFrame.from_postgis(query, session.connection(), crs=crs)
def mkdir(dir: Path | str) -> Path: def mkdir(dir: Path | str) -> Path:
path = Path(dir) path = Path(dir)
if not path.is_dir(): if not path.is_dir():
logger.info(f'Create directory {path}') logger.info(f"Create directory {path}")
path.mkdir(parents=True, exist_ok=True) path.mkdir(parents=True, exist_ok=True)
return path return path

View file

122
treetrail-kube.yaml Normal file
View file

@ -0,0 +1,122 @@
apiVersion: v1
kind: Service
metadata:
name: treetrail
annotations:
io.kubernetes.cri-o.SandboxID/gisaf-backend: treetrail-cri-o
io.kubernetes.cri-o.SandboxID/gisaf-database: treetrail-cri-o
io.kubernetes.cri-o.SandboxID/gisaf-frontend: treetrail-cri-o
io.kubernetes.cri-o.SandboxID/gisaf-redis: treetrail-cri-o
io.podman.annotations.infra.name: treetrail-infra
labels:
app: treetrail
spec:
ports:
- name: "80"
nodePort: 31080
port: 80
targetPort: 80
- name: "4532"
nodePort: 31432
port: 4532
targetPort: 4532
selector:
app: treetrail
type: NodePort
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: treetrail
name: treetrail
spec:
containers:
- image: code.philo.ydns.eu/philorg/treetrail-frontend:latest
args:
- nginx
- -g
- daemon off;
name: treetrail-frontend
ports:
- containerPort: 80
hostPort: 8080
- image: code.philo.ydns.eu/philorg/treetrail-backend:latest
name: treetrail-backend
- image: code.philo.ydns.eu/philorg/treetrail-database:latest
args:
- postgres
name: treetrail-database
ports:
- containerPort: 4532
hostPort: 15432
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: treetrail-pgdata
volumes:
- name: treetrail-pgdata
persistentVolumeClaim:
claimName: treetrail-pgdata-pvc
restartPolicy: Always
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: treetrail-pgdata-pv
labels:
type: local
app: postgres
spec:
storageClassName: manual
capacity:
storage: 2Gi
accessModes:
- ReadWriteMany
hostPath:
path: /data/postgresql
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: treetrail-pgdata-pvc
spec:
storageClassName: manual
accessModes:
- ReadWriteMany
resources:
requests:
storage: 2Gi
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: treetrail
namespace: default
#annotations:
# kubernetes.io/ingress.class: traefik
# #traefik.ingress.kubernetes.io/router.middlewares: default-strip-prefix@kubernetescrd
spec:
rules:
- http:
paths:
- path: /treetrail
pathType: Prefix
backend:
service:
name: treetrail-frontend
port:
number: 80
- path: /treetrail/v1
pathType: Prefix
backend:
service:
name: treetrail-backend
port:
number: 8081
- path: /treetrail/plantekey
pathType: Prefix
backend:
service:
name: treetrail-backend
port:
number: 8081

70
uv.lock generated
View file

@ -1,4 +1,5 @@
version = 1 version = 1
revision = 1
requires-python = ">=3.11" requires-python = ">=3.11"
resolution-markers = [ resolution-markers = [
"python_full_version < '3.12'", "python_full_version < '3.12'",
@ -342,7 +343,7 @@ name = "click"
version = "8.1.7" version = "8.1.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "platform_system == 'Windows'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
wheels = [ wheels = [
@ -396,6 +397,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 },
] ]
[[package]]
name = "dunamai"
version = "1.23.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/4e/a5c8c337a1d9ac0384298ade02d322741fb5998041a5ea74d1cd2a4a1d47/dunamai-1.23.0.tar.gz", hash = "sha256:a163746de7ea5acb6dacdab3a6ad621ebc612ed1e528aaa8beedb8887fccd2c4", size = 44681 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/4c/963169386309fec4f96fd61210ac0a0666887d0fb0a50205395674d20b71/dunamai-1.23.0-py3-none-any.whl", hash = "sha256:a0906d876e92441793c6a423e16a4802752e723e9c9a5aabdc5535df02dbe041", size = 26342 },
]
[[package]] [[package]]
name = "ecdsa" name = "ecdsa"
version = "0.19.0" version = "0.19.0"
@ -1109,6 +1122,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 }, { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 },
{ url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 }, { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 },
{ url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 }, { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 },
{ url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 },
] ]
[[package]] [[package]]
@ -1131,27 +1145,37 @@ wheels = [
[[package]] [[package]]
name = "pyarrow" name = "pyarrow"
version = "17.0.0" version = "19.0.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/7f/09/a9046344212690f0632b9c709f9bf18506522feb333c894d0de81d62341a/pyarrow-19.0.1.tar.gz", hash = "sha256:3bf266b485df66a400f282ac0b6d1b500b9d2ae73314a153dbe97d6d5cc8a99e", size = 1129437 }
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/27/4e/ea6d43f324169f8aec0e57569443a38bab4b398d09769ca64f7b4d467de3/pyarrow-17.0.0.tar.gz", hash = "sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28", size = 1112479 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/46/ce89f87c2936f5bb9d879473b9663ce7a4b1f4359acc2f0eb39865eaa1af/pyarrow-17.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977", size = 29028748 }, { url = "https://files.pythonhosted.org/packages/a0/55/f1a8d838ec07fe3ca53edbe76f782df7b9aafd4417080eebf0b42aab0c52/pyarrow-19.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:cc55d71898ea30dc95900297d191377caba257612f384207fe9f8293b5850f90", size = 30713987 },
{ url = "https://files.pythonhosted.org/packages/8d/8e/ce2e9b2146de422f6638333c01903140e9ada244a2a477918a368306c64c/pyarrow-17.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3", size = 27190965 }, { url = "https://files.pythonhosted.org/packages/13/12/428861540bb54c98a140ae858a11f71d041ef9e501e6b7eb965ca7909505/pyarrow-19.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:7a544ec12de66769612b2d6988c36adc96fb9767ecc8ee0a4d270b10b1c51e00", size = 32135613 },
{ url = "https://files.pythonhosted.org/packages/3b/c8/5675719570eb1acd809481c6d64e2136ffb340bc387f4ca62dce79516cea/pyarrow-17.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15", size = 39269081 }, { url = "https://files.pythonhosted.org/packages/2f/8a/23d7cc5ae2066c6c736bce1db8ea7bc9ac3ef97ac7e1c1667706c764d2d9/pyarrow-19.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0148bb4fc158bfbc3d6dfe5001d93ebeed253793fff4435167f6ce1dc4bddeae", size = 41149147 },
{ url = "https://files.pythonhosted.org/packages/5e/78/3931194f16ab681ebb87ad252e7b8d2c8b23dad49706cadc865dff4a1dd3/pyarrow-17.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597", size = 39864921 }, { url = "https://files.pythonhosted.org/packages/a2/7a/845d151bb81a892dfb368bf11db584cf8b216963ccce40a5cf50a2492a18/pyarrow-19.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f24faab6ed18f216a37870d8c5623f9c044566d75ec586ef884e13a02a9d62c5", size = 42178045 },
{ url = "https://files.pythonhosted.org/packages/d8/81/69b6606093363f55a2a574c018901c40952d4e902e670656d18213c71ad7/pyarrow-17.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420", size = 38740798 }, { url = "https://files.pythonhosted.org/packages/a7/31/e7282d79a70816132cf6cae7e378adfccce9ae10352d21c2fecf9d9756dd/pyarrow-19.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:4982f8e2b7afd6dae8608d70ba5bd91699077323f812a0448d8b7abdff6cb5d3", size = 40532998 },
{ url = "https://files.pythonhosted.org/packages/4c/21/9ca93b84b92ef927814cb7ba37f0774a484c849d58f0b692b16af8eebcfb/pyarrow-17.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4", size = 39871877 }, { url = "https://files.pythonhosted.org/packages/b8/82/20f3c290d6e705e2ee9c1fa1d5a0869365ee477e1788073d8b548da8b64c/pyarrow-19.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:49a3aecb62c1be1d822f8bf629226d4a96418228a42f5b40835c1f10d42e4db6", size = 42084055 },
{ url = "https://files.pythonhosted.org/packages/30/d1/63a7c248432c71c7d3ee803e706590a0b81ce1a8d2b2ae49677774b813bb/pyarrow-17.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03", size = 25151089 }, { url = "https://files.pythonhosted.org/packages/ff/77/e62aebd343238863f2c9f080ad2ef6ace25c919c6ab383436b5b81cbeef7/pyarrow-19.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:008a4009efdb4ea3d2e18f05cd31f9d43c388aad29c636112c2966605ba33466", size = 25283133 },
{ url = "https://files.pythonhosted.org/packages/d4/62/ce6ac1275a432b4a27c55fe96c58147f111d8ba1ad800a112d31859fae2f/pyarrow-17.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22", size = 29019418 }, { url = "https://files.pythonhosted.org/packages/78/b4/94e828704b050e723f67d67c3535cf7076c7432cd4cf046e4bb3b96a9c9d/pyarrow-19.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:80b2ad2b193e7d19e81008a96e313fbd53157945c7be9ac65f44f8937a55427b", size = 30670749 },
{ url = "https://files.pythonhosted.org/packages/8e/0a/dbd0c134e7a0c30bea439675cc120012337202e5fac7163ba839aa3691d2/pyarrow-17.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053", size = 27152197 }, { url = "https://files.pythonhosted.org/packages/7e/3b/4692965e04bb1df55e2c314c4296f1eb12b4f3052d4cf43d29e076aedf66/pyarrow-19.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:ee8dec072569f43835932a3b10c55973593abc00936c202707a4ad06af7cb294", size = 32128007 },
{ url = "https://files.pythonhosted.org/packages/cb/05/3f4a16498349db79090767620d6dc23c1ec0c658a668d61d76b87706c65d/pyarrow-17.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a", size = 39263026 }, { url = "https://files.pythonhosted.org/packages/22/f7/2239af706252c6582a5635c35caa17cb4d401cd74a87821ef702e3888957/pyarrow-19.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d5d1ec7ec5324b98887bdc006f4d2ce534e10e60f7ad995e7875ffa0ff9cb14", size = 41144566 },
{ url = "https://files.pythonhosted.org/packages/c2/0c/ea2107236740be8fa0e0d4a293a095c9f43546a2465bb7df34eee9126b09/pyarrow-17.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc", size = 39880798 }, { url = "https://files.pythonhosted.org/packages/fb/e3/c9661b2b2849cfefddd9fd65b64e093594b231b472de08ff658f76c732b2/pyarrow-19.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ad4c0eb4e2a9aeb990af6c09e6fa0b195c8c0e7b272ecc8d4d2b6574809d34", size = 42202991 },
{ url = "https://files.pythonhosted.org/packages/f6/b0/b9164a8bc495083c10c281cc65064553ec87b7537d6f742a89d5953a2a3e/pyarrow-17.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a", size = 38715172 }, { url = "https://files.pythonhosted.org/packages/fe/4f/a2c0ed309167ef436674782dfee4a124570ba64299c551e38d3fdaf0a17b/pyarrow-19.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d383591f3dcbe545f6cc62daaef9c7cdfe0dff0fb9e1c8121101cabe9098cfa6", size = 40507986 },
{ url = "https://files.pythonhosted.org/packages/f1/c4/9625418a1413005e486c006e56675334929fad864347c5ae7c1b2e7fe639/pyarrow-17.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b", size = 39874508 }, { url = "https://files.pythonhosted.org/packages/27/2e/29bb28a7102a6f71026a9d70d1d61df926887e36ec797f2e6acfd2dd3867/pyarrow-19.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b4c4156a625f1e35d6c0b2132635a237708944eb41df5fbe7d50f20d20c17832", size = 42087026 },
{ url = "https://files.pythonhosted.org/packages/ae/49/baafe2a964f663413be3bd1cf5c45ed98c5e42e804e2328e18f4570027c1/pyarrow-17.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7", size = 25099235 }, { url = "https://files.pythonhosted.org/packages/16/33/2a67c0f783251106aeeee516f4806161e7b481f7d744d0d643d2f30230a5/pyarrow-19.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:5bd1618ae5e5476b7654c7b55a6364ae87686d4724538c24185bbb2952679960", size = 25250108 },
{ url = "https://files.pythonhosted.org/packages/2b/8d/275c58d4b00781bd36579501a259eacc5c6dfb369be4ddeb672ceb551d2d/pyarrow-19.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e45274b20e524ae5c39d7fc1ca2aa923aab494776d2d4b316b49ec7572ca324c", size = 30653552 },
{ url = "https://files.pythonhosted.org/packages/a0/9e/e6aca5cc4ef0c7aec5f8db93feb0bde08dbad8c56b9014216205d271101b/pyarrow-19.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d9dedeaf19097a143ed6da37f04f4051aba353c95ef507764d344229b2b740ae", size = 32103413 },
{ url = "https://files.pythonhosted.org/packages/6a/fa/a7033f66e5d4f1308c7eb0dfcd2ccd70f881724eb6fd1776657fdf65458f/pyarrow-19.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ebfb5171bb5f4a52319344ebbbecc731af3f021e49318c74f33d520d31ae0c4", size = 41134869 },
{ url = "https://files.pythonhosted.org/packages/2d/92/34d2569be8e7abdc9d145c98dc410db0071ac579b92ebc30da35f500d630/pyarrow-19.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a21d39fbdb948857f67eacb5bbaaf36802de044ec36fbef7a1c8f0dd3a4ab2", size = 42192626 },
{ url = "https://files.pythonhosted.org/packages/0a/1f/80c617b1084fc833804dc3309aa9d8daacd46f9ec8d736df733f15aebe2c/pyarrow-19.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:99bc1bec6d234359743b01e70d4310d0ab240c3d6b0da7e2a93663b0158616f6", size = 40496708 },
{ url = "https://files.pythonhosted.org/packages/e6/90/83698fcecf939a611c8d9a78e38e7fed7792dcc4317e29e72cf8135526fb/pyarrow-19.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1b93ef2c93e77c442c979b0d596af45e4665d8b96da598db145b0fec014b9136", size = 42075728 },
{ url = "https://files.pythonhosted.org/packages/40/49/2325f5c9e7a1c125c01ba0c509d400b152c972a47958768e4e35e04d13d8/pyarrow-19.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9d46e06846a41ba906ab25302cf0fd522f81aa2a85a71021826f34639ad31ef", size = 25242568 },
{ url = "https://files.pythonhosted.org/packages/3f/72/135088d995a759d4d916ec4824cb19e066585b4909ebad4ab196177aa825/pyarrow-19.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c0fe3dbbf054a00d1f162fda94ce236a899ca01123a798c561ba307ca38af5f0", size = 30702371 },
{ url = "https://files.pythonhosted.org/packages/2e/01/00beeebd33d6bac701f20816a29d2018eba463616bbc07397fdf99ac4ce3/pyarrow-19.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:96606c3ba57944d128e8a8399da4812f56c7f61de8c647e3470b417f795d0ef9", size = 32116046 },
{ url = "https://files.pythonhosted.org/packages/1f/c9/23b1ea718dfe967cbd986d16cf2a31fe59d015874258baae16d7ea0ccabc/pyarrow-19.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f04d49a6b64cf24719c080b3c2029a3a5b16417fd5fd7c4041f94233af732f3", size = 41091183 },
{ url = "https://files.pythonhosted.org/packages/3a/d4/b4a3aa781a2c715520aa8ab4fe2e7fa49d33a1d4e71c8fc6ab7b5de7a3f8/pyarrow-19.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9137cf7e1640dce4c190551ee69d478f7121b5c6f323553b319cac936395f6", size = 42171896 },
{ url = "https://files.pythonhosted.org/packages/23/1b/716d4cd5a3cbc387c6e6745d2704c4b46654ba2668260d25c402626c5ddb/pyarrow-19.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7c1bca1897c28013db5e4c83944a2ab53231f541b9e0c3f4791206d0c0de389a", size = 40464851 },
{ url = "https://files.pythonhosted.org/packages/ed/bd/54907846383dcc7ee28772d7e646f6c34276a17da740002a5cefe90f04f7/pyarrow-19.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:58d9397b2e273ef76264b45531e9d552d8ec8a6688b7390b5be44c02a37aade8", size = 42085744 },
] ]
[[package]] [[package]]
@ -1589,7 +1613,6 @@ wheels = [
[[package]] [[package]]
name = "treetrail-backend" name = "treetrail-backend"
version = "0.3.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },
@ -1618,6 +1641,7 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "dunamai" },
{ name = "httpx" }, { name = "httpx" },
{ name = "ipdb" }, { name = "ipdb" },
{ name = "pandas-stubs" }, { name = "pandas-stubs" },
@ -1645,7 +1669,7 @@ requires-dist = [
{ name = "passlib", extras = ["bcrypt"] }, { name = "passlib", extras = ["bcrypt"] },
{ name = "pillow" }, { name = "pillow" },
{ name = "psycopg2-binary" }, { name = "psycopg2-binary" },
{ name = "pyarrow" }, { name = "pyarrow", specifier = ">=19.0.1" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "python-jose", extras = ["cryptography"] }, { name = "python-jose", extras = ["cryptography"] },
{ name = "python-multipart" }, { name = "python-multipart" },
@ -1658,8 +1682,10 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "dunamai", specifier = ">=1.23.0" },
{ name = "httpx" }, { name = "httpx" },
{ name = "ipdb" }, { name = "ipdb" },
{ name = "ipdb", specifier = ">=0.13.13" },
{ name = "pandas-stubs" }, { name = "pandas-stubs" },
{ name = "pytest" }, { name = "pytest" },
{ name = "types-aiofiles" }, { name = "types-aiofiles" },