38 Commits

Author SHA1 Message Date
f09836c470 Merge pull request 'fix/cluster-manager-crash-due-to-overpass-exception' (#72) from fix/cluster-manager-crash-due-to-overpass-exception into main
Some checks failed
Build and deploy the backend to production / Build and push image (push) Successful in 1m50s
Build and release apps to production track / Get version (push) Has been cancelled
Build and release apps to production track / Build and upload android app (push) Has been cancelled
Build and release apps to production track / Build and upload ios app (push) Has been cancelled
Reviewed-on: #72
2025-11-24 19:22:20 +00:00
9e90aed957 better logs in cluster manager
Some checks failed
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Successful in 2m34s
Run testing on the backend code / Build (pull_request) Failing after 55m28s
2025-11-24 15:56:34 +01:00
d176416b15 changed pylint install
Some checks failed
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Successful in 2m32s
Run testing on the backend code / Build (pull_request) Has been cancelled
2025-11-24 15:43:23 +01:00
5063bc2b69 switched back to manual install of uv for testing 2025-11-24 15:42:20 +01:00
e95088ad25 simpler lint job
Some checks failed
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Failing after 2m24s
Run testing on the backend code / Build (pull_request) Failing after 22s
2025-11-24 15:35:25 +01:00
313bdede03 Merge pull request 'backend - towards a better gitops deploy strategy' (#70) from feature/staging-deployment-on-pr into main
Reviewed-on: #70
2025-11-24 14:30:56 +00:00
8ceaf549e4 other test
Some checks failed
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Has been cancelled
Run testing on the backend code / Build (pull_request) Failing after 22s
2025-11-24 15:27:58 +01:00
63d3ff63e7 changed to manual uv install
Some checks failed
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Failing after 5m16s
Run testing on the backend code / Build (pull_request) Has been cancelled
2025-11-24 15:22:03 +01:00
e1dbbbb274 fixed ci for lint and test
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m48s
Run linting on the backend code / Build (pull_request) Failing after 41s
Run testing on the backend code / Build (pull_request) Failing after 25s
Build and deploy the backend to staging / Deploy to staging (pull_request) Failing after 1m0s
2025-11-24 15:14:46 +01:00
139555dc8e Merge remote-tracking branch 'origin/upgrade/move-backend-to-uv' into fix/cluster-manager-crash-due-to-overpass-exception
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m23s
Run linting on the backend code / Build (pull_request) Failing after 2m22s
Run testing on the backend code / Build (pull_request) Failing after 2m26s
Build and deploy the backend to staging / Deploy to staging (pull_request) Failing after 47s
2025-11-24 14:57:02 +01:00
1785e125c7 detected -> found in cluster manager
Some checks failed
Run testing on the backend code / Build (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Has been cancelled
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
2025-11-24 14:53:31 +01:00
82d37288d6 stop tracking env 2025-11-24 14:53:13 +01:00
9b0821926c fix for cluster manager
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m11s
Run linting on the backend code / Build (pull_request) Failing after 2m22s
Run testing on the backend code / Build (pull_request) Failing after 2m26s
Build and deploy the backend to staging / Deploy to staging (pull_request) Failing after 3h3m27s
2025-11-20 18:48:26 +01:00
0514fa063f suggested fix to avoid UnboundLocalError 2025-11-20 18:47:34 +01:00
71c7325370 man what?
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m35s
Build and deploy the backend to staging / Add a comment to the PR to notify about the deployment (pull_request) Successful in 5s
Run linting on the backend code / Build (pull_request) Has been cancelled
Run testing on the backend code / Build (pull_request) Has been cancelled
2025-10-14 17:19:24 +02:00
aeed9c7dc9 maybe like this?
Some checks failed
Run testing on the backend code / Build (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m32s
Build and deploy the backend to staging / Add a comment to the PR to notify about the deployment (pull_request) Failing after 4s
2025-10-14 17:15:07 +02:00
89c5fc9370 more tea attempts
Some checks failed
Run linting on the backend code / Build (pull_request) Has been cancelled
Run testing on the backend code / Build (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m56s
Build and deploy the backend to staging / Add a comment to the PR to notify about the deployment (pull_request) Failing after 14s
2025-10-14 17:00:07 +02:00
d8c6bfcda0 try whithout body flag
Some checks failed
Run testing on the backend code / Build (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m25s
Build and deploy the backend to staging / Add a comment to the PR to notify about the deployment (pull_request) Failing after 4s
Run linting on the backend code / Build (pull_request) Successful in 3m31s
2025-10-14 16:18:32 +02:00
ec3ed054fd try using the gitea cli instead
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m15s
Build and deploy the backend to staging / Add a comment to the PR to notify about the deployment (pull_request) Failing after 25s
Run linting on the backend code / Build (pull_request) Successful in 4m7s
Run testing on the backend code / Build (pull_request) Has been cancelled
2025-10-14 16:02:27 +02:00
219cfcf1a6 try with correct url?
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m49s
Build and deploy the backend to staging / Add a comment to the PR to notify about the deployment (pull_request) Failing after 21s
Run linting on the backend code / Build (pull_request) Has been cancelled
Run testing on the backend code / Build (pull_request) Has been cancelled
2025-10-14 14:50:27 +02:00
708c07cf49 add a comment containing additional deployment information
Some checks failed
Run testing on the backend code / Build (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m7s
Build and deploy the backend to staging / Add a comment to the PR to notify about the deployment (pull_request) Failing after 21s
Run linting on the backend code / Build (pull_request) Successful in 3m35s
2025-10-14 14:31:56 +02:00
e14900e9f0 remove backend deployment submodule
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m12s
Run linting on the backend code / Build (pull_request) Successful in 1m0s
Run testing on the backend code / Build (pull_request) Failing after 23m1s
2025-10-14 11:21:28 +02:00
d1cbf972fe since branch names may contain slashes and other special chars they are not suitable for tag names. only use the hash
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m54s
Run linting on the backend code / Build (pull_request) Successful in 3m19s
Run testing on the backend code / Build (pull_request) Failing after 24m59s
2025-10-13 18:05:27 +02:00
fe2a0cf1d5 add missing dollar sign
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Failing after 36s
Run linting on the backend code / Build (pull_request) Successful in 2m46s
Run testing on the backend code / Build (pull_request) Has been cancelled
2025-10-13 18:03:39 +02:00
c11faee824 towards a better gitops deploy strategy
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Failing after 38s
Run linting on the backend code / Build (pull_request) Successful in 3m20s
Run testing on the backend code / Build (pull_request) Failing after 27m28s
2025-10-13 17:17:02 +02:00
9ccf68d983 fixed the toilets and works with uv now
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m58s
Run linting on the backend code / Build (pull_request) Failing after 20s
Run testing on the backend code / Build (pull_request) Failing after 22m6s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 1m8s
2025-07-27 17:13:11 +02:00
132aa5a19b changed to no dev when building the docker image
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m24s
Run linting on the backend code / Build (pull_request) Failing after 21s
Run testing on the backend code / Build (pull_request) Failing after 22m41s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 36s
2025-07-26 12:44:41 +02:00
19b0c37a97 fixed the missing dependency in the refiner and changed the test run to using uv 2025-07-26 12:44:12 +02:00
ecdef605a7 cleanup and removed pipenv files 2025-07-26 12:41:58 +02:00
e2a918112b changed to uv fo managing dependencies 2025-07-26 12:41:15 +02:00
96b0718081 removed unused landmark attributes
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m39s
Run linting on the backend code / Build (pull_request) Successful in 31s
Run testing on the backend code / Build (pull_request) Failing after 49s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 38s
2025-07-13 17:47:12 +02:00
d9e5d9dac6 fixed dependcu
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m8s
Run linting on the backend code / Build (pull_request) Successful in 29s
Run testing on the backend code / Build (pull_request) Failing after 46s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 28s
2025-07-13 17:45:13 +02:00
b0f9d31ee2 Implement backend API for landmarks, trip optimization, and toilet locations
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m49s
Run linting on the backend code / Build (pull_request) Successful in 30s
Run testing on the backend code / Build (pull_request) Failing after 45s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 32s
- Added landmarks_router.py to handle landmark retrieval based on user preferences and location.
- Implemented optimization_router.py for trip optimization, including handling preferences and landmarks.
- Created toilets_router.py to fetch toilet locations within a specified radius from a given location.
- Enhanced error handling and logging across all new endpoints.
- Generated a comprehensive report.html for test results and environment details.
2025-07-13 17:43:24 +02:00
54bc9028ad simplified test pipeline
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m38s
Run linting on the backend code / Build (pull_request) Successful in 27s
Run testing on the backend code / Build (pull_request) Failing after 17m36s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 35s
2025-07-02 21:59:07 +02:00
37926e68ec fixed typo in invalid inputs
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m24s
Run linting on the backend code / Build (pull_request) Successful in 27s
Run testing on the backend code / Build (pull_request) Failing after 20m39s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 32s
2025-07-02 21:58:47 +02:00
e2d3d29956 working split
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m46s
Run linting on the backend code / Build (pull_request) Successful in 2m31s
Run testing on the backend code / Build (pull_request) Failing after 12m37s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 29s
2025-06-22 14:24:00 +02:00
6921ab57f8 added more structure
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 3m29s
Run linting on the backend code / Build (pull_request) Successful in 27s
Run testing on the backend code / Build (pull_request) Failing after 12m29s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 34s
2025-06-21 18:54:42 +02:00
97dacb1189 Fastlane fixes working up to a full release (#68)
Reviewed-on: #68
2025-04-30 11:01:48 +00:00
82 changed files with 3383 additions and 2636 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use nix

View File

@@ -3,6 +3,9 @@ on:
tags: tags:
- v* - v*
permissions:
pull-requests: write
name: Build and deploy the backend to production name: Build and deploy the backend to production
jobs: jobs:
@@ -10,15 +13,7 @@ jobs:
name: Build and push image name: Build and push image
uses: ./.gitea/workflows/workflow_build-image.yaml uses: ./.gitea/workflows/workflow_build-image.yaml
with: with:
tag: stable # sets the tag to the git tag that triggered the workflow - the deployment (configured in a separate repository) will use this tag and be deployed to production by argocd
tag: ${{ github.ref_name }}
secrets: secrets:
PACKAGE_REGISTRY_ACCESS: ${{ secrets.PACKAGE_REGISTRY_ACCESS }} PACKAGE_REGISTRY_ACCESS: ${{ secrets.PACKAGE_REGISTRY_ACCESS }}
deploy-prod:
name: Deploy to production
uses: ./.gitea/workflows/workflow_deploy-container.yaml
with:
overlay: prod
secrets:
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
needs: build-and-push

View File

@@ -12,15 +12,32 @@ jobs:
name: Build and push image name: Build and push image
uses: ./.gitea/workflows/workflow_build-image.yaml uses: ./.gitea/workflows/workflow_build-image.yaml
with: with:
tag: unstable # sets a unique tag for each commit in the PR - this gets deployed to a separate application instance using argocd
tag: sha${{ github.sha }}
secrets: secrets:
PACKAGE_REGISTRY_ACCESS: ${{ secrets.PACKAGE_REGISTRY_ACCESS }} PACKAGE_REGISTRY_ACCESS: ${{ secrets.PACKAGE_REGISTRY_ACCESS }}
deploy-prod: notify:
name: Deploy to staging runs-on: ubuntu-latest
uses: ./.gitea/workflows/workflow_deploy-container.yaml name: Add a comment to the PR to notify about the deployment
with: steps:
overlay: stg - name: Download gitea client
secrets: run: |
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }} curl -sSL -o tea https://dl.gitea.com/tea/0.11.0/tea-0.11.0-linux-amd64
needs: build-and-push chmod +x tea
- name: Login
run: |
./tea login add --url git.kluster.moll.re --name bot --token ${{ secrets.GITEA_TOKEN }}
./tea login default
- name: Post comment
run: |
./tea comment --repo anydev/anyway --login bot ${{ github.event.number }} """
The backend has been deployed to staging with url https://pr-${{ github.event.number }}.anyway-stg.anydev.info. Check the deployment status in ArgoCD:
[![App Status](https://argocd.kluster.moll.re/api/badge?name=anydev-anyway-backend-stg-pr-${{ github.event.number }}&revision=true&showAppName=true)](https://argocd.kluster.moll.re/applications/anydev-anyway-backend-stg-pr-${{ github.event.number }})
"""
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_BASE_URL: ${{ secrets.GITEA_BASE_URL }}
GITEA_REPO: ${{ secrets.GITEA_REPO }}

View File

@@ -15,18 +15,10 @@ jobs:
- uses: https://gitea.com/actions/checkout@v4 - uses: https://gitea.com/actions/checkout@v4
- name: Install dependencies - name: Install pylint
run: | run: |
apt-get update && apt-get install -y python3 python3-pip apt-get update && apt-get install -y python3 python3-pip pylint
pip install pipenv
- name: Install packages
run: |
ls -la
# only install dev-packages
pipenv install --categories=dev-packages
working-directory: backend
- name: Run linter - name: Run linter
run: pipenv run pylint src --fail-under=9 run: pylint src --fail-under=9
working-directory: backend working-directory: backend

View File

@@ -15,20 +15,18 @@ jobs:
- uses: https://gitea.com/actions/checkout@v4 - uses: https://gitea.com/actions/checkout@v4
- name: Install dependencies - name: Install uv (manually)
run: | run: |
apt-get update && apt-get install -y python3 python3-pip curl -LsSf https://astral.sh/uv/install.sh | sh
pip install pipenv echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install packages - name: Install dependencies
run: |
ls -la
# install all packages, including dev-packages
pipenv install --dev
working-directory: backend working-directory: backend
run: |
uv sync --frozen --no-cache --no-dev
- name: Run Tests - name: Run Tests
run: pipenv run pytest src --html=report.html --self-contained-html --log-cli-level=DEBUG run: uv run pytest src --html=report.html --self-contained-html --log-cli-level=DEBUG
working-directory: backend working-directory: backend
- name: Upload HTML report - name: Upload HTML report

View File

@@ -5,7 +5,7 @@ on:
paths: paths:
- frontend/** - frontend/**
name: Build and release release apps to production track name: Build and release apps to beta track
jobs: jobs:
get-version: get-version:
@@ -13,6 +13,7 @@ jobs:
runs-on: macos runs-on: macos
steps: steps:
- uses: https://gitea.com/actions/checkout@v4 - uses: https://gitea.com/actions/checkout@v4
- name: Fetch tags from main branch - name: Fetch tags from main branch
# since this workflow is triggered by a pull request, we want to match the latest tag of the main branch # since this workflow is triggered by a pull request, we want to match the latest tag of the main branch
id: version id: version
@@ -20,14 +21,21 @@ jobs:
git fetch origin main --tags git fetch origin main --tags
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
# remove the 'v' prefix from the tag name # remove the 'v' prefix from the tag name
echo "BUILD_NAME=${LATEST_TAG//v}" >> $GITHUB_ENV echo "BUILD_NAME=${LATEST_TAG//v}" >> $GITHUB_OUTPUT
- name: Output the version that is being used
run: |
echo "Building for version ${{ steps.version.outputs.BUILD_NAME }}"
outputs:
build_name: ${{ steps.version.outputs.BUILD_NAME }}
build-android: build-android:
name: Build and upload android app name: Build and upload android app
uses: ./.gitea/workflows/workflow_build-app-android.yaml uses: ./.gitea/workflows/workflow_build-app-android.yaml
with: with:
build_type: beta build_type: beta
build_name: ${{ env.BUILD_NAME }} build_name: ${{ needs.get-version.outputs.build_name }}
secrets: secrets:
ANDROID_SECRET_PROPERTIES_BASE64: ${{ secrets.ANDROID_SECRET_PROPERTIES_BASE64 }} ANDROID_SECRET_PROPERTIES_BASE64: ${{ secrets.ANDROID_SECRET_PROPERTIES_BASE64 }}
ANDROID_GOOGLE_PLAY_JSON_BASE64: ${{ secrets.ANDROID_GOOGLE_PLAY_JSON_BASE64 }} ANDROID_GOOGLE_PLAY_JSON_BASE64: ${{ secrets.ANDROID_GOOGLE_PLAY_JSON_BASE64 }}
@@ -40,7 +48,7 @@ jobs:
uses: ./.gitea/workflows/workflow_build-app-ios.yaml uses: ./.gitea/workflows/workflow_build-app-ios.yaml
with: with:
build_type: beta build_type: beta
build_name: ${{ env.BUILD_NAME }} build_name: ${{ needs.get-version.outputs.build_name }}
secrets: secrets:
IOS_ASC_KEY_ID: ${{ secrets.IOS_ASC_KEY_ID }} IOS_ASC_KEY_ID: ${{ secrets.IOS_ASC_KEY_ID }}
IOS_ASC_ISSUER_ID: ${{ secrets.IOS_ASC_ISSUER_ID }} IOS_ASC_ISSUER_ID: ${{ secrets.IOS_ASC_ISSUER_ID }}
@@ -48,4 +56,4 @@ jobs:
IOS_MATCH_REPO_SSH_KEY_BASE64: ${{ secrets.IOS_MATCH_REPO_SSH_KEY_BASE64 }} IOS_MATCH_REPO_SSH_KEY_BASE64: ${{ secrets.IOS_MATCH_REPO_SSH_KEY_BASE64 }}
IOS_MATCH_PASSWORD: ${{ secrets.IOS_MATCH_PASSWORD }} IOS_MATCH_PASSWORD: ${{ secrets.IOS_MATCH_PASSWORD }}
IOS_GOOGLE_MAPS_API_KEY: ${{ secrets.IOS_GOOGLE_MAPS_API_KEY }} IOS_GOOGLE_MAPS_API_KEY: ${{ secrets.IOS_GOOGLE_MAPS_API_KEY }}
needs: get-version needs: build-android # technically not needed, but this prevents the builds from running in parallel

View File

@@ -3,7 +3,7 @@ on:
tags: tags:
- v* - v*
name: Build and release release apps to production track name: Build and release apps to production track
jobs: jobs:
get-version: get-version:
@@ -11,21 +11,28 @@ jobs:
runs-on: macos runs-on: macos
steps: steps:
- uses: https://gitea.com/actions/checkout@v4 - uses: https://gitea.com/actions/checkout@v4
- name: Get version from git tag - name: Get version from git tag
id: version id: version
env: env:
REF_NAME: ${{ gitea.ref_name }} REF_NAME: ${{ gitea.ref_name }}
# remove the 'v' prefix from the tag name # remove the 'v' prefix from the tag name
run: | run: |
echo "BUILD_NAME=${REF_NAME//v}" >> $GITHUB_ENV echo "BUILD_NAME=${REF_NAME//v}" >> $GITHUB_OUTPUT
- name: Output the version that is being used
run: |
echo "Building for version ${{ steps.version.outputs.BUILD_NAME }}"
outputs:
build_name: ${{ steps.version.outputs.BUILD_NAME }}
build-android: build-android:
name: Build and upload android app name: Build and upload android app
uses: ./.gitea/workflows/workflow_build-app-android.yaml uses: ./.gitea/workflows/workflow_build-app-android.yaml
with: with:
build_type: release build_type: release
build_name: ${{ env.BUILD_NAME }} build_name: ${{ needs.get-version.outputs.build_name }}
secrets: secrets:
ANDROID_SECRET_PROPERTIES_BASE64: ${{ secrets.ANDROID_SECRET_PROPERTIES_BASE64 }} ANDROID_SECRET_PROPERTIES_BASE64: ${{ secrets.ANDROID_SECRET_PROPERTIES_BASE64 }}
ANDROID_GOOGLE_PLAY_JSON_BASE64: ${{ secrets.ANDROID_GOOGLE_PLAY_JSON_BASE64 }} ANDROID_GOOGLE_PLAY_JSON_BASE64: ${{ secrets.ANDROID_GOOGLE_PLAY_JSON_BASE64 }}
@@ -38,12 +45,12 @@ jobs:
uses: ./.gitea/workflows/workflow_build-app-ios.yaml uses: ./.gitea/workflows/workflow_build-app-ios.yaml
with: with:
build_type: release build_type: release
build_name: ${{ env.BUILD_NAME }} build_name: ${{ needs.get-version.outputs.build_name }}
secrets: secrets:
IOS_ASC_KEY_ID: ${{ secrets.IOS_ASC_KEY_ID }} IOS_ASC_KEY_ID: ${{ secrets.IOS_ASC_KEY_ID }}
IOS_ASC_ISSUER_ID: ${{ secrets.IOS_ASC_ISSUER_ID }} IOS_ASC_ISSUER_ID: ${{ secrets.IOS_ASC_ISSUER_ID }}
IOS_ASC_KEY: ${{ secrets.IOS_ASC_KEY }} IOS_ASC_KEY: ${{ secrets.IOS_ASC_KEY }}
IOS_MATCH_PASSWORD: ${{ secrets.IOS_MATCH_PASSWORD }}
IOS_MATCH_REPO_SSH_KEY_BASE64: ${{ secrets.IOS_MATCH_REPO_SSH_KEY_BASE64 }} IOS_MATCH_REPO_SSH_KEY_BASE64: ${{ secrets.IOS_MATCH_REPO_SSH_KEY_BASE64 }}
IOS_MATCH_PASSWORD: ${{ secrets.IOS_MATCH_PASSWORD }}
IOS_GOOGLE_MAPS_API_KEY: ${{ secrets.IOS_GOOGLE_MAPS_API_KEY }} IOS_GOOGLE_MAPS_API_KEY: ${{ secrets.IOS_GOOGLE_MAPS_API_KEY }}
needs: get-version needs: build-android # technically not needed, but this prevents the builds from running in parallel

View File

@@ -27,7 +27,7 @@ defaults:
jobs: jobs:
build: build:
runs-on: macos runs-on: macos-14
env: env:
# $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
BUNDLE_GEMFILE: ${{ gitea.workspace }}/frontend/android/Gemfile BUNDLE_GEMFILE: ${{ gitea.workspace }}/frontend/android/Gemfile

View File

@@ -31,7 +31,7 @@ defaults:
jobs: jobs:
build: build:
runs-on: macos runs-on: macos-14
env: env:
# $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
BUNDLE_GEMFILE: ${{ gitea.workspace }}/frontend/ios/Gemfile BUNDLE_GEMFILE: ${{ gitea.workspace }}/frontend/ios/Gemfile
@@ -53,9 +53,13 @@ jobs:
ruby-version: 3.3 ruby-version: 3.3
bundler-cache: true # runs 'bundle install' and caches installed gems automatically bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: GuillaumeFalourd/setup-rsync@v1.2
# rsync is required by the google maps ios tools
- name: Install dependencies and clean up - name: Install dependencies and clean up
run: | run: |
flutter pub get flutter pub get
flutter precache --ios
bundle exec pod install --allow-root bundle exec pod install --allow-root
flutter clean flutter clean
bundle exec pod cache clean --all --allow-root bundle exec pod cache clean --all --allow-root
@@ -69,6 +73,11 @@ jobs:
env: env:
MATCH_REPO_SSH_KEY: ${{ secrets.IOS_MATCH_REPO_SSH_KEY_BASE64 }} MATCH_REPO_SSH_KEY: ${{ secrets.IOS_MATCH_REPO_SSH_KEY_BASE64 }}
- name: Replace API Key from secret
# on a macOS runner, sed requires a replacement suffix after the -i flag
run: |
sed -i '' -e "s/IOS_GOOGLE_MAPS_API_KEY/${{ secrets.IOS_GOOGLE_MAPS_API_KEY }}/g" Runner/AppDelegate.swift
- name: Run fastlane lane - name: Run fastlane lane
run: bundle exec fastlane deploy_${{ inputs.build_type }} run: bundle exec fastlane deploy_${{ inputs.build_type }}
env: env:

View File

@@ -1,35 +0,0 @@
on:
workflow_call:
inputs:
overlay:
required: true
type: string
secrets:
KUBE_CONFIG:
required: true
name: Deploy the newly built container
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@v4
with:
submodules: true
- name: setup kubectl
uses: https://github.com/azure/setup-kubectl@v4
- name: Set kubeconfig
run: |
echo "${{ secrets.KUBE_CONFIG }}" > kubeconfig
- name: Deploy to k8s
run: |
kubectl apply -k backend/deployment/overlays/${{ inputs.overlay }} --kubeconfig=kubeconfig
kubectl -n anyway-backend rollout restart deployment/anyway-backend-${{ inputs.overlay }} --kubeconfig=kubeconfig

1
.gitignore vendored
View File

@@ -1 +1,2 @@
cache/ cache/
.direnv/

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "backend/deployment"]
path = backend/deployment
url = https://git.kluster.moll.re/anydev/anyway-backend-deployment

8
.vscode/launch.json vendored
View File

@@ -9,9 +9,7 @@
"name": "Backend - debug", "name": "Backend - debug",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"env": { "envFile": "${workspaceFolder}/backend/debug.env",
"DEBUG": "true"
},
"jinja": true, "jinja": true,
"cwd": "${workspaceFolder}/backend", "cwd": "${workspaceFolder}/backend",
"module": "fastapi", "module": "fastapi",
@@ -25,9 +23,7 @@
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "src/tester.py", "program": "src/tester.py",
"env": { "envFile": "${workspaceFolder}/backend/debug.env",
"DEBUG": "true"
},
"cwd": "${workspaceFolder}/backend" "cwd": "${workspaceFolder}/backend"
}, },
// frontend - flutter app // frontend - flutter app

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"nixEnvSelector.nixFile": "${workspaceFolder}/default.nix"
}

8
backend/.gitignore vendored
View File

@@ -1,3 +1,6 @@
# all .env files
*.env
# osm-cache # osm-cache
cache_XML/ cache_XML/
@@ -12,6 +15,9 @@ __pycache__/
# C extensions # C extensions
*.so *.so
# Pytest reports
report.html
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
@@ -128,7 +134,7 @@ celerybeat.pid
*.sage.py *.sage.py
# Environments # Environments
.env *.env
.venv .venv
env/ env/
venv/ venv/

1
backend/.python-version Normal file
View File

@@ -0,0 +1 @@
3.12.9

View File

@@ -1,11 +1,29 @@
FROM python:3.11-slim FROM python:3.12-slim-bookworm
# The installer requires curl (and certificates) to download the release archive
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
# Download the latest installer
ADD https://astral.sh/uv/install.sh /uv-installer.sh
# Run the installer then remove it
RUN sh /uv-installer.sh && rm /uv-installer.sh
# Ensure the installed binary is on the `PATH`
ENV PATH="/root/.local/bin/:$PATH"
# Set the working directory
WORKDIR /app WORKDIR /app
COPY Pipfile Pipfile.lock .
RUN pip install pipenv # Copy uv files
RUN pipenv install --deploy --system COPY pyproject.toml pyproject.toml
COPY uv.lock uv.lock
COPY .python-version .python-version
# Sync the venv
RUN uv sync --frozen --no-cache --no-dev
# Copy application files
COPY src src COPY src src
EXPOSE 8000 EXPOSE 8000
@@ -17,4 +35,4 @@ ENV MEMCACHED_HOST_PATH=none
ENV LOKI_URL=none ENV LOKI_URL=none
# explicitly use a string instead of an argument list to force a shell and variable expansion # explicitly use a string instead of an argument list to force a shell and variable expansion
CMD fastapi run src/main.py --port 8000 --workers $NUM_WORKERS CMD uv run fastapi run src/main.py --port 8000 --workers $NUM_WORKERS

View File

@@ -1,27 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[dev-packages]
pylint = "*"
pytest = "*"
tomli = "*"
httpx = "*"
exceptiongroup = "*"
pytest-html = "*"
typing-extensions = "*"
dill = "*"
[packages]
numpy = "*"
fastapi = "*"
pydantic = "*"
shapely = "*"
pymemcache = "*"
fastapi-cli = "*"
scikit-learn = "*"
loki-logger-handler = "*"
pulp = "*"
scipy = "*"
requests = "*"

1246
backend/Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,363 +0,0 @@
[
{
"name": "Chinatown",
"type": "shopping",
"location": [
45.7554934,
4.8444852
],
"osm_type": "way",
"osm_id": 996515596,
"attractiveness": 129,
"n_tags": 0,
"image_url": null,
"website_url": null,
"wiki_url": null,
"keywords": {},
"description": null,
"duration": 30,
"name_en": null,
"uuid": "285d159c-68ee-4b37-8d71-f27ee3d38b02",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
},
{
"name": "Galeries Lafayette",
"type": "shopping",
"location": [
45.7627107,
4.8556833
],
"osm_type": "way",
"osm_id": 1069872743,
"attractiveness": 197,
"n_tags": 11,
"image_url": null,
"website_url": "http://www.galerieslafayette.com/",
"wiki_url": null,
"keywords": null,
"description": null,
"duration": 30,
"name_en": null,
"uuid": "28f1bc30-10d3-4944-8861-0ed9abca012d",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
},
{
"name": "Muji",
"type": "shopping",
"location": [
45.7615971,
4.8543781
],
"osm_type": "way",
"osm_id": 1044165817,
"attractiveness": 259,
"n_tags": 14,
"image_url": null,
"website_url": "https://www.muji.com/fr/",
"wiki_url": null,
"keywords": null,
"description": null,
"duration": 30,
"name_en": "Muji",
"uuid": "957f86a5-6c00-41a2-815d-d6f739052be4",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
},
{
"name": "HEMA",
"type": "shopping",
"location": [
45.7619133,
4.8565239
],
"osm_type": "way",
"osm_id": 1069872750,
"attractiveness": 156,
"n_tags": 9,
"image_url": null,
"website_url": "https://fr.westfield.com/lapartdieu/store/HEMA/www.hema.fr",
"wiki_url": null,
"keywords": null,
"description": null,
"duration": 30,
"name_en": null,
"uuid": "8dae9d3e-e4c4-4e80-941d-0b106e22c85b",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
},
{
"name": "Cordeliers",
"type": "shopping",
"location": [
45.7622752,
4.8337998
],
"osm_type": "node",
"osm_id": 5545183519,
"attractiveness": 813,
"n_tags": 0,
"image_url": null,
"website_url": null,
"wiki_url": null,
"keywords": {},
"description": null,
"duration": 30,
"name_en": null,
"uuid": "ba02adb5-e28f-4645-8c2d-25ead6232379",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
},
{
"name": "Halles de Lyon Paul Bocuse",
"type": "shopping",
"location": [
45.7628282,
4.8505601
],
"osm_type": "relation",
"osm_id": 971529,
"attractiveness": 272,
"n_tags": 12,
"image_url": null,
"website_url": "https://www.halles-de-lyon-paulbocuse.com/",
"wiki_url": "fr:Halles de Lyon-Paul Bocuse",
"keywords": {
"importance": "national",
"height": null,
"place_type": "marketplace",
"date": null
},
"description": "Halles de Lyon Paul Bocuse is a marketplace of national importance.",
"duration": 30,
"name_en": null,
"uuid": "bbd50de3-aa91-425d-90c2-d4abfd1b4abe",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
},
{
"name": "Grand Bazar",
"type": "shopping",
"location": [
45.7632141,
4.8361975
],
"osm_type": "way",
"osm_id": 82399951,
"attractiveness": 93,
"n_tags": 7,
"image_url": null,
"website_url": null,
"wiki_url": null,
"keywords": null,
"description": null,
"duration": 30,
"name_en": null,
"uuid": "3de9131c-87c5-4efb-9fa8-064896fb8b29",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
},
{
"name": "Shopping Area",
"type": "shopping",
"location": [
45.7673452,
4.8438683
],
"osm_type": "node",
"osm_id": 0,
"attractiveness": 156,
"n_tags": 0,
"image_url": null,
"website_url": null,
"wiki_url": null,
"keywords": {},
"description": null,
"duration": 30,
"name_en": null,
"uuid": "df2482a8-7e2e-4536-aad3-564899b2fa65",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
},
{
"name": "Cour Oxyg\u00e8ne",
"type": "shopping",
"location": [
45.7620905,
4.8568873
],
"osm_type": "way",
"osm_id": 132673030,
"attractiveness": 63,
"n_tags": 5,
"image_url": null,
"website_url": null,
"wiki_url": null,
"keywords": null,
"description": null,
"duration": 30,
"name_en": null,
"uuid": "ed134f76-9a02-4bee-9c10-78454f7bc4ce",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
},
{
"name": "P\u00f4le de Commerces et de Loisirs Confluence",
"type": "shopping",
"location": [
45.7410414,
4.8171031
],
"osm_type": "way",
"osm_id": 440270633,
"attractiveness": 259,
"n_tags": 14,
"image_url": null,
"website_url": "https://www.confluence.fr/",
"wiki_url": null,
"keywords": null,
"description": null,
"duration": 30,
"name_en": null,
"uuid": "dd7e2f5f-0e60-4560-b903-e5ded4b6e36a",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
},
{
"name": "Grand H\u00f4tel-Dieu",
"type": "shopping",
"location": [
45.7586955,
4.8364597
],
"osm_type": "relation",
"osm_id": 300128,
"attractiveness": 546,
"n_tags": 22,
"image_url": null,
"website_url": "https://grand-hotel-dieu.com",
"wiki_url": "fr:H\u00f4tel-Dieu de Lyon",
"keywords": {
"importance": "international",
"height": null,
"place_type": "building",
"date": "C17"
},
"description": "Grand H\u00f4tel-Dieu is an internationally famous building. It was constructed in C17.",
"duration": 30,
"name_en": null,
"uuid": "a91265a8-ffbd-44f7-a7ab-3ff75f08fbab",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
},
{
"name": "Westfield La Part-Dieu",
"type": "shopping",
"location": [
45.761331,
4.855676
],
"osm_type": "way",
"osm_id": 62338376,
"attractiveness": 546,
"n_tags": 22,
"image_url": null,
"website_url": "https://fr.westfield.com/lapartdieu",
"wiki_url": "fr:La Part-Dieu (centre commercial)",
"keywords": null,
"description": null,
"duration": 30,
"name_en": null,
"uuid": "7d60316f-d689-4fcf-be68-ffc09353b826",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
},
{
"name": "Ainay",
"type": "shopping",
"location": [
45.7553105,
4.8312084
],
"osm_type": "node",
"osm_id": 5545126047,
"attractiveness": 132,
"n_tags": 0,
"image_url": null,
"website_url": null,
"wiki_url": null,
"keywords": {},
"description": null,
"duration": 30,
"name_en": null,
"uuid": "ad214f3d-a4b9-4078-876a-446caa7ab01c",
"must_do": false,
"must_avoid": false,
"is_secondary": false,
"time_to_reach_next": 0,
"next_uuid": null,
"is_viewpoint": false,
"is_place_of_worship": false
}
]

6
backend/main.py Normal file
View File

@@ -0,0 +1,6 @@
def main():
print("Hello from backend!")
if __name__ == "__main__":
main()

55
backend/pyproject.toml Normal file
View File

@@ -0,0 +1,55 @@
[project]
name = "backend"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"annotated-types==0.7.0 ; python_full_version >= '3.8'",
"anyio==4.8.0 ; python_full_version >= '3.9'",
"certifi==2024.12.14 ; python_full_version >= '3.6'",
"charset-normalizer==3.4.1 ; python_full_version >= '3.7'",
"click==8.1.8 ; python_full_version >= '3.7'",
"fastapi==0.115.7 ; python_full_version >= '3.8'",
"fastapi-cli==0.0.7 ; python_full_version >= '3.8'",
"h11==0.14.0 ; python_full_version >= '3.7'",
"httptools==0.6.4",
"idna==3.10 ; python_full_version >= '3.6'",
"joblib==1.4.2 ; python_full_version >= '3.8'",
"loki-logger-handler==1.1.0 ; python_full_version >= '2.7'",
"markdown-it-py==3.0.0 ; python_full_version >= '3.8'",
"mdurl==0.1.2 ; python_full_version >= '3.7'",
"numpy==2.2.2 ; python_full_version >= '3.10'",
"pulp==2.9.0 ; python_full_version >= '3.7'",
"pydantic==2.10.6 ; python_full_version >= '3.8'",
"pydantic-core==2.27.2 ; python_full_version >= '3.8'",
"pygments==2.19.1 ; python_full_version >= '3.8'",
"pymemcache==4.0.0 ; python_full_version >= '3.7'",
"python-dotenv==1.0.1",
"pyyaml==6.0.2",
"requests==2.32.3 ; python_full_version >= '3.8'",
"rich==13.9.4 ; python_full_version >= '3.8'",
"rich-toolkit==0.13.2 ; python_full_version >= '3.8'",
"scikit-learn==1.6.1 ; python_full_version >= '3.9'",
"scipy==1.15.1 ; python_full_version >= '3.10'",
"shapely==2.0.6 ; python_full_version >= '3.7'",
"shellingham==1.5.4 ; python_full_version >= '3.7'",
"sniffio==1.3.1 ; python_full_version >= '3.7'",
"starlette==0.45.3 ; python_full_version >= '3.9'",
"threadpoolctl==3.5.0 ; python_full_version >= '3.8'",
"typer==0.15.1 ; python_full_version >= '3.7'",
"typing-extensions==4.12.2 ; python_full_version >= '3.8'",
"urllib3==2.3.0 ; python_full_version >= '3.9'",
"uvicorn[standard]==0.34.0 ; python_full_version >= '3.9'",
"uvloop==0.21.0",
"watchfiles==1.0.4",
"websockets==14.2",
]
[dependency-groups]
dev = [
"httpx>=0.28.1",
"ipykernel>=6.30.0",
"pytest>=8.4.1",
"pytest-html>=4.1.1",
]

File diff suppressed because one or more lines are too long

View File

@@ -102,9 +102,6 @@ class ClusterManager:
selector = sel, selector = sel,
out = out out = out
) )
except Exception as e:
self.logger.warning(f"Error fetching clusters: {e}")
if result is None : if result is None :
self.logger.debug(f"Found no {cluster_type} clusters, overpass query returned no datapoints.") self.logger.debug(f"Found no {cluster_type} clusters, overpass query returned no datapoints.")
self.valid = False self.valid = False
@@ -146,9 +143,14 @@ class ClusterManager:
self.valid = False self.valid = False
else : else :
self.logger.debug(f"Detected 0 {cluster_type} clusters.") self.logger.debug(f"Found 0 {cluster_type} clusters.")
self.valid = False self.valid = False
except Exception as e:
self.logger.warning(f"Could not fetch clusters: {e}")
self.valid = False
def generate_clusters(self) -> list[Landmark]: def generate_clusters(self) -> list[Landmark]:
""" """

View File

@@ -4,7 +4,6 @@ import yaml
from ..structs.preferences import Preferences from ..structs.preferences import Preferences
from ..structs.landmark import Landmark from ..structs.landmark import Landmark
from ..utils.take_most_important import take_most_important
from .cluster_manager import ClusterManager from .cluster_manager import ClusterManager
from ..overpass.overpass import Overpass, get_base_info from ..overpass.overpass import Overpass, get_base_info
from ..utils.bbox import create_bbox from ..utils.bbox import create_bbox
@@ -23,7 +22,7 @@ class LandmarkManager:
church_coeff: float # coeff to adjsut score of churches church_coeff: float # coeff to adjsut score of churches
nature_coeff: float # coeff to adjust score of parks nature_coeff: float # coeff to adjust score of parks
overall_coeff: float # coeff to adjust weight of tags overall_coeff: float # coeff to adjust weight of tags
n_important: int # number of important landmarks to consider # n_important: int # number of important landmarks to consider
def __init__(self) -> None: def __init__(self) -> None:
@@ -42,7 +41,7 @@ class LandmarkManager:
self.wikipedia_bonus = parameters['wikipedia_bonus'] self.wikipedia_bonus = parameters['wikipedia_bonus']
self.viewpoint_bonus = parameters['viewpoint_bonus'] self.viewpoint_bonus = parameters['viewpoint_bonus']
self.pay_bonus = parameters['pay_bonus'] self.pay_bonus = parameters['pay_bonus']
self.n_important = parameters['N_important'] # self.n_important = parameters['N_important']
with OPTIMIZER_PARAMETERS_PATH.open('r') as f: with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f) parameters = yaml.safe_load(f)
@@ -55,7 +54,12 @@ class LandmarkManager:
self.logger.info('LandmakManager successfully initialized.') self.logger.info('LandmakManager successfully initialized.')
def generate_landmarks_list(self, center_coordinates: tuple[float, float], preferences: Preferences) -> tuple[list[Landmark], list[Landmark]]: def generate_landmarks_list(
self,
center_coordinates: tuple[float, float],
preferences: Preferences,
allow_clusters: bool = True
) -> list[Landmark] :
""" """
Generate and prioritize a list of landmarks based on user preferences. Generate and prioritize a list of landmarks based on user preferences.
@@ -63,16 +67,17 @@ class LandmarkManager:
and current location. It scores and corrects these landmarks, removes duplicates, and then selects the most important and current location. It scores and corrects these landmarks, removes duplicates, and then selects the most important
landmarks based on a predefined criterion. landmarks based on a predefined criterion.
Args: Parameters :
center_coordinates (tuple[float, float]): The latitude and longitude of the center location around which to search. center_coordinates (tuple[float, float]): The latitude and longitude of the center location around which to search.
preferences (Preferences): The user's preference settings that influence the landmark selection. preferences (Preferences): The user's preference settings that influence the landmark selection.
allow_clusters (bool, optional) : If set to False, no clusters will be fetched. Mainly used for the option to fetch landmarks nearby.
Returns: Returns:
tuple[list[Landmark], list[Landmark]]: tuple[list[Landmark], list[Landmark]]:
- A list of all existing landmarks. - A list of all existing landmarks.
- A list of the most important landmarks based on the user's preferences. - A list of the most important landmarks based on the user's preferences.
""" """
self.logger.debug('Starting to fetch landmarks...') self.logger.info(f'Starting to fetch landmarks around {center_coordinates}...')
max_walk_dist = int((preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor) max_walk_dist = int((preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor)
radius = min(max_walk_dist, int(self.max_bbox_side/2)) radius = min(max_walk_dist, int(self.max_bbox_side/2))
@@ -89,6 +94,7 @@ class LandmarkManager:
all_landmarks.update(current_landmarks) all_landmarks.update(current_landmarks)
self.logger.info(f'Found {len(current_landmarks)} sightseeing landmarks') self.logger.info(f'Found {len(current_landmarks)} sightseeing landmarks')
if allow_clusters :
# special pipeline for historic neighborhoods # special pipeline for historic neighborhoods
neighborhood_manager = ClusterManager(bbox, 'sightseeing') neighborhood_manager = ClusterManager(bbox, 'sightseeing')
historic_clusters = neighborhood_manager.generate_clusters() historic_clusters = neighborhood_manager.generate_clusters()
@@ -113,16 +119,19 @@ class LandmarkManager:
landmark.duration = 30 landmark.duration = 30
all_landmarks.update(current_landmarks) all_landmarks.update(current_landmarks)
if allow_clusters :
# special pipeline for shopping malls # special pipeline for shopping malls
shopping_manager = ClusterManager(bbox, 'shopping') shopping_manager = ClusterManager(bbox, 'shopping')
shopping_clusters = shopping_manager.generate_clusters() shopping_clusters = shopping_manager.generate_clusters()
all_landmarks.update(shopping_clusters) all_landmarks.update(shopping_clusters)
landmarks_constrained = take_most_important(all_landmarks, self.n_important) # DETAILS HERE
# self.logger.info(f'All landmarks generated : {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.') # self.logger.info(f'All landmarks generated : {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.')
self.logger.info(f'Found {len(all_landmarks)} landmarks in total.')
return sorted(all_landmarks, key=lambda x: x.attractiveness, reverse=True)
return all_landmarks, landmarks_constrained
def set_landmark_score(self, landmark: Landmark, landmarktype: str, preference_level: int) : def set_landmark_score(self, landmark: Landmark, landmarktype: str, preference_level: int) :
""" """
@@ -236,6 +245,17 @@ class LandmarkManager:
continue continue
tags = elem.get('tags') tags = elem.get('tags')
n_tags=len(tags)
# Skip this landmark if not suitable
if tags.get('building:part') is not None :
continue
if tags.get('disused') is not None :
continue
if tags.get('boundary') is not None :
continue
if tags.get('shop') is not None and landmarktype != 'shopping' :
continue
# Convert this to Landmark object # Convert this to Landmark object
landmark = Landmark(name=name, landmark = Landmark(name=name,
@@ -244,180 +264,36 @@ class LandmarkManager:
osm_id=id, osm_id=id,
osm_type=osm_type, osm_type=osm_type,
attractiveness=0, attractiveness=0,
n_tags=len(tags)) n_tags=n_tags)
# Browse through tags to add information to landmark. # Extract useful information for score calculation later down the road.
for key, value in tags.items(): landmark.image_url = tags.get('image')
landmark.website_url = tags.get('website')
landmark.wiki_url = tags.get('wikipedia')
landmark.name_en = tags.get('name:en')
# Skip this landmark if not suitable. # Check for place of worship
if key == 'building:part' and value == 'yes' : if tags.get('place_of_worship') is not None :
break
if 'disused:' in key :
break
if 'boundary:' in key :
break
if 'shop' in key and landmarktype != 'shopping' :
break
# if value == 'apartments' :
# break
# Fill in the other attributes.
if key == 'image' :
landmark.image_url = value
if key == 'website' :
landmark.website_url = value
if value == 'place_of_worship' :
landmark.is_place_of_worship = True landmark.is_place_of_worship = True
if key == 'wikipedia' : landmark.name_en = tags.get('place_of_worship')
landmark.wiki_url = value
if key == 'name:en' :
landmark.name_en = value
if 'building:' in key or 'pay' in key :
landmark.n_tags -= 1
# Set the duration. Needed for the optimization.
# Set the duration. if tags.get('amenity') in ['aquarium', 'planetarium'] or tags.get('tourism') in ['aquarium', 'museum', 'zoo']:
if value in ['museum', 'aquarium', 'planetarium'] :
landmark.duration = 60 landmark.duration = 60
elif value == 'viewpoint' : elif tags.get('tourism') == 'viewpoint' :
landmark.is_viewpoint = True landmark.is_viewpoint = True
landmark.duration = 10 landmark.duration = 10
elif value == 'cathedral' : elif tags.get('building') == 'cathedral' :
landmark.is_place_of_worship = False landmark.is_place_of_worship = False
landmark.duration = 10 landmark.duration = 10
landmark.description, landmark.keywords = self.description_and_keywords(tags) # Compute the score and add landmark to the list.
self.set_landmark_score(landmark, landmarktype, preference_level) self.set_landmark_score(landmark, landmarktype, preference_level)
landmarks.append(landmark) landmarks.append(landmark)
continue
return landmarks return landmarks
def description_and_keywords(self, tags: dict):
"""
Generates a description and a set of keywords for a given landmark based on its tags.
Params:
tags (dict): A dictionary containing metadata about the landmark, including its name,
importance, height, date of construction, and visitor information.
Returns:
description (str): A string description of the landmark.
keywords (dict): A dictionary of keywords with fields such as 'importance', 'height',
'place_type', and 'date'.
"""
# Extract relevant fields
name = tags.get('name')
importance = tags.get('importance', None)
n_visitors = tags.get('tourism:visitors', None)
height = tags.get('height')
place_type = self.get_place_type(tags)
date = self.get_date(tags)
if place_type is None :
return None, None
# Start the description.
if importance is None :
if len(tags.keys()) < 5 :
return None, None
if len(tags.keys()) < 10 :
description = f"{name} is a well known {place_type}."
elif len(tags.keys()) < 17 :
importance = 'national'
description = f"{name} is a {place_type} of national importance."
else :
importance = 'international'
description = f"{name} is an internationally famous {place_type}."
else :
description = f"{name} is a {place_type} of {importance} importance."
if height is not None and date is not None :
description += f" This {place_type} was constructed in {date} and is ca. {height} meters high."
elif height is not None :
description += f" This {place_type} stands ca. {height} meters tall."
elif date is not None:
description += f" It was constructed in {date}."
# Format the visitor number
if n_visitors is not None :
n_visitors = int(n_visitors)
if n_visitors < 1000000 :
description += f" It welcomes {int(n_visitors/1000)} thousand visitors every year."
else :
description += f" It welcomes {round(n_visitors/1000000, 1)} million visitors every year."
# Set the keywords.
keywords = {"importance": importance,
"height": height,
"place_type": place_type,
"date": date}
return description, keywords
def get_place_type(self, data):
"""
Determines the type of the place based on available tags such as 'amenity', 'building',
'historic', and 'leisure'. The priority order is: 'historic' > 'building' (if not generic) >
'amenity' > 'leisure'.
Params:
data (dict): A dictionary containing metadata about the place.
Returns:
place_type (str): The determined type of the place, or None if no relevant type is found.
"""
amenity = data.get('amenity', None)
building = data.get('building', None)
historic = data.get('historic', None)
leisure = data.get('leisure')
if historic and historic != "yes":
return historic
if building and building not in ["yes", "civic", "government", "apartments", "residential", "commericial", "industrial", "retail", "religious", "public", "service"]:
return building
if amenity:
return amenity
if leisure:
return leisure
return None
def get_date(self, data):
"""
Extracts the most relevant date from the available tags, prioritizing 'construction_date',
'start_date', 'year_of_construction', and 'opening_date' in that order.
Params:
data (dict): A dictionary containing metadata about the place.
Returns:
date (str): The most relevant date found, or None if no date is available.
"""
construction_date = data.get('construction_date', None)
opening_date = data.get('opening_date', None)
start_date = data.get('start_date', None)
year_of_construction = data.get('year_of_construction', None)
# Prioritize based on availability
if construction_date:
return construction_date
if start_date:
return start_date
if year_of_construction:
return year_of_construction
if opening_date:
return opening_date
return None
def dict_to_selector_list(d: dict) -> list: def dict_to_selector_list(d: dict) -> list:
""" """
Convert a dictionary of key-value pairs to a list of Overpass query strings. Convert a dictionary of key-value pairs to a list of Overpass query strings.

View File

@@ -0,0 +1,123 @@
"""Main app for backend api"""
import logging
import time
import random
from fastapi import HTTPException, APIRouter
from ..structs.landmark import Landmark
from ..structs.preferences import Preferences, Preference
from .landmarks_manager import LandmarkManager
# Setup the logger and the Landmarks Manager
logger = logging.getLogger(__name__)
manager = LandmarkManager()
# Initialize the API router
router = APIRouter()
@router.post("/get/landmarks")
def get_landmarks(
preferences: Preferences,
start: tuple[float, float],
) -> list[Landmark]:
"""
Function that returns all available landmarks given some preferences and a start position.
Args:
preferences : the preferences specified by the user as the post body
start : the coordinates of the starting point
Returns:
list[Landmark] : The full list of fetched landmarks
"""
if preferences is None:
raise HTTPException(status_code=406, detail="Preferences not provided or incomplete.")
if (preferences.shopping.score == 0 and
preferences.sightseeing.score == 0 and
preferences.nature.score == 0) :
raise HTTPException(status_code=406, detail="All preferences are 0.")
if start is None:
raise HTTPException(status_code=406, detail="Start coordinates not provided")
if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180):
raise HTTPException(status_code=422, detail="Start coordinates not in range")
logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}")
start_time = time.time()
# Generate the landmarks from the start location
landmarks = manager.generate_landmarks_list(
center_coordinates = start,
preferences = preferences
)
if len(landmarks) == 0 :
raise HTTPException(status_code=500, detail="No landmarks were found.")
t_generate_landmarks = time.time() - start_time
logger.info(f'Fetched {len(landmarks)} landmarks in \t: {round(t_generate_landmarks,3)} seconds')
return landmarks
@router.post("/get-nearby/landmarks/{lat}/{lon}")
def get_landmarks_nearby(
lat: float,
lon: float
) -> list[Landmark] :
"""
Suggests nearby landmarks based on a given latitude and longitude.
This endpoint returns a curated list of up to 5 landmarks around the given geographical coordinates. It uses fixed preferences for
sightseeing, shopping, and nature, with a maximum time constraint of 30 minutes to limit the number of landmarks returned.
Args:
lat (float): Latitude of the user's current location.
lon (float): Longitude of the user's current location.
Returns:
list[Landmark]: A list of selected nearby landmarks.
"""
logger.info(f'Fetching landmarks nearby ({lat}, {lon}).')
# Define fixed preferences:
prefs = Preferences(
sightseeing = Preference(
type='sightseeing',
score=5
),
shopping = Preference(
type='shopping',
score=2
),
nature = Preference(
type='nature',
score=5
),
max_time_minute=30,
detour_tolerance_minute=0,
)
# Find the landmarks around the location
landmarks_around = manager.generate_landmarks_list(
center_coordinates = (lat, lon),
preferences = prefs,
allow_clusters=False,
)
if len(landmarks_around) == 0 :
raise HTTPException(status_code=500, detail="No landmarks were found.")
# select 8 - 12 landmarks from there
if len(landmarks_around) > 8 :
n_imp = random.randint(2,5)
rest = random.randint(8 - n_imp, min(12, len(landmarks_around))-n_imp)
print(f'len = {len(landmarks_around)}\nn_imp = {n_imp}\nrest = {rest}')
landmarks_around = landmarks_around[:n_imp] + random.sample(landmarks_around[n_imp:], rest)
logger.info(f'Found {len(landmarks_around)} landmarks to suggest nearby ({lat}, {lon}).')
# logger.debug('Suggested landmarks :\n\t' + '\n\t'.join(f'{landmark}' for landmark in landmarks_around))
return landmarks_around

View File

@@ -33,14 +33,14 @@ def configure_logging():
# silence the chatty logs loki generates itself # silence the chatty logs loki generates itself
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING) logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
# no need for time since it's added by loki or can be shown in kube logs # no need for time since it's added by loki or can be shown in kube logs
logging_format = '%(name)s - %(levelname)s - %(message)s' logging_format = '%(name)-55s - %(levelname)-7s - %(message)s'
else: else:
# if we are in a debug (local) session, set verbose and rich logging # if we are in a debug (local) session, set verbose and rich logging
from rich.logging import RichHandler from rich.logging import RichHandler
logging_handlers = [RichHandler()] logging_handlers = [RichHandler()]
logging_level = logging.DEBUG if is_debug else logging.INFO logging_level = logging.DEBUG if is_debug else logging.INFO
logging_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' logging_format = '%(asctime)s - %(name)-55s - %(levelname)-7s - %(message)s'

View File

@@ -1,19 +1,18 @@
"""Main app for backend api""" """Main app for backend api"""
import logging import logging
import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, BackgroundTasks from fastapi import FastAPI, HTTPException
from .logging_config import configure_logging from .logging_config import configure_logging
from .structs.landmark import Landmark from .structs.landmark import Landmark
from .structs.preferences import Preferences
from .structs.linked_landmarks import LinkedLandmarks from .structs.linked_landmarks import LinkedLandmarks
from .structs.trip import Trip from .structs.trip import Trip
from .landmarks.landmarks_manager import LandmarkManager from .landmarks.landmarks_manager import LandmarkManager
from .toilets.toilet_routes import router as toilets_router from .toilets.toilets_router import router as toilets_router
from .optimization.optimization_router import router as optimization_router
from .landmarks.landmarks_router import router as landmarks_router, get_landmarks_nearby
from .optimization.optimizer import Optimizer from .optimization.optimizer import Optimizer
from .optimization.refiner import Refiner from .optimization.refiner import Refiner
from .overpass.overpass import fill_cache
from .cache import client as cache_client from .cache import client as cache_client
@@ -37,115 +36,22 @@ app = FastAPI(lifespan=lifespan)
# Fetches the global list of landmarks given preferences and start/end coordinates. Two routes
# Call with "/get/landmarks/" for main entry point of the trip generation pipeline.
# Call with "/get-nearby/landmarks/" for the NEARBY feature.
app.include_router(landmarks_router)
# Optimizes the trip given preferences. Second step in the main trip generation pipeline
# Call with "/optimize/trip"
app.include_router(optimization_router)
# Fetches toilets near given coordinates.
# Call with "/get/toilets" for fetching toilets around coordinates
app.include_router(toilets_router) app.include_router(toilets_router)
@app.post("/trip/new")
def new_trip(preferences: Preferences,
start: tuple[float, float],
end: tuple[float, float] | None = None,
background_tasks: BackgroundTasks = None) -> Trip:
"""
Main function to call the optimizer.
Args:
preferences : the preferences specified by the user as the post body
start : the coordinates of the starting point
end : the coordinates of the finishing point
Returns:
(uuid) : The uuid of the first landmark in the optimized route
"""
if preferences is None:
raise HTTPException(status_code=406, detail="Preferences not provided or incomplete.")
if (preferences.shopping.score == 0 and
preferences.sightseeing.score == 0 and
preferences.nature.score == 0) :
raise HTTPException(status_code=406, detail="All preferences are 0.")
if start is None:
raise HTTPException(status_code=406, detail="Start coordinates not provided")
if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180):
raise HTTPException(status_code=422, detail="Start coordinates not in range")
if end is None:
end = start
logger.info("No end coordinates provided. Using start=end.")
logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}")
start_landmark = Landmark(name='start',
type='start',
location=(start[0], start[1]),
osm_type='start',
osm_id=0,
attractiveness=0,
duration=0,
must_do=True,
n_tags = 0)
end_landmark = Landmark(name='finish',
type='finish',
location=(end[0], end[1]),
osm_type='end',
osm_id=0,
attractiveness=0,
duration=0,
must_do=True,
n_tags=0)
start_time = time.time()
# Generate the landmarks from the start location
landmarks, landmarks_short = manager.generate_landmarks_list(
center_coordinates = start,
preferences = preferences
)
if len(landmarks) == 0 :
raise HTTPException(status_code=500, detail="No landmarks were found.")
# insert start and finish to the landmarks list
landmarks_short.insert(0, start_landmark)
landmarks_short.append(end_landmark)
t_generate_landmarks = time.time() - start_time
logger.info(f'Fetched {len(landmarks)} landmarks in \t: {round(t_generate_landmarks,3)} seconds')
start_time = time.time()
# First stage optimization
try:
base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
except Exception as exc:
logger.error(f"Trip generation failed: {str(exc)}")
raise HTTPException(status_code=500, detail=f"Optimization failed: {str(exc)}") from exc
t_first_stage = time.time() - start_time
start_time = time.time()
# Second stage optimization
# TODO : only if necessary (not enough landmarks for ex.)
try :
refined_tour = refiner.refine_optimization(landmarks, base_tour,
preferences.max_time_minute,
preferences.detour_tolerance_minute)
except Exception as exc :
logger.warning(f"Refiner failed. Proceeding with base trip {str(exc)}")
refined_tour = base_tour
t_second_stage = time.time() - start_time
logger.debug(f'First stage optimization\t: {round(t_first_stage,3)} seconds')
logger.debug(f'Second stage optimization\t: {round(t_second_stage,3)} seconds')
logger.info(f'Total computation time\t: {round(t_first_stage + t_second_stage,3)} seconds')
linked_tour = LinkedLandmarks(refined_tour)
# upon creation of the trip, persistence of both the trip and its landmarks is ensured.
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
logger.info(f'Generated a trip of {trip.total_time} minutes with {len(refined_tour)} landmarks in {round(t_generate_landmarks + t_first_stage + t_second_stage,3)} seconds.')
logger.debug('Detailed trip :\n\t' + '\n\t'.join(f'{landmark}' for landmark in refined_tour))
background_tasks.add_task(fill_cache)
return trip
#### For already existing trips/landmarks #### For already existing trips/landmarks
@app.get("/trip/{trip_uuid}") @app.get("/trip/{trip_uuid}")
@@ -224,3 +130,4 @@ def update_trip_time(trip_uuid: str, removed_landmark_uuid: str) -> Trip:
trip = Trip.from_linked_landmarks(linked_tour, cache_client) trip = Trip.from_linked_landmarks(linked_tour, cache_client)
return trip return trip

View File

@@ -0,0 +1,141 @@
"""API entry point for the trip optimization."""
import logging
import time
import yaml
from fastapi import HTTPException, APIRouter, BackgroundTasks
from .optimizer import Optimizer
from .refiner import Refiner
from ..structs.landmark import Landmark
from ..structs.preferences import Preferences
from ..structs.linked_landmarks import LinkedLandmarks
from ..structs.trip import Trip
from ..overpass.overpass import fill_cache
from ..cache import client as cache_client
from ..constants import OPTIMIZER_PARAMETERS_PATH
# Setup the Logger, Optimizer and Refiner
logger = logging.getLogger(__name__)
optimizer = Optimizer()
refiner = Refiner(optimizer=optimizer)
# Initialize the API router
router = APIRouter()
@router.post("/optimize/trip")
def optimize_trip(
preferences: Preferences,
landmarks: list[Landmark],
start: tuple[float, float],
end: tuple[float, float] | None = None,
background_tasks: BackgroundTasks = None
) -> Trip:
"""
Main function to call the optimizer.
Args:
preferences (Preferences) : the preferences specified by the user as the post body.
start (tuple[float, float]) : the coordinates of the starting point.
end tuple[float, float] : the coordinates of the finishing point.
backgroud_tasks (BackgroundTasks) : necessary to fill the cache after the trip has been returned.
Returns:
(uuid) : The uuid of the first landmark in the optimized route
"""
if preferences is None:
raise HTTPException(status_code=406, detail="Preferences not provided or incomplete.")
if len(landmarks) == 0 :
raise HTTPException(status_code=406, detail="No landmarks provided for computing the trip.")
if (preferences.shopping.score == 0 and
preferences.sightseeing.score == 0 and
preferences.nature.score == 0) :
raise HTTPException(status_code=406, detail="All preferences are 0.")
if start is None:
raise HTTPException(status_code=406, detail="Start coordinates not provided")
if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180):
raise HTTPException(status_code=422, detail="Start coordinates not in range")
if end is None:
end = start
logger.info("No end coordinates provided. Using start=end.")
# Start the timer
start_time = time.time()
logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}")
start_landmark = Landmark(
name='start',
type='start',
location=(start[0], start[1]),
osm_type='start',
osm_id=0,
attractiveness=0,
duration=0,
must_do=True,
n_tags = 0
)
end_landmark = Landmark(
name='finish',
type='finish',
location=(end[0], end[1]),
osm_type='end',
osm_id=0,
attractiveness=0,
duration=0,
must_do=True,
n_tags=0
)
# From the parameters load the length at which to truncate the landmarks list.
with OPTIMIZER_PARAMETERS_PATH.open('r') as f:
parameters = yaml.safe_load(f)
n_important = parameters['N_important']
# Truncate to the most important landmarks for a shorter list
landmarks_short = landmarks[:n_important]
# insert start and finish to the shorter landmarks list
landmarks_short.insert(0, start_landmark)
landmarks_short.append(end_landmark)
# First stage optimization
try:
base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
except Exception as exc:
logger.error(f"Trip generation failed: {str(exc)}")
raise HTTPException(status_code=500, detail=f"Optimization failed: {str(exc)}") from exc
t_first_stage = time.time() - start_time
start_time = time.time()
# Second stage optimization
try :
refined_tour = refiner.refine_optimization(
landmarks, base_tour,
preferences.max_time_minute,
preferences.detour_tolerance_minute
)
except Exception as exc :
logger.warning(f"Refiner failed. Proceeding with base trip {str(exc)}")
refined_tour = base_tour
t_second_stage = time.time() - start_time
logger.debug(f'First stage optimization\t: {round(t_first_stage,3)} seconds')
logger.debug(f'Second stage optimization\t: {round(t_second_stage,3)} seconds')
logger.info(f'Total computation time\t: {round(t_first_stage + t_second_stage,3)} seconds')
linked_tour = LinkedLandmarks(refined_tour)
# upon creation of the trip, persistence of both the trip and its landmarks is ensured.
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
logger.info(f'Optimized a trip of {trip.total_time} minutes with {len(refined_tour)} landmarks in {round(t_first_stage + t_second_stage,3)} seconds.')
logger.info('Detailed trip :\n\t' + '\n\t'.join(f'{landmark}' for landmark in refined_tour))
background_tasks.add_task(fill_cache)
return trip

View File

@@ -6,7 +6,6 @@ from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull
from ..structs.landmark import Landmark from ..structs.landmark import Landmark
from ..utils.get_time_distance import get_time from ..utils.get_time_distance import get_time
from ..utils.take_most_important import take_most_important
from .optimizer import Optimizer from .optimizer import Optimizer
from ..constants import OPTIMIZER_PARAMETERS_PATH from ..constants import OPTIMIZER_PARAMETERS_PATH
@@ -238,7 +237,7 @@ class Refiner :
if self.is_in_area(area, landmark.location) and landmark.name not in visited_names: if self.is_in_area(area, landmark.location) and landmark.name not in visited_names:
second_order_landmarks.append(landmark) second_order_landmarks.append(landmark)
return take_most_important(second_order_landmarks, int(self.max_landmarks_refiner*0.75)) return sorted(second_order_landmarks, key=lambda x: x.attractiveness, reverse=True)[:int(self.max_landmarks_refiner*0.75)]
# Try fix the shortest path using shapely # Try fix the shortest path using shapely

View File

@@ -402,6 +402,8 @@ def fill_cache():
n_files = 0 n_files = 0
total = 0 total = 0
overpass.logger.info('Trip successfully returned, starting to fill cache.')
with os.scandir(OSM_CACHE_DIR) as it: with os.scandir(OSM_CACHE_DIR) as it:
for entry in it: for entry in it:
if entry.is_file() and entry.name.startswith('hollow_'): if entry.is_file() and entry.name.startswith('hollow_'):

View File

@@ -7,5 +7,4 @@ tag_exponent: 1.15
image_bonus: 1.1 image_bonus: 1.1
viewpoint_bonus: 10 viewpoint_bonus: 10
wikipedia_bonus: 1.25 wikipedia_bonus: 1.25
N_important: 60
pay_bonus: -1 pay_bonus: -1

View File

@@ -7,3 +7,4 @@ overshoot: 0.0016
time_limit: 1 time_limit: 1
gap_rel: 0.025 gap_rel: 0.025
max_iter: 80 max_iter: 80
N_important: 60

View File

@@ -49,8 +49,8 @@ class Landmark(BaseModel) :
image_url : Optional[str] = None image_url : Optional[str] = None
website_url : Optional[str] = None website_url : Optional[str] = None
wiki_url : Optional[str] = None wiki_url : Optional[str] = None
keywords: Optional[dict] = {} # keywords: Optional[dict] = {}
description : Optional[str] = None # description : Optional[str] = None
duration : Optional[int] = 5 duration : Optional[int] = 5
name_en : Optional[str] = None name_en : Optional[str] = None

View File

@@ -2,6 +2,7 @@
from .landmark import Landmark from .landmark import Landmark
from ..utils.get_time_distance import get_time from ..utils.get_time_distance import get_time
from ..utils.description import description_and_keywords
class LinkedLandmarks: class LinkedLandmarks:
""" """
@@ -35,18 +36,23 @@ class LinkedLandmarks:
Create the links between the landmarks in the list by setting their Create the links between the landmarks in the list by setting their
.next_uuid and the .time_to_next attributes. .next_uuid and the .time_to_next attributes.
""" """
# Mark secondary landmarks as such # Mark secondary landmarks as such
self.update_secondary_landmarks() self.update_secondary_landmarks()
for i, landmark in enumerate(self._landmarks[:-1]): for i, landmark in enumerate(self._landmarks[:-1]):
# Set uuid of the next landmark
landmark.next_uuid = self._landmarks[i + 1].uuid landmark.next_uuid = self._landmarks[i + 1].uuid
# Adjust time to reach and total time
time_to_next = get_time(landmark.location, self._landmarks[i + 1].location) time_to_next = get_time(landmark.location, self._landmarks[i + 1].location)
landmark.time_to_reach_next = time_to_next landmark.time_to_reach_next = time_to_next
self.total_time += time_to_next self.total_time += time_to_next
self.total_time += landmark.duration self.total_time += landmark.duration
# Fill in the keywords and description. GOOD IDEA, BAD EXECUTION, tags aren't available anymore at this stage
# landmark.description, landmark.keywords = description_and_keywords(tags)
self._landmarks[-1].next_uuid = None self._landmarks[-1].next_uuid = None
self._landmarks[-1].time_to_reach_next = 0 self._landmarks[-1].time_to_reach_next = 0

View File

@@ -1,7 +1,7 @@
"""Defines the Preferences used as input for trip generation.""" """Defines the Preferences used as input for trip generation."""
from typing import Optional, Literal from typing import Optional, Literal
from pydantic import BaseModel from pydantic import BaseModel, field_validator
class Preference(BaseModel) : class Preference(BaseModel) :
@@ -15,6 +15,13 @@ class Preference(BaseModel) :
type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish'] type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish']
score: int # score could be from 1 to 5 score: int # score could be from 1 to 5
@field_validator("type")
@classmethod
def validate_type(cls, v):
if v not in {'sightseeing', 'nature', 'shopping', 'start', 'finish'}:
raise ValueError(f"Invalid type: {v}")
return v
# Input for optimization # Input for optimization
class Preferences(BaseModel) : class Preferences(BaseModel) :

View File

@@ -19,30 +19,50 @@ def invalid_client():
([48.8566, 2.3522], {}, 422), ([48.8566, 2.3522], {}, 422),
# Invalid cases: incomplete preferences. # Invalid cases: incomplete preferences.
([48.084588, 7.280405], {"sightseeing": {"type": "nature", "score": 5}, # no shopping ([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 5}, # no shopping pref
"nature": {"type": "nature", "score": 5}, "nature": {"type": "nature", "score": 5},
}, 422), }, 422),
([48.084588, 7.280405], {"sightseeing": {"type": "nature", "score": 5}, # no nature ([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 5}, # no nature pref
"shopping": {"type": "shopping", "score": 5}, "shopping": {"type": "shopping", "score": 5},
}, 422), }, 422),
([48.084588, 7.280405], {"nature": {"type": "nature", "score": 5}, # no sightseeing ([48.084588, 7.280405], {"nature": {"type": "nature", "score": 5}, # no sightseeing pref
"shopping": {"type": "shopping", "score": 5}, "shopping": {"type": "shopping", "score": 5},
}, 422), }, 422),
([48.084588, 7.280405], {"sightseeing": {"type": "nature", "score": 1}, # mixed up preferences types. TODO: i suggest reducing the complexity by remove the Preference object.
"nature": {"type": "shopping", "score": 1},
"shopping": {"type": "shopping", "score": 1},
}, 422),
([48.084588, 7.280405], {"doesnotexist": {"type": "sightseeing", "score": 2}, # non-existing preferences types
"nature": {"type": "nature", "score": 2},
"shopping": {"type": "shopping", "score": 2},
}, 422),
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 3}, # non-existing preferences types
"nature": {"type": "doesntexisteither", "score": 3},
"shopping": {"type": "shopping", "score": 3},
}, 422),
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": -1}, # negative preference value
"nature": {"type": "doesntexisteither", "score": 4},
"shopping": {"type": "shopping", "score": 4},
}, 422),
([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 10}, # too high preference value
"nature": {"type": "doesntexisteither", "score": 4},
"shopping": {"type": "shopping", "score": 4},
}, 422),
# Invalid cases: unexisting coords # Invalid cases: unexisting coords
([91, 181], {"sightseeing": {"type": "nature", "score": 5}, ([91, 181], {"sightseeing": {"type": "sightseeing", "score": 5},
"nature": {"type": "nature", "score": 5}, "nature": {"type": "nature", "score": 5},
"shopping": {"type": "shopping", "score": 5}, "shopping": {"type": "shopping", "score": 5},
}, 422), }, 422),
([-91, 181], {"sightseeing": {"type": "nature", "score": 5}, ([-91, 181], {"sightseeing": {"type": "sightseeing", "score": 5},
"nature": {"type": "nature", "score": 5}, "nature": {"type": "nature", "score": 5},
"shopping": {"type": "shopping", "score": 5}, "shopping": {"type": "shopping", "score": 5},
}, 422), }, 422),
([91, -181], {"sightseeing": {"type": "nature", "score": 5}, ([91, -181], {"sightseeing": {"type": "sightseeing", "score": 5},
"nature": {"type": "nature", "score": 5}, "nature": {"type": "nature", "score": 5},
"shopping": {"type": "shopping", "score": 5}, "shopping": {"type": "shopping", "score": 5},
}, 422), }, 422),
([-91, -181], {"sightseeing": {"type": "nature", "score": 5}, ([-91, -181], {"sightseeing": {"type": "sightseeing", "score": 5},
"nature": {"type": "nature", "score": 5}, "nature": {"type": "nature", "score": 5},
"shopping": {"type": "shopping", "score": 5}, "shopping": {"type": "shopping", "score": 5},
}, 422), }, 422),
@@ -53,8 +73,8 @@ def test_input(invalid_client, start, preferences, status_code): # pylint: dis
Test new trip creation with different sets of preferences and locations. Test new trip creation with different sets of preferences and locations.
""" """
response = invalid_client.post( response = invalid_client.post(
"/trip/new", "/get/landmarks",
json={ json ={
"preferences": preferences, "preferences": preferences,
"start": start "start": start
} }

View File

@@ -1,343 +0,0 @@
"""Collection of tests to ensure correct implementation and track progress. """
import time
from fastapi.testclient import TestClient
import pytest
from .test_utils import load_trip_landmarks, log_trip_details
from ..main import app
@pytest.fixture(scope="module")
def client():
"""Client used to call the app."""
return TestClient(app)
def test_turckheim(client, request): # pylint: disable=redefined-outer-name
"""
Test n°1 : Custom test in Turckheim to ensure small villages are also supported.
Args:
client:
request:
"""
start_time = time.time() # Start timer
duration_minutes = 20
response = client.post(
"/trip/new",
json={
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
"nature": {"type": "nature", "score": 0},
"shopping": {"type": "shopping", "score": 0},
"max_time_minute": duration_minutes,
"detour_tolerance_minute": 0},
"start": [48.084588, 7.280405]
# "start": [45.74445023349939, 4.8222687890538865]
# "start": [45.75156398104873, 4.827154464827647]
}
)
result = response.json()
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
# Get computation time
comp_time = time.time() - start_time
# Add details to report
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
# checks :
assert response.status_code == 200 # check for successful planning
assert isinstance(landmarks, list) # check that the return type is a list
assert len(landmarks) > 2 # check that there is something to visit
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
# assert 2!= 3
def test_bellecour(client, request) : # pylint: disable=redefined-outer-name
"""
Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area.
Args:
client:
request:
"""
start_time = time.time() # Start timer
duration_minutes = 120
response = client.post(
"/trip/new",
json={
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
"nature": {"type": "nature", "score": 5},
"shopping": {"type": "shopping", "score": 5},
"max_time_minute": duration_minutes,
"detour_tolerance_minute": 0},
"start": [45.7576485, 4.8330241]
}
)
result = response.json()
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
# Get computation time
comp_time = time.time() - start_time
# Add details to report
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
# for elem in landmarks :
# print(elem)
# checks :
assert response.status_code == 200 # check for successful planning
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
def test_cologne(client, request) : # pylint: disable=redefined-outer-name
"""
Test n°3 : Custom test in Cologne to ensure proper decision making in crowded area.
Args:
client:
request:
"""
start_time = time.time() # Start timer
duration_minutes = 240
response = client.post(
"/trip/new",
json={
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
"nature": {"type": "nature", "score": 5},
"shopping": {"type": "shopping", "score": 5},
"max_time_minute": duration_minutes,
"detour_tolerance_minute": 0},
"start": [50.942352665, 6.957777972392]
}
)
result = response.json()
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
# Get computation time
comp_time = time.time() - start_time
# Add details to report
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
# for elem in landmarks :
# print(elem)
# checks :
assert response.status_code == 200 # check for successful planning
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
def test_strasbourg(client, request) : # pylint: disable=redefined-outer-name
"""
Test n°4 : Custom test in Strasbourg to ensure proper decision making in crowded area.
Args:
client:
request:
"""
start_time = time.time() # Start timer
duration_minutes = 180
response = client.post(
"/trip/new",
json={
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
"nature": {"type": "nature", "score": 5},
"shopping": {"type": "shopping", "score": 5},
"max_time_minute": duration_minutes,
"detour_tolerance_minute": 0},
"start": [48.5846589226, 7.74078715721]
}
)
result = response.json()
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
# Get computation time
comp_time = time.time() - start_time
# Add details to report
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
# for elem in landmarks :
# print(elem)
# checks :
assert response.status_code == 200 # check for successful planning
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
def test_zurich(client, request) : # pylint: disable=redefined-outer-name
"""
Test n°5 : Custom test in Zurich to ensure proper decision making in crowded area.
Args:
client:
request:
"""
start_time = time.time() # Start timer
duration_minutes = 180
response = client.post(
"/trip/new",
json={
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
"nature": {"type": "nature", "score": 5},
"shopping": {"type": "shopping", "score": 5},
"max_time_minute": duration_minutes,
"detour_tolerance_minute": 0},
"start": [47.377884227, 8.5395114066]
}
)
result = response.json()
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
# Get computation time
comp_time = time.time() - start_time
# Add details to report
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
# for elem in landmarks :
# print(elem)
# checks :
assert response.status_code == 200 # check for successful planning
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
def test_paris(client, request) : # pylint: disable=redefined-outer-name
"""
Test n°6 : Custom test in Paris (les Halles) centre to ensure proper decision making in crowded area.
Args:
client:
request:
"""
start_time = time.time() # Start timer
duration_minutes = 200
response = client.post(
"/trip/new",
json={
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
"nature": {"type": "nature", "score": 0},
"shopping": {"type": "shopping", "score": 5},
"max_time_minute": duration_minutes,
"detour_tolerance_minute": 0},
"start": [48.85468881798671, 2.3423925755998374]
}
)
result = response.json()
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
# Get computation time
comp_time = time.time() - start_time
# Add details to report
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
# for elem in landmarks :
# print(elem)
# checks :
assert response.status_code == 200 # check for successful planning
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
def test_new_york(client, request) : # pylint: disable=redefined-outer-name
"""
Test n°7 : Custom test in New York to ensure proper decision making in crowded area.
Args:
client:
request:
"""
start_time = time.time() # Start timer
duration_minutes = 600
response = client.post(
"/trip/new",
json={
"preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
"nature": {"type": "nature", "score": 5},
"shopping": {"type": "shopping", "score": 5},
"max_time_minute": duration_minutes,
"detour_tolerance_minute": 0},
"start": [40.72592726802, -73.9920434795]
}
)
result = response.json()
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
# Get computation time
comp_time = time.time() - start_time
# Add details to report
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
# for elem in landmarks :
# print(elem)
# checks :
assert response.status_code == 200 # check for successful planning
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"
def test_shopping(client, request) : # pylint: disable=redefined-outer-name
"""
Test n°8 : Custom test in Lyon centre to ensure shopping clusters are found.
Args:
client:
request:
"""
start_time = time.time() # Start timer
duration_minutes = 240
response = client.post(
"/trip/new",
json={
"preferences": {"sightseeing": {"type": "sightseeing", "score": 0},
"nature": {"type": "nature", "score": 0},
"shopping": {"type": "shopping", "score": 5},
"max_time_minute": duration_minutes,
"detour_tolerance_minute": 0},
"start": [45.7576485, 4.8330241]
}
)
result = response.json()
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
# Get computation time
comp_time = time.time() - start_time
# Add details to report
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
# for elem in landmarks :
# print(elem)
# checks :
assert response.status_code == 200 # check for successful planning
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
assert duration_minutes*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {duration_minutes}"
assert duration_minutes*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {duration_minutes}"

View File

@@ -0,0 +1,46 @@
"""Collection of tests to ensure correct implementation and track progress of the get_landmarks_nearby feature. """
from fastapi.testclient import TestClient
import pytest
from ..main import app
@pytest.fixture(scope="module")
def client():
"""Client used to call the app."""
return TestClient(app)
@pytest.mark.parametrize(
"location,status_code",
[
([45.7576485, 4.8330241], 200), # Lyon, France
([41.4020572, 2.1818985], 200), # Barcelona, Spain
([59.3293, 18.0686], 200), # Stockholm, Sweden
([43.6532, -79.3832], 200), # Toronto, Canada
([38.7223, -9.1393], 200), # Lisbon, Portugal
([6.5244, 3.3792], 200), # Lagos, Nigeria
([17.3850, 78.4867], 200), # Hyderabad, India
([30.0444, 31.2357], 200), # Cairo, Egypt
([50.8503, 4.3517], 200), # Brussels, Belgium
([35.2271, -80.8431], 200), # Charlotte, USA
([10.4806, -66.9036], 200), # Caracas, Venezuela
([9.51074, -13.71118], 200), # Conakry, Guinea
]
)
def test_nearby(client, location, status_code): # pylint: disable=redefined-outer-name
"""
Test n°1 : Verify handling of invalid input.
Args:
client:
request:
"""
response = client.post(f"/get-nearby/landmarks/{location[0]}/{location[1]}")
suggestions = response.json()
# checks :
assert response.status_code == status_code # check for successful planning
assert isinstance(suggestions, list) # check that the return type is a list
assert len(suggestions) > 0

View File

@@ -30,7 +30,7 @@ def test_invalid_input(client, location, radius, status_code): # pylint: disa
request: request:
""" """
response = client.post( response = client.post(
"/toilets/new", "/get/toilets",
params={ params={
"location": location, "location": location,
"radius": radius "radius": radius
@@ -58,7 +58,7 @@ def test_no_toilets(client, location, status_code): # pylint: disable=redefin
request: request:
""" """
response = client.post( response = client.post(
"/toilets/new", "/get/toilets",
params={ params={
"location": location "location": location
} }
@@ -87,7 +87,7 @@ def test_toilets(client, location, status_code): # pylint: disable=redefined-
request: request:
""" """
response = client.post( response = client.post(
"/toilets/new", "/get/toilets",
params={ params={
"location": location, "location": location,
"radius" : 600 "radius" : 600

View File

@@ -0,0 +1,81 @@
"""Collection of tests to ensure correct implementation and track progress."""
import time
from fastapi.testclient import TestClient
import pytest
from .test_utils import load_trip_landmarks, log_trip_details
from ..structs.preferences import Preferences, Preference
from ..main import app
@pytest.fixture(scope="module")
def client():
"""Client used to call the app."""
return TestClient(app)
@pytest.mark.parametrize(
"sightseeing, shopping, nature, max_time_minute, start_coords, end_coords",
[
# Edge cases
(0, 0, 5, 240, [45.7576485, 4.8330241], None), # Lyon, Bellecour - test shopping only
# Realistic
(5, 0, 0, 20, [48.0845881, 7.2804050], None), # Turckheim
(5, 5, 5, 120, [45.7576485, 4.8330241], None), # Lyon, Bellecour
(5, 2, 5, 240, [50.9423526, 6.9577780], None), # Cologne, centre
(3, 5, 0, 180, [48.5846589226, 7.74078715721], None), # Strasbourg, centre
(2, 4, 5, 180, [47.377884227, 8.5395114066], None), # Zurich, centre
(5, 0, 5, 200, [48.85468881798671, 2.3423925755998374], None), # Paris, centre
(5, 5, 5, 600, [40.72592726802, -73.9920434795], None), # New York, Lower Manhattan
]
)
def test_trip(client, request, sightseeing, shopping, nature, max_time_minute, start_coords, end_coords):
start_time = time.time() # Start timer
prefs = Preferences(
sightseeing=Preference(type='sightseeing', score=sightseeing),
shopping=Preference(type='shopping', score=shopping),
nature=Preference(type='nature', score=nature),
max_time_minute=max_time_minute,
detour_tolerance_minute=0,
)
start = start_coords
end = end_coords
# Step 1: request the list of landmarks in the vicinty of the starting point
response = client.post(
"/get/landmarks",
json={
"preferences": prefs.model_dump(),
"start": start_coords,
"end": end_coords,
}
)
landmarks = response.json()
# Step 2: Feed the landmarks to the optimizer to compute the trip
response = client.post(
"/optimize/trip",
json={
"preferences": prefs.model_dump(),
"landmarks": landmarks,
"start": start,
"end": end,
}
)
result = response.json()
landmarks = load_trip_landmarks(client, result['first_landmark_uuid'])
# Get computation time
comp_time = time.time() - start_time
# Add details to report
log_trip_details(request, landmarks, result['total_time'], prefs.max_time_minute)
# checks :
assert response.status_code == 200 # check for successful planning
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
assert prefs.max_time_minute*0.8 < result['total_time'], f"Trip too short: {result['total_time']} instead of {prefs.max_time_minute}"
assert prefs.max_time_minute*1.2 > result['total_time'], f"Trip too long: {result['total_time']} instead of {prefs.max_time_minute}"

View File

@@ -1,9 +1,12 @@
"""Helper methods for testing.""" """Helper methods for testing."""
import time
import logging import logging
from functools import wraps
from fastapi import HTTPException from fastapi import HTTPException
from ..structs.landmark import Landmark
from ..cache import client as cache_client from ..cache import client as cache_client
from ..structs.landmark import Landmark
from ..structs.preferences import Preferences, Preference
def landmarks_to_osmid(landmarks: list[Landmark]) -> list[int] : def landmarks_to_osmid(landmarks: list[Landmark]) -> list[int] :
@@ -91,3 +94,34 @@ def log_trip_details(request, landmarks: list[Landmark], duration: int, target_d
request.node.trip_details = trip_string request.node.trip_details = trip_string
request.node.trip_duration = str(duration) # result['total_time'] request.node.trip_duration = str(duration) # result['total_time']
request.node.target_duration = str(target_duration) request.node.target_duration = str(target_duration)
def trip_params(
sightseeing: int,
shopping: int,
nature: int,
max_time_minute: int,
start_coords: tuple[float, float] = None,
end_coords: tuple[float, float] = None,
):
def decorator(test_func):
@wraps(test_func)
def wrapper(client, request):
prefs = Preferences(
sightseeing=Preference(type='sightseeing', score=sightseeing),
shopping=Preference(type='shopping', score=shopping),
nature=Preference(type='nature', score=nature),
max_time_minute=max_time_minute,
detour_tolerance_minute=0,
)
start = start_coords
end = end_coords
# Inject into test function
return test_func(client, request, prefs, start, end)
return wrapper
return decorator

View File

@@ -70,6 +70,8 @@ class ToiletsManager:
toilets_list = self.to_toilets(result) toilets_list = self.to_toilets(result)
self.logger.debug(f'Found {len(toilets_list)} toilets around {self.location}')
return toilets_list return toilets_list

View File

@@ -1,16 +1,20 @@
"""Defines the endpoint for fetching toilet locations.""" """API entry point for fetching toilet locations."""
from fastapi import HTTPException, APIRouter, Query from fastapi import HTTPException, APIRouter, Query
from ..structs.toilets import Toilets
from .toilets_manager import ToiletsManager from .toilets_manager import ToiletsManager
from ..structs.toilets import Toilets
# Define the API router # Initialize the API router
router = APIRouter() router = APIRouter()
@router.post("/toilets/new") @router.post("/get/toilets")
def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -> list[Toilets] : def get_toilets(
location: tuple[float, float] = Query(...),
radius: int = 500
) -> list[Toilets] :
""" """
Endpoint to find toilets within a specified radius from a given location. Endpoint to find toilets within a specified radius from a given location.
@@ -35,4 +39,5 @@ def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -
except KeyError as exc: except KeyError as exc:
raise HTTPException(status_code=404, detail="No toilets found") from exc raise HTTPException(status_code=404, detail="No toilets found") from exc
return toilets_list return toilets_list

View File

@@ -0,0 +1,123 @@
"""Add more information about the landmarks by writing a short description and keywords. """
def description_and_keywords(tags: dict):
"""
Generates a description and a set of keywords for a given landmark based on its tags.
Params:
tags (dict): A dictionary containing metadata about the landmark, including its name,
importance, height, date of construction, and visitor information.
Returns:
description (str): A string description of the landmark.
keywords (dict): A dictionary of keywords with fields such as 'importance', 'height',
'place_type', and 'date'.
"""
# Extract relevant fields
name = tags.get('name')
importance = tags.get('importance', None)
n_visitors = tags.get('tourism:visitors', None)
height = tags.get('height')
place_type = get_place_type(tags)
date = get_date(tags)
if place_type is None :
return None, None
# Start the description.
if importance is None :
if len(tags.keys()) < 5 :
return None, None
if len(tags.keys()) < 10 :
description = f"{name} is a well known {place_type}."
elif len(tags.keys()) < 17 :
importance = 'national'
description = f"{name} is a {place_type} of national importance."
else :
importance = 'international'
description = f"{name} is an internationally famous {place_type}."
else :
description = f"{name} is a {place_type} of {importance} importance."
if height is not None and date is not None :
description += f" This {place_type} was constructed in {date} and is ca. {height} meters high."
elif height is not None :
description += f" This {place_type} stands ca. {height} meters tall."
elif date is not None:
description += f" It was constructed in {date}."
# Format the visitor number
if n_visitors is not None :
n_visitors = int(n_visitors)
if n_visitors < 1000000 :
description += f" It welcomes {int(n_visitors/1000)} thousand visitors every year."
else :
description += f" It welcomes {round(n_visitors/1000000, 1)} million visitors every year."
# Set the keywords.
keywords = {"importance": importance,
"height": height,
"place_type": place_type,
"date": date}
return description, keywords
def get_place_type(tags):
"""
Determines the type of the place based on available tags such as 'amenity', 'building',
'historic', and 'leisure'. The priority order is: 'historic' > 'building' (if not generic) >
'amenity' > 'leisure'.
Params:
tags (dict): A dictionary containing metadata about the place.
Returns:
place_type (str): The determined type of the place, or None if no relevant type is found.
"""
amenity = tags.get('amenity', None)
building = tags.get('building', None)
historic = tags.get('historic', None)
leisure = tags.get('leisure')
if historic and historic != "yes":
return historic
if building and building not in ["yes", "civic", "government", "apartments", "residential", "commericial", "industrial", "retail", "religious", "public", "service"]:
return building
if amenity:
return amenity
if leisure:
return leisure
return None
def get_date(tags):
"""
Extracts the most relevant date from the available tags, prioritizing 'construction_date',
'start_date', 'year_of_construction', and 'opening_date' in that order.
Params:
tags (dict): A dictionary containing metadata about the place.
Returns:
date (str): The most relevant date found, or None if no date is available.
"""
construction_date = tags.get('construction_date', None)
opening_date = tags.get('opening_date', None)
start_date = tags.get('start_date', None)
year_of_construction = tags.get('year_of_construction', None)
# Prioritize based on availability
if construction_date:
return construction_date
if start_date:
return start_date
if year_of_construction:
return year_of_construction
if opening_date:
return opening_date
return None

View File

@@ -1,17 +0,0 @@
"""Helper function to return only the major landmarks from a large list."""
from ..structs.landmark import Landmark
def take_most_important(landmarks: list[Landmark], n_important) -> list[Landmark]:
"""
Given a list of landmarks, return the n_important most important landmarks
Args:
landmarks: list[Landmark] - list of landmarks
n_important: int - number of most important landmarks to return
Returns:
list[Landmark] - list of the n_important most important landmarks
"""
# Sort landmarks by attractiveness (descending)
sorted_landmarks = sorted(landmarks, key=lambda x: x.attractiveness, reverse=True)
return sorted_landmarks[:n_important]

1330
backend/uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
default.nix Normal file
View File

@@ -0,0 +1,17 @@
{ pkgs ? import <nixpkgs> { config.android_sdk.accept_license = true; config.allowUnfree = true; } }:
pkgs.mkShell {
buildInputs = [
pkgs.flutter
#pkgs.android-tools # for adb
#pkgs.openjdk # required for Android builds
];
# Set up Android SDK paths if needed
shellHook = ''
export ANDROID_SDK_ROOT=${pkgs.androidsdk}/libexec/android-sdk
export PATH=$PATH:${pkgs.androidsdk}/libexec/android-sdk/platform-tools
echo "Flutter dev environment ready. 'adb' and 'flutter' are available."
'';
}

View File

@@ -1,59 +0,0 @@
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Set up ruby env
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.1
bundler-cache: true
- name: Setup java for android build
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'zulu'
- name: Setup android SDK
uses: android-actions/setup-android@v3
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.22.0
cache: true
- name: Infer version number from git tag
id: version
env:
REF_NAME: ${{ github.ref_name }}
run:
# remove the 'v' prefix from the tag name
echo "BUILD_NAME=${REF_NAME//v}" >> $GITHUB_ENV
- name: Put selected secrets into files
run: |
echo "${{ secrets.ANDROID_SECRET_PROPERTIES_BASE64 }}" | base64 -d > secrets.properties
echo "${{ secrets.ANDROID_GOOGLE_PLAY_JSON_BASE64 }}" | base64 -d > google-key.json
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > release.keystore
working-directory: android
- name: Install fastlane
run: bundle install
working-directory: android
- name: Run fastlane lane
run: bundle exec fastlane deploy_release
working-directory: android
env:
BUILD_NUMBER: ${{ github.run_number }}
# BUILD_NAME is implicitly available
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}

View File

@@ -1,64 +0,0 @@
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: macos-latest
env:
# $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
BUNDLE_GEMFILE: ${{ github.workspace }}/ios/Gemfile
steps:
- uses: actions/checkout@v4
- name: Set up ruby env
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.22.0
cache: true
- name: Infer version number from git tag
id: version
env:
REF_NAME: ${{ github.ref_name }}
run:
# remove the 'v' prefix from the tag name
echo "BUILD_NAME=${REF_NAME//v}" >> $GITHUB_ENV
- name: Setup SSH key for match git repo
# and mark the host as known
run: |
echo $MATCH_REPO_SSH_KEY | base64 --decode > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -p 2222 git.kluster.moll.re > ~/.ssh/known_hosts
env:
MATCH_REPO_SSH_KEY: ${{ secrets.IOS_MATCH_REPO_SSH_KEY_BASE64 }}
- name: Install dependencies and clean up
run: |
flutter pub get
bundle exec pod install
flutter clean
bundle exec pod cache clean --all
working-directory: ios
- name: Run fastlane lane
run: bundle exec fastlane deploy_release --verbose
working-directory: ios
env:
BUILD_NUMBER: ${{ github.run_number }}
# BUILD_NAME is implicitly available
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
IOS_ASC_KEY_ID: ${{ secrets.IOS_ASC_KEY_ID }}
IOS_ASC_ISSUER_ID: ${{ secrets.IOS_ASC_ISSUER_ID }}
IOS_ASC_KEY: ${{ secrets.IOS_ASC_KEY }}
MATCH_PASSWORD: ${{ secrets.IOS_MATCH_PASSWORD }}
IOS_GOOGLE_MAPS_API_KEY: ${{ secrets.IOS_GOOGLE_MAPS_API_KEY }}

View File

@@ -17,7 +17,7 @@ platform :android do
) )
upload_to_play_store( upload_to_play_store(
track: 'alpha', track: 'beta',
# upload aab files intstead # upload aab files intstead
skip_upload_apk: true, skip_upload_apk: true,
skip_upload_changelogs: true, skip_upload_changelogs: true,

View File

@@ -1,7 +0,0 @@
AnyWay is an application that helps you plan truly unique city trips. When planning a new trip, you can specify <your> preferences and constraints and anyway generates a personalized itinerary just for you.
Anyway follows these core principles:
- **Personalization**: Trips should be match your interests - not just the most popular destinations.
- **Efficiency**: Don't just walk in circles! Anyway creates the most efficient route for you.
- **Flexibility**: Vacations are the time to be spontaneous. Anyway lets you update your plans on the go.
- **Discoverability**: Tourism means exploration. Anyway encourages you to take detours and make spontaneous decisions.

View File

@@ -0,0 +1,7 @@
AnyWay is an application that helps you plan truly unique city trips. When planning a new trip, you can specify your preferences and constraints and anyway generates a personalized itinerary just for you.
Anyway follows these core principles:
- Personalization: Trips should be match your interests - not just the most popular destinations.
- Efficiency: Don't just walk in circles! Anyway creates the most efficient route for you.
- Flexibility: Vacations are the time to be spontaneous. Anyway lets you update your plans on the go.
- Discoverability: Tourism means exploration. Anyway encourages you to take detours and make spontaneous decisions.

View File

@@ -1,4 +1,4 @@
app_identifier("info.anydev.testing") # The bundle identifier of your app app_identifier("info.anydev.anyway") # The bundle identifier of your app
apple_id("me@moll.re") # Your Apple Developer Portal username apple_id("me@moll.re") # Your Apple Developer Portal username
itc_team_id("127439860") # App Store Connect Team ID itc_team_id("127439860") # App Store Connect Team ID

View File

@@ -28,17 +28,6 @@ platform :ios do
readonly: true, readonly: true,
) )
# replace secrets by real values, the stupid way
sh(
"sed",
"-i",
"",
"s/IOS_GOOGLE_MAPS_API_KEY/#{ENV["IOS_GOOGLE_MAPS_API_KEY"]}/g",
"../Runner/AppDelegate.swift"
)
sh( sh(
"flutter", "flutter",
"build", "build",
@@ -74,15 +63,6 @@ platform :ios do
readonly: true, readonly: true,
) )
# replace secrets by real values, the stupid way
sh(
"sed",
"-i",
"",
"s/IOS_GOOGLE_MAPS_API_KEY/#{ENV["IOS_GOOGLE_MAPS_API_KEY"]}/g",
"../Runner/AppDelegate.swift"
)
sh( sh(
"flutter", "flutter",
"build", "build",
@@ -101,9 +81,9 @@ platform :ios do
upload_to_app_store( upload_to_app_store(
overwrite_screenshots: true, overwrite_screenshots: true,
metadata_path: "fastlane/metadata", metadata_path: "fastlane/metadata",
screenshot_path: "fastlane/screenshots", screenshots_path: "fastlane/screenshots",
precheck_include_in_app_purchases: false, precheck_include_in_app_purchases: false,
force: true, # Skip HTMl report verification
submit_for_review: true, submit_for_review: true,
automatic_release: true, automatic_release: true,
# automatically release the app after review # automatically release the app after review

View File

@@ -1 +1 @@
2025 anydev

View File

@@ -1,7 +1,7 @@
AnyWay is an application that helps you plan truly unique city trips. When planning a new trip, you can specify <our> preferences and constraints and anyway generates a personalized itinerary just for you. AnyWay is an application that helps you plan truly unique city trips. When planning a new trip, you can specify your preferences and constraints and anyway generates a personalized itinerary just for you.
Anyway follows these core principles: Anyway follows these core principles:
- **Personalization**: Trips should be match your interests - not just the most popular destinations. - Personalization: Trips should be match your interests - not just the most popular destinations.
- **Efficiency**: Don't just walk in circles! Anyway creates the most efficient route for you. - Efficiency: Don't just walk in circles! Anyway creates the most efficient route for you.
- **Flexibility**: Vacations are the time to be spontaneous. Anyway lets you update your plans on the go. - Flexibility: Vacations are the time to be spontaneous. Anyway lets you update your plans on the go.
- **Discoverability**: Tourism means exploration. Anyway encourages you to take detours and make spontaneous decisions. - Discoverability: Tourism means exploration. Anyway encourages you to take detours and make spontaneous decisions.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -1 +1 @@
AnyWay Any.Way

View File

@@ -1 +1 @@
TRAVEL

View File

@@ -1 +1 @@
anydev.anyway@gmail.com

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

Before

Width:  |  Height:  |  Size: 626 KiB

After

Width:  |  Height:  |  Size: 626 KiB

View File

Before

Width:  |  Height:  |  Size: 758 KiB

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

Before

Width:  |  Height:  |  Size: 574 KiB

After

Width:  |  Height:  |  Size: 574 KiB

View File

Before

Width:  |  Height:  |  Size: 800 KiB

After

Width:  |  Height:  |  Size: 800 KiB

1091
report.html Normal file

File diff suppressed because it is too large Load Diff