diff --git a/.containerignore b/.containerignore index 5c05940..db4bae9 100644 --- a/.containerignore +++ b/.containerignore @@ -1,5 +1,4 @@ .venv -.git dist .pytest_cache .mypy_cache diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml index 14bc5f3..63f256a 100644 --- a/.forgejo/workflows/build.yaml +++ b/.forgejo/workflows/build.yaml @@ -30,6 +30,8 @@ jobs: echo '${{ toJSON(env) }}' - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install app with 'uv pip install' run: uv pip install --python=$UV_PROJECT_ENVIRONMENT --no-deps . @@ -48,40 +50,35 @@ jobs: with: fetch-depth: 0 - - name: Get the version from git - id: version - run: echo "version=$(git describe --dirty --tags)" >> $GITHUB_OUTPUT + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v4 + with: + version: "0.6.6" - - name: Check if the container should be built - id: builder - env: - RUN: ${{ toJSON(inputs.build || !contains(steps.version.outputs.version, '-g')) }} - run: | - echo "run=$RUN" >> $GITHUB_OUTPUT - echo "Run build: $RUN" + - name: Install + run: uv sync - - name: Info - version and test if the git version is clean (then python package and image container should be built) - env: - VERSION: ${{ steps.version.outputs.version }} - RUN: ${{ steps.builder.outputs.run }} - FORCE: ${{ toJSON(inputs.build) }} - run: | - echo "Version $VERSION, force (manual input): $FORCE, run the build: $RUN" - - name: Set the version in pyproject.toml (workaround for uv not supporting dynamic version) - if: fromJSON(steps.builder.outputs.run) - env: - VERSION: ${{ steps.version.outputs.version }} - run: sed "s/0.0.0/${VERSION}/" -i pyproject.toml + - name: Get version + run: echo "VERSION=$(.venv/bin/dunamai from any --style semver)" >> $GITHUB_ENV + + - name: Version + run: echo $VERSION + + - name: Get distance from tag + run: echo "DISTANCE=$(.venv/bin/dunamai from any --format '{distance}')" >> $GITHUB_ENV + + - name: Distance + run: echo $DISTANCE - name: Workaround for bug of podman-login - if: fromJSON(steps.builder.outputs.run) + if: env.DISTANCE == '0' run: | mkdir -p $HOME/.docker echo "{ \"auths\": {} }" > $HOME/.docker/config.json - name: Log in to the container registry (with another workaround) - if: fromJSON(steps.builder.outputs.run) + if: env.DISTANCE == '0' uses: actions/podman-login@v1 with: registry: ${{ vars.REGISTRY }} @@ -90,37 +87,31 @@ jobs: auth_file_path: /tmp/auth.json - name: Build the container image - if: fromJSON(steps.builder.outputs.run) + if: env.DISTANCE == '0' uses: actions/buildah-build@v1 with: image: gisaf-backend oci: true labels: gisaf-backend - tags: latest ${{ steps.version.outputs.version }} + tags: "latest ${{ env.VERSION }}" containerfiles: | ./Containerfile - build-args: | - APP_VERSION=${{ steps.version.outputs.version }} - name: Push the image to the registry - if: fromJSON(steps.builder.outputs.run) + if: env.DISTANCE == '0' uses: actions/push-to-registry@v2 with: registry: "docker://${{ vars.REGISTRY }}/${{ vars.ORGANISATION }}" image: gisaf-backend - tags: latest ${{ steps.version.outputs.version }} + tags: "latest ${{ env.VERSION }}" - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "0.5.9" - - - name: Build python package - if: fromJSON(steps.builder.outputs.run) + - name: Build wheel + if: env.DISTANCE == '0' run: uv build --wheel - name: Publish Python package (home) - if: fromJSON(steps.builder.outputs.run) + if: env.DISTANCE == '0' env: LOCAL_PYPI_TOKEN: ${{ secrets.LOCAL_PYPI_TOKEN }} run: uv publish --publish-url https://code.philo.ydns.eu/api/packages/philorg/pypi --token $LOCAL_PYPI_TOKEN + continue-on-error: true diff --git a/Containerfile b/Containerfile index 18b0cfb..03508c5 100644 --- a/Containerfile +++ b/Containerfile @@ -1,19 +1,17 @@ # Build: podman build -t code.philo.ydns.eu/philorg/gisaf-backend -f Containerfile -FROM code.philo.ydns.eu/philorg/gisaf-backend-deps +FROM docker.io/library/python:latest -ENV PYTHONPATH $UV_PROJECT_ENVIRONMENT/lib/python3.12/site-packages +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/ ENV GISAF__BACKEND__PORT 8898 ENV GISAF__BACKEND__BIND__ADDR 0.0.0.0 -ARG APP_VERSION=0.0.0 -COPY . /src +COPY . /app -RUN uv pip install \ - --python=$UV_PROJECT_ENVIRONMENT \ - --no-deps \ - /src +# Sync the project into a new environment, using the frozen lockfile +WORKDIR /app + +RUN uv pip install --system . -RUN echo $APP_VERSION > /app/version.txt CMD uvicorn gisaf.application:app --port ${GISAF__BACKEND__PORT} --host ${GISAF__BACKEND__BIND__ADDR} diff --git a/README.md b/README.md index 8f760bf..133a663 100644 --- a/README.md +++ b/README.md @@ -1 +1,59 @@ # Gisaf + +*Gisaf* is a web based GIS (Geographical Information System) initially developed +for the CSR Geomatics unit in Auroville, India. + +Its main audiences are local administrations which need a platform +to develop and maintain a geographical, topological and environmental data, +allow teams of surveyors to upload data and update the GIS in near real time, +and collect data from a wide range of sources. + +Although it just works out of the box, it is primarily intended to be customized +and extended with the data, uses, needs. + +## Features + +- Layers defined with Python plugins or ANSI-standard categories +- Open source industry standard interfaces (PostGIS database, OGCAPI) +- Support of different geographical projections +- Integrated administration interface +- Export of data from standard formats (Geopackage, Shapefile) +- Import and update of data managed in a well defined workflow with "baskets" +- Acquisition of different kinds of surveyors' equipments, accuracies +- Robust mechanisms of identification of the data sources, survey times +- Geographical feature statuses (eg. Existing, Deprecated, Future) +- Role-based access to data +- Support temporal and other data for different features +- Easy integration with IoT sensors, eg. using the MQTT protocol +- Detailed information for all features +- Tagging of features +- Customizable, user triggered actions +- Customizable background maps +- Customizable dashboards +- Customizable base maps (domain specific set of layers) +- Python and Jupyter integration: easy to interact with, add features, components, + explore data and conduct scientific research +- Plugin architecture: add functions with Python packages +- Free and open source + +## Software stack + +- Python: the Swiss Army knife language +- Pydantic: the power of typing +- FastAPI: web wizzard +- SQLModel: SQL (ORM) +- Postgis: geographical extension for the PostGresql database +- Pandas and Geopandas: the data jugglers + +## Installation / deployment + +See the documentation in the deployment directory. + +## Configuration + +Documentation is being rewritten. Please contact us. + +## Demo + +A very basic demo without any data, basically to give a feel of the user interface, +is available [here](https://gisaf.philo.ydns.eu). diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 0000000..966bdde --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,105 @@ +# Gisaf deployment / installation + +This documentation covers the specifics of Gisaf deployment, but +it does no cover application configuration details, +the set up of a public facing web server handling dns domain / https security. + +*Gisaf* is shipped as containers for easy and effective installation, +and packages to deploy them for different environments. + +There are 4 containers: + +- Frontend (Angular build: static files, served by an nginx process, + eventually forwarding requests to the backend) +- Backend (Python server) +- Postgis database server +- Redis server + +Commands below assume that they are run from their respective directories +(`deployment/systemd`, `deployment/kubernetes`, ...). + +## Systemd + +With the help of *podman*, systemd can handle starting/restarting services, etc, +even with a non-privileged user. + +The 4 containers are in the same *pod*, as it is meant primarily for a lightweight +production environment, but this can be easily adapted. + +The `deployment/systemd` directory contains files used for running *Gisaf* with Systemd. +Podman is used to facilitate the creation of the relevant services. + +### Deployment + +```sh +cp -r * $HOME/.config/containers/systemd/ +systemctl --user daemon-reload +systemctl --user start gisaf-pod.service +``` + +Environment variables for the Gisaf configuration can be added +in the `gisaf-backend.container` file. + +Note that starting on system boot requires the user to be enabled accordingly with: + +```sh +sudo loginctl enable-linger +``` + +## Plain Kubernetes (no Helm) + +The `kubernetes` directory contains files for deployment on Kubernetes. + +The standard installation uses a namespace named `gisaf`. + +The Kubernetes configuration is split in 2 files: `gisaf.yaml` and `config.yaml` + +Deployment: + +```sh +kubectl create namespace gisaf +kubectl apply -f config.yaml +kubectl apply -f gisaf.yaml +``` + +Update after modification on the server (frontend and backend): + +```sh +kubectl --namespace gisaf rollout restart deployment gisaf-server-deployment +``` + +## Helm + +The `helm` chart is in the directory named `helm`. + +### Deploy on Kubernetes + +Deploying with Helm on Kubernetes makes it straightforward to +run on cloud services. + +```sh +kubectl create namespace gisaf +helm install gisaf helm +``` + +#### Update + +```sh +helm upgrade gisaf helm +``` + +### Publish the Helm chart + +First, build the Helm package: + +```sh +helm package helm +``` + +Then upload it: + +```sh +helm --user phil: -X POST \ +--upload-file gisaf-0.1.0.tgz \ +https://code.philo.ydns.eu/api/packages/philorg/helm/api/charts +``` diff --git a/deployment/kubernetes/.gitignore b/deployment/kubernetes/.gitignore new file mode 100644 index 0000000..68242e7 --- /dev/null +++ b/deployment/kubernetes/.gitignore @@ -0,0 +1 @@ +gisaf-*.tgz diff --git a/deployment/kubernetes/config.yaml b/deployment/kubernetes/config.yaml new file mode 100644 index 0000000..5bbfa78 --- /dev/null +++ b/deployment/kubernetes/config.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: gisaf-config + namespace: gisaf +data: + GISAF__MAP__LNG: "6.178" + GISAF__MAP__LAT: "45.8818" + GISAF__MAP__ZOOM: "14" + GISAF__MAP__PITCH: "40" diff --git a/deployment/kubernetes/gisaf.yaml b/deployment/kubernetes/gisaf.yaml index 50eca18..9932322 100644 --- a/deployment/kubernetes/gisaf.yaml +++ b/deployment/kubernetes/gisaf.yaml @@ -2,6 +2,7 @@ apiVersion: v1 kind: Service metadata: name: gisaf-database + namespace: gisaf labels: app: gisaf-database spec: @@ -14,56 +15,66 @@ spec: app: gisaf-database --- -apiVersion: v1 -kind: Pod +apiVersion: apps/v1 +kind: Deployment metadata: - name: gisaf-database - annotations: - io.kubernetes.cri-o.SandboxID/gisaf-database: gisaf-cri-o - io.kubernetes.cri-o.SandboxID/gisaf-redis: gisaf-cri-o - io.podman.annotations.infra.name: gisaf-infra + name: gisaf-database-deployment + namespace: gisaf labels: app: gisaf-database spec: - hostAliases: - - ip: "127.0.0.1" - hostnames: - - "gisaf-redis" - - "gisaf-database" - containers: - - name: gisaf-database - image: code.philo.ydns.eu/philorg/gisaf-database:latest - args: - - postgres - volumeMounts: - - mountPath: /var/lib/postgresql/data - name: gisaf-pgdata - ports: - - containerPort: 5432 - name: psql - - image: docker.io/library/redis:alpine - name: gisaf-redis - args: - - redis-server - volumeMounts: - - mountPath: /data + replicas: 1 + selector: + matchLabels: + app: gisaf-database + template: + metadata: + namespace: gisaf + labels: + app: gisaf-database + spec: + hostAliases: + - ip: "127.0.0.1" + hostnames: + - "gisaf-redis" + - "gisaf-database" + containers: + - name: gisaf-database + image: code.philo.ydns.eu/philorg/gisaf-database:latest + imagePullPolicy: Always + args: + - postgres + volumeMounts: + - mountPath: /var/lib/postgresql/data + name: gisaf-pgdata + ports: + - containerPort: 5432 + name: psql + - image: docker.io/library/redis:alpine + imagePullPolicy: Always name: gisaf-redis - ports: - - containerPort: 6379 - name: redis - volumes: - - name: gisaf-pgdata - persistentVolumeClaim: - claimName: gisaf-pgdata-pvc - - name: gisaf-redis - persistentVolumeClaim: - claimName: gisaf-redis-pvc + args: + - redis-server + volumeMounts: + - mountPath: /data + name: gisaf-redis + ports: + - containerPort: 6379 + name: redis + volumes: + - name: gisaf-pgdata + persistentVolumeClaim: + claimName: gisaf-pgdata-pvc + - name: gisaf-redis + persistentVolumeClaim: + claimName: gisaf-redis-pvc --- apiVersion: v1 kind: Service metadata: name: gisaf-server + namespace: gisaf labels: app: gisaf-server spec: @@ -76,57 +87,71 @@ spec: #type: NodePort --- -apiVersion: v1 -kind: Pod +apiVersion: apps/v1 +kind: Deployment metadata: - name: gisaf-server - annotations: - io.kubernetes.cri-o.SandboxID/gisaf-backend: gisaf-cri-o - io.kubernetes.cri-o.SandboxID/gisaf-frontend: gisaf-cri-o - io.podman.annotations.infra.name: gisaf-infra + name: gisaf-server-deployment + namespace: gisaf labels: app: gisaf-server spec: - hostAliases: - - ip: "127.0.0.1" - hostnames: - - "gisaf-frontend" - - "gisaf-backend" - initContainers: - - name: gisaf-backend-initdb - image: code.philo.ydns.eu/philorg/gisaf-backend:latest - command: ["gisaf", "create-db"] - env: - - name: GISAF__DB__HOST - value: gisaf-database - containers: - - name: gisaf-backend - image: code.philo.ydns.eu/philorg/gisaf-backend:latest - env: - - name: GISAF__GISAF_LIVE__REDIS - value: redis://gisaf-database - - name: GISAF__DB__HOST - value: gisaf-database - - name: gisaf-frontend - image: code.philo.ydns.eu/philorg/gisaf-frontend:latest - args: - - nginx - - -g - - daemon off; - ports: - - containerPort: 80 - hostPort: 8899 + replicas: 2 + selector: + matchLabels: + app: gisaf-server + template: + metadata: + namespace: gisaf + labels: + app: gisaf-server + spec: + hostAliases: + - ip: "127.0.0.1" + hostnames: + - "gisaf-frontend" + - "gisaf-backend" + initContainers: + - name: gisaf-backend-initdb + image: code.philo.ydns.eu/philorg/gisaf-backend:0.5.0-alpha.10 + imagePullPolicy: Always + command: ["gisaf", "create-db"] + env: + - name: GISAF__DB__HOST + value: gisaf-database + containers: + - name: gisaf-backend + image: code.philo.ydns.eu/philorg/gisaf-backend:0.5.0-alpha.10 + imagePullPolicy: Always + env: + - name: GISAF__GISAF_LIVE__REDIS + value: redis://gisaf-database + - name: GISAF__DB__HOST + value: gisaf-database + envFrom: + - configMapRef: + name: gisaf-config + - name: gisaf-frontend + image: code.philo.ydns.eu/philorg/gisaf-frontend:0.5.0-alpha.13 + imagePullPolicy: Always + args: + - nginx + - -g + - daemon off; + ports: + - containerPort: 80 + #hostPort: 8899 --- apiVersion: v1 kind: PersistentVolume metadata: name: gisaf-pgdata-pv + namespace: gisaf labels: type: local app: gisaf-postgres spec: - storageClassName: manual + storageClassName: local-path capacity: storage: 2Gi accessModes: @@ -139,11 +164,12 @@ apiVersion: v1 kind: PersistentVolume metadata: name: gisaf-redis-pv + namespace: gisaf labels: type: local app: gisaf-redis spec: - storageClassName: manual + storageClassName: local-path capacity: storage: 2Gi accessModes: @@ -156,8 +182,9 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: gisaf-pgdata-pvc + namespace: gisaf spec: - storageClassName: manual + storageClassName: local-path accessModes: - ReadWriteMany resources: @@ -169,8 +196,9 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: gisaf-redis-pvc + namespace: gisaf spec: - storageClassName: manual + storageClassName: local-path accessModes: - ReadWriteMany resources: @@ -182,7 +210,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: gisaf - namespace: default + namespace: gisaf #annotations: # kubernetes.io/ingress.class: traefik # #traefik.ingress.kubernetes.io/router.middlewares: default-strip-prefix@kubernetescrd diff --git a/deployment/kubernetes/helm/.helmignore b/deployment/kubernetes/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/deployment/kubernetes/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deployment/kubernetes/helm/Chart.yaml b/deployment/kubernetes/helm/Chart.yaml new file mode 100644 index 0000000..f995115 --- /dev/null +++ b/deployment/kubernetes/helm/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: gisaf +description: Gisaf Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "0.15.0-alpha" diff --git a/deployment/kubernetes/helm/templates/config.yaml b/deployment/kubernetes/helm/templates/config.yaml new file mode 100644 index 0000000..0f6165e --- /dev/null +++ b/deployment/kubernetes/helm/templates/config.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-config + namespace: gisaf +data: + GISAF__MAP__LNG: "{{ .Values.map.lng }}" + GISAF__MAP__LAT: "{{ .Values.map.lat }}" + GISAF__MAP__ZOOM: "{{ .Values.map.zoom }}" + GISAF__MAP__PITCH: "{{ .Values.map.pitch }}" diff --git a/deployment/kubernetes/helm/templates/database.yaml b/deployment/kubernetes/helm/templates/database.yaml new file mode 100644 index 0000000..d9ad4f7 --- /dev/null +++ b/deployment/kubernetes/helm/templates/database.yaml @@ -0,0 +1,135 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: gisaf-database + namespace: gisaf + labels: + app: gisaf-database +spec: + ports: + - name: psql + port: 5432 + - name: redis + port: 6379 + selector: + app: gisaf-database + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gisaf-database-deployment + namespace: gisaf + labels: + app: gisaf-database +spec: + replicas: 1 + selector: + matchLabels: + app: gisaf-database + template: + metadata: + namespace: gisaf + labels: + app: gisaf-database + spec: + hostAliases: + - ip: "127.0.0.1" + hostnames: + - "gisaf-redis" + - "gisaf-database" + containers: + - name: gisaf-database + image: code.philo.ydns.eu/philorg/gisaf-database:latest + imagePullPolicy: Always + args: + - postgres + volumeMounts: + - mountPath: /var/lib/postgresql/data + name: gisaf-pgdata + ports: + - containerPort: 5432 + name: psql + - image: docker.io/library/redis:alpine + imagePullPolicy: Always + name: gisaf-redis + args: + - redis-server + volumeMounts: + - mountPath: /data + name: gisaf-redis + ports: + - containerPort: 6379 + name: redis + volumes: + - name: gisaf-pgdata + persistentVolumeClaim: + claimName: gisaf-pgdata-pvc + - name: gisaf-redis + persistentVolumeClaim: + claimName: gisaf-redis-pvc + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: gisaf-pgdata-pvc + namespace: gisaf +spec: + storageClassName: local-path + accessModes: + - ReadWriteMany + resources: + requests: + storage: 2Gi + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: gisaf-redis-pvc + namespace: gisaf +spec: + storageClassName: local-path + accessModes: + - ReadWriteMany + resources: + requests: + storage: 200Mi + +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: gisaf-pgdata-pv + namespace: gisaf + labels: + type: local + app: gisaf-postgres +spec: + storageClassName: local-path + capacity: + storage: 2Gi + accessModes: + - ReadWriteMany + hostPath: + path: /data/gisaf/postgresql + +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: gisaf-redis-pv + namespace: gisaf + labels: + type: local + app: gisaf-redis +spec: + storageClassName: local-path + capacity: + storage: 2Gi + accessModes: + - ReadWriteMany + hostPath: + path: /data/gisaf/redis diff --git a/deployment/kubernetes/helm/templates/network.yaml b/deployment/kubernetes/helm/templates/network.yaml new file mode 100644 index 0000000..d02a579 --- /dev/null +++ b/deployment/kubernetes/helm/templates/network.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gisaf + namespace: gisaf + #annotations: + # kubernetes.io/ingress.class: traefik + # #traefik.ingress.kubernetes.io/router.middlewares: default-strip-prefix@kubernetescrd +spec: + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: gisaf-server + port: + number: 80 + - path: /api + pathType: Prefix + backend: + service: + name: gisaf-server + port: + number: 8081 + diff --git a/deployment/kubernetes/helm/templates/server.yaml b/deployment/kubernetes/helm/templates/server.yaml new file mode 100644 index 0000000..2ced596 --- /dev/null +++ b/deployment/kubernetes/helm/templates/server.yaml @@ -0,0 +1,69 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: gisaf-server + namespace: gisaf + labels: + app: gisaf-server +spec: + ports: + - port: 80 + targetPort: 80 + selector: + app: gisaf-server + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gisaf-server-deployment + namespace: gisaf + labels: + app: gisaf-server +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: gisaf-server + template: + metadata: + namespace: gisaf + labels: + app: gisaf-server + spec: + hostAliases: + - ip: "127.0.0.1" + hostnames: + - "gisaf-frontend" + - "gisaf-backend" + initContainers: + - name: gisaf-backend-initdb + image: "code.philo.ydns.eu/philorg/gisaf-backend:{{ .Values.version.backend }}" + imagePullPolicy: Always + command: ["gisaf", "create-db"] + env: + - name: GISAF__DB__HOST + value: gisaf-database + containers: + - name: gisaf-backend + image: "code.philo.ydns.eu/philorg/gisaf-backend:{{ .Values.version.backend }}" + imagePullPolicy: Always + env: + - name: GISAF__GISAF_LIVE__REDIS + value: redis://gisaf-database + - name: GISAF__DB__HOST + value: gisaf-database + envFrom: + - configMapRef: + name: gisaf-config + - name: gisaf-frontend + image: "code.philo.ydns.eu/philorg/gisaf-frontend:{{ .Values.version.frontend }}" + imagePullPolicy: Always + args: + - nginx + - -g + - daemon off; + ports: + - containerPort: 80 + diff --git a/deployment/kubernetes/helm/values.yaml b/deployment/kubernetes/helm/values.yaml new file mode 100644 index 0000000..deb2537 --- /dev/null +++ b/deployment/kubernetes/helm/values.yaml @@ -0,0 +1,13 @@ +# Replicaset count for the server (2 pods: frontend and backend) +replicaCount: 2 + +version: + backend: 0.5.0-alpha.10 + frontend: 0.5.0-alpha.13 + +# Map config +map: + lng: "6.178" + lat: "45.8818" + zoom: "14" + pitch: "40" diff --git a/pyproject.toml b/pyproject.toml index f8ae259..936cb5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "gisaf-backend" -version = "0.0.0" description = "Gisaf backend" +dynamic = ["version"] authors = [{ name = "phil", email = "phil.dev@philome.mooo.com" }] dependencies = [ "aiopath>=0.7.1", @@ -40,20 +40,9 @@ readme = "README.md" [project.scripts] gisaf = "gisaf.cli:cli" -[project.optional-dependencies] -contextily = ["contextily>=1.4.0"] -mqtt = ["aiomqtt>=1.2.1"] -all = ["gisaf-backend[contextily]", "gisaf-backend[mqtt]"] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["src/gisaf"] - -[tool.uv] -dev-dependencies = [ +[dependency-groups] +dev = [ + "dunamai>=1.23.0", "ipdb>=0.13.13", "pandas-stubs>=2.1.4.231218", "pretty-errors>=1.2.25", @@ -64,3 +53,27 @@ dev-dependencies = [ "types-passlib>=1.7.7.20240311", "pytest>=8.3.4", ] + +[project.optional-dependencies] +contextily = ["contextily>=1.4.0"] +mqtt = ["aiomqtt>=1.2.1"] +all = ["gisaf-backend[contextily]", "gisaf-backend[mqtt]"] + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.hatch.build.targets.wheel] +packages = ["src/gisaf"] + +[tool.uv-dynamic-versioning] +style = "semver" + +[tool.uv] +package = true + +[tool.black] +line-length = 98 diff --git a/src/gisaf/__init__.py b/src/gisaf/__init__.py index e69de29..710f782 100644 --- a/src/gisaf/__init__.py +++ b/src/gisaf/__init__.py @@ -0,0 +1,11 @@ +import importlib.metadata + +try: + from dunamai import Version, Style + + __version__ = Version.from_git().serialize(style=Style.SemVer, dirty=True) +except ImportError: + # __name__ could be used if the package name is the same + # as the directory - not the case here + # __version__ = importlib.metadata.version(__name__) + __version__ = importlib.metadata.version("gisaf-backend") diff --git a/src/gisaf/application.py b/src/gisaf/application.py index fe6cdd5..82dff4d 100644 --- a/src/gisaf/application.py +++ b/src/gisaf/application.py @@ -42,6 +42,8 @@ app = FastAPI( version=conf.version, lifespan=lifespan, default_response_class=responses.ORJSONResponse, + docs_url="/api/docs", + redoc_url="/api/redoc", ) app.include_router(api, prefix="/api") diff --git a/src/gisaf/config.py b/src/gisaf/config.py index ab771f2..1cdb478 100644 --- a/src/gisaf/config.py +++ b/src/gisaf/config.py @@ -3,7 +3,7 @@ import logging from pathlib import Path from typing import Any, Type, Tuple from yaml import safe_load -from importlib.metadata import version +from importlib.metadata import version as importlib_version from xdg import BaseDirectory from pydantic import BaseModel @@ -14,6 +14,9 @@ from pydantic_settings import ( YamlConfigSettingsSource, ) +from gisaf import __version__ + + logger = logging.getLogger(__name__) ENV = environ.get("env", "prod") @@ -27,12 +30,8 @@ config_files = [ class DashboardHome(BaseModel): title: str = "Gisaf - home/dashboards" - content_file: Path = ( - Path(Path.cwd().root) / "etc" / "gisaf" / "dashboard_home_content.html" - ) - footer_file: Path = ( - Path(Path.cwd().root) / "etc" / "gisaf" / "dashboard_home_footer.html" - ) + content_file: Path = Path(Path.cwd().root) / "etc" / "gisaf" / "dashboard_home_content.html" + footer_file: Path = Path(Path.cwd().root) / "etc" / "gisaf" / "dashboard_home_footer.html" class GisafConfig(BaseModel): @@ -120,7 +119,9 @@ class DB(BaseModel): echo: bool = False def get_sqla_url(self): - return f"postgresql+asyncpg://{self.user}:{self.password}@{self.host}:{self.port}/{self.db}" + return ( + f"postgresql+asyncpg://{self.user}:{self.password}@{self.host}:{self.port}/{self.db}" + ) def get_pg_url(self): return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.db}" @@ -202,9 +203,7 @@ class OGCAPI(BaseModel): class TileServer(BaseModel): baseDir: Path = Path(BaseDirectory.xdg_data_home) / "gisaf" / "mbtiles_files_dir" useRequestUrl: bool = False - spriteBaseDir: Path = ( - Path(BaseDirectory.xdg_data_home) / "gisaf" / "mbtiles_sprites_dir" - ) + spriteBaseDir: Path = Path(BaseDirectory.xdg_data_home) / "gisaf" / "mbtiles_sprites_dir" spriteUrl: str = "/tiles/sprite/sprite" spriteBaseUrl: str = "https://gisaf.example.org" openMapTilesKey: str | None = None @@ -311,9 +310,7 @@ class Config(BaseSettings): dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> Tuple[PydanticBaseSettingsSource, ...]: - configs = [ - YamlConfigSettingsSource(settings_cls, yaml_file=cf) for cf in config_files - ] + configs = [YamlConfigSettingsSource(settings_cls, yaml_file=cf) for cf in config_files] return ( env_settings, init_settings, @@ -349,7 +346,7 @@ class Config(BaseSettings): plot: Plot = Plot() plugins: dict[str, dict[str, Any]] = {} survey: Survey = Survey() - version: str = version("gisaf-backend") + version: str = __version__ weather_station: dict[str, dict[str, Any]] = {} widgets: Widgets = Widgets() diff --git a/src/gisaf/models/bootstrap.py b/src/gisaf/models/bootstrap.py index 9455af2..fbd8d08 100644 --- a/src/gisaf/models/bootstrap.py +++ b/src/gisaf/models/bootstrap.py @@ -3,6 +3,7 @@ from pydantic import BaseModel from gisaf.config import conf, Map, Measures, Geo from gisaf.models.authentication import UserRead + class Proj(BaseModel): srid: str srid_for_proj: str @@ -16,4 +17,5 @@ class BootstrapData(BaseModel): geo: Geo = conf.geo measures: Measures = conf.measures redirect: str = conf.gisaf.redirect - user: UserRead | None = None # type: ignore \ No newline at end of file + user: UserRead | None = None # type: ignore + diff --git a/uv.lock b/uv.lock index 436ba4c..30cf5b7 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.12" [[package]] @@ -276,7 +277,7 @@ name = "click" version = "8.1.7" source = { registry = "https://pypi.org/simple" } 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 } wheels = [ @@ -425,6 +426,18 @@ wheels = [ { 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]] name = "ecdsa" version = "0.19.0" @@ -538,7 +551,6 @@ wheels = [ [[package]] name = "gisaf-backend" -version = "0.0.0" source = { editable = "." } dependencies = [ { name = "aiopath" }, @@ -587,6 +599,7 @@ mqtt = [ [package.dev-dependencies] dev = [ { name = "asyncpg-stubs" }, + { name = "dunamai" }, { name = "ipdb" }, { name = "pandas-stubs" }, { name = "pretty-errors" }, @@ -599,17 +612,17 @@ dev = [ [package.metadata] requires-dist = [ + { name = "aiomqtt", marker = "extra == 'all'", specifier = ">=1.2.1" }, { name = "aiomqtt", marker = "extra == 'mqtt'", specifier = ">=1.2.1" }, { name = "aiopath", specifier = ">=0.7.1" }, { name = "aiosqlite", specifier = ">=0.19.0" }, { name = "apscheduler", specifier = ">=3.10.4" }, { name = "asyncpg", specifier = ">=0.28.0" }, + { name = "contextily", marker = "extra == 'all'", specifier = ">=1.4.0" }, { name = "contextily", marker = "extra == 'contextily'", specifier = ">=1.4.0" }, { name = "fastapi", specifier = ">=0.111" }, { name = "geoalchemy2", specifier = ">=0.14.2" }, { name = "geopandas", specifier = ">=1.0.1" }, - { name = "gisaf-backend", extras = ["contextily"], marker = "extra == 'all'" }, - { name = "gisaf-backend", extras = ["mqtt"], marker = "extra == 'all'" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "itsdangerous", specifier = ">=2.1.2" }, { name = "matplotlib", specifier = ">=3.8.3" }, @@ -633,10 +646,12 @@ requires-dist = [ { name = "uvicorn", specifier = ">=0.23.2" }, { name = "websockets", specifier = ">=12.0" }, ] +provides-extras = ["all", "contextily", "mqtt"] [package.metadata.requires-dev] dev = [ { name = "asyncpg-stubs", specifier = ">=0.29.1" }, + { name = "dunamai", specifier = ">=1.23.0" }, { name = "ipdb", specifier = ">=0.13.13" }, { name = "pandas-stubs", specifier = ">=2.1.4.231218" }, { name = "pretty-errors", specifier = ">=1.2.25" }, @@ -1207,6 +1222,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/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/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 }, ] [[package]] @@ -1786,7 +1802,7 @@ name = "tzlocal" version = "5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "tzdata", marker = "platform_system == 'Windows'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/d3/c19d65ae67636fe63953b20c2e4a8ced4497ea232c43ff8d01db16de8dc0/tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e", size = 30201 } wheels = [