Skip to content

Add plugins infrastructure and factor out the enhancer and GTFS generator #34

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 50 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
ad0689e
added pyproject.toml
Jan 25, 2024
7ac3ffe
plugin discovery
Jan 25, 2024
57c3f74
metrics secrets
Jan 25, 2024
8129e82
install plugins in Dockerfile
Feb 1, 2024
338ecd7
Moved metric user and password configuration to metrics plugin
Feb 29, 2024
7a5a89f
Added amarillo-metrics to requirements.txt
Mar 22, 2024
d539b04
moved static files to amarillo/static
Feb 6, 2024
e1a35fa
copy static files to working directory
Feb 6, 2024
421d7c0
moved gtfs generation and enhancer.py to enhancer plugin
Jan 30, 2024
d8f7b0b
moved 'configure_enhancer_services' to enhancer plugin
Jan 30, 2024
25c76ae
moved routing.py, trips.py and stops.py to enhancer plugin
Feb 6, 2024
1e6cea8
removed enhancer from Dockerfile
Feb 6, 2024
cc493c3
moved tests to enhancer
Feb 6, 2024
8ff0d1f
moved gtfs services to enhancer
Feb 6, 2024
48f6fd9
moved gtfs model to enhancer
Feb 8, 2024
b702756
moved region/gtfs endpoint to export plugin
Feb 9, 2024
3063e1a
carpool GRFS attributes
Feb 9, 2024
ea7a2d6
__init__.py EOL, extra secrets clarification
May 13, 2024
456a190
CRUD metrics
Feb 19, 2024
76ffd33
Added route_color and route_text_color to Carpool model
Feb 21, 2024
8995309
Allow plugins to add extra configuration values
Aug 1, 2024
4af9cf3
Moved stop importer tests to amarillo-stops
Aug 1, 2024
7db9706
Call enhancer in background task
May 17, 2024
6b42501
Carpool event hooks
Jul 18, 2024
f85291f
Build amarillo-base and derived image
Jun 24, 2024
b1ec8e1
Publish to PyPI, removed conditionals in Jenkinsfile
Aug 5, 2024
fac12e4
Removed credential requirement from standard.Dockerfile
Aug 6, 2024
ea4af99
Changed Jenkinsfile distribution version to 2.0.0
Aug 6, 2024
54e25b3
Run as amarillo user
Aug 6, 2024
04f8fae
Disable 'notify CD script' stage
Aug 6, 2024
5c9904b
Fixed error.log permissions
Aug 6, 2024
f58a739
Set version to 2.0.0
Aug 6, 2024
912bd56
Use /data for region and agency configuration
May 17, 2024
e31c097
fix: call _extract_stop on class
hbruch May 6, 2024
e2253a8
Enhance synced trips
Aug 14, 2024
6235798
fix: encode request body as utf-8
Aug 14, 2024
b932144
Add Mitanand to server list
Oct 30, 2024
af2f7c3
Log user info why API key is used
Nov 21, 2024
b575b64
Build mitanand image
Nov 25, 2024
b739eb7
Fix mitanand jenkins stage name
Nov 25, 2024
818999d
Plugin development readme
Dec 13, 2024
d783f8b
Update logging.conf to save errors to file
Dec 13, 2024
71942e8
Added make_sample_data script
Dec 19, 2024
835db9a
Revert "Added make_sample_data script" (Adding to amarillo-compose in…
Jan 22, 2025
63edfc2
Enhance missing carpools, ConnectionError message
Jan 22, 2025
475b224
Enhance updated carpools on startup
Jan 28, 2025
8f6d813
Fixed carpool example coordinates
Mar 5, 2025
f887fb2
Save failed carpool enhancements to data/failed
Mar 5, 2025
7956131
Removed unused dependencies
Mar 5, 2025
bbf9fd7
Update README.md
Mar 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ setup.ipynb

*~
/.idea/
/.vscode/
secrets
gtfs/*.zip
gtfs/*.pbf
Expand All @@ -142,4 +143,11 @@ data/failed/
data/trash/
data/gtfs/
data/tmp

data/**

#these files are under app/static but they get copied to the outside directory on startup
logging.conf
config
static/**
templates/**
conf/**
24 changes: 6 additions & 18 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,6 @@ LABEL maintainer="[email protected]"

WORKDIR /app

RUN \
apt update \
&& apt install -y \
# GDAL headers are required for fiona, which is required for geopandas.
# Also gcc is used to compile C++ code.
libgdal-dev g++ \
# libspatialindex is required for rtree.
libspatialindex-dev \
# Remove package index obtained by `apt update`.
&& rm -rf /var/lib/apt/lists/*

ENV ADMIN_TOKEN=''
ENV RIDE2GO_TOKEN=''

Expand All @@ -27,13 +16,12 @@ RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
ENV MODULE_NAME=amarillo.main

COPY ./amarillo /app/amarillo
COPY enhancer.py /app
COPY prestart.sh /app
COPY ./static /app/static
COPY ./templates /app/templates
COPY config /app
COPY logging.conf /app
COPY ./conf /app/conf
COPY ./amarillo/plugins /app/amarillo/plugins
COPY ./amarillo/static/static /app/static
COPY ./amarillo/static/templates /app/templates
COPY ./amarillo/static/config /app
COPY ./amarillo/static/logging.conf /app
COPY ./amarillo/static/data /app/data

# This image inherits uvicorn-gunicorn's CMD. If you'd like to start uvicorn, use this instead
# CMD ["uvicorn", "amarillo.main:app", "--host", "0.0.0.0", "--port", "8000"]
103 changes: 76 additions & 27 deletions Jenkinsfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
pipeline {
agent any
agent { label 'builtin' }
environment {
GITEA_CREDS = credentials('AMARILLO-JENKINS-GITEA-USER')
PYPI_CREDS = credentials('AMARILLO-JENKINS-PYPI-USER')
TWINE_REPO_URL = "https://git.gerhardt.io/api/packages/amarillo/pypi"
PLUGINS_REPO_URL = "git.gerhardt.io/api/packages/amarillo/pypi/simple"
DOCKER_REGISTRY_URL = 'https://git.gerhardt.io'
PLUGINS_REPO_URL = "https://git.gerhardt.io/api/packages/amarillo/pypi/simple"
PYPI_REPO_URL = "https://pypi.org/simple"
DOCKER_REGISTRY = 'git.gerhardt.io'
DERIVED_DOCKERFILE = 'standard.Dockerfile'
MITANAND_DOCKERFILE = 'mitanand.Dockerfile'
OWNER = 'amarillo'
BASE_IMAGE_NAME = 'amarillo-base'
IMAGE_NAME = 'amarillo'
AMARILLO_DISTRIBUTION = '0.2'
TAG = "${AMARILLO_DISTRIBUTION}.${BUILD_NUMBER}"
PLUGINS = 'amarillo-metrics amarillo-enhancer amarillo-grfs-export'
DEPLOY_WEBHOOK_URL = 'http://amarillo.mfdz.de:8888/mitanand'
MITANAND_IMAGE_NAME = 'amarillo-mitanand'
AMARILLO_DISTRIBUTION = '2.0.0'
TAG = "${AMARILLO_DISTRIBUTION}${env.BRANCH_NAME == 'main' ? '' : '-' + env.BRANCH_NAME}-${BUILD_NUMBER}"
DEPLOY_WEBHOOK_URL = "http://amarillo.mfdz.de:8888/dev"
DEPLOY_SECRET = credentials('AMARILLO-JENKINS-DEPLOY-SECRET')
}
stages {
Expand Down Expand Up @@ -44,52 +48,97 @@ pipeline {
}
stage('Publish package to PyPI') {
when {
branch 'release'
branch 'main'
}
steps {
sh 'python3 -m twine upload --verbose --username $PYPI_CREDS_USR --password $PYPI_CREDS_PSW ./dist/*'
}
}
stage('Build Mitanand docker image') {
when {
branch 'mitanand'
}
stage('Build base docker image') {
steps {
echo 'Building image'
script {
docker.build("${OWNER}/${IMAGE_NAME}:${TAG}",
//--no-cache to make sure plugins are updated
"--no-cache --build-arg='PACKAGE_REGISTRY_URL=${PLUGINS_REPO_URL}' --build-arg='PLUGINS=${PLUGINS}' --secret id=AMARILLO_REGISTRY_CREDENTIALS,env=GITEA_CREDS .")
docker.build("${OWNER}/${BASE_IMAGE_NAME}:${TAG}")
}
}
}
stage('Push image to container registry') {
when {
branch 'mitanand'
stage('Push base image to container registry') {
steps {
echo 'Pushing image to registry'
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", 'AMARILLO-JENKINS-GITEA-USER'){
def image = docker.image("${OWNER}/${BASE_IMAGE_NAME}:${TAG}")
image.push()
image.push('latest')
}
}
}
}
stage('Build derived docker image') {
steps {
echo 'Building image'
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", 'AMARILLO-JENKINS-GITEA-USER'){
docker.build("${OWNER}/${IMAGE_NAME}:${TAG}",
//--no-cache to make sure plugins are updated
"-f ${DERIVED_DOCKERFILE} --no-cache --build-arg='PACKAGE_REGISTRY_URL=${env.BRANCH_NAME == 'main' ? env.PYPI_REPO_URL : env.PLUGINS_REPO_URL}' --build-arg='DOCKER_REGISTRY=${DOCKER_REGISTRY}' --secret id=AMARILLO_REGISTRY_CREDENTIALS,env=GITEA_CREDS .")
}

}
}
}
stage('Push derived image to container registry') {
steps {
echo 'Pushing image to registry'
script {
docker.withRegistry(DOCKER_REGISTRY_URL, 'AMARILLO-JENKINS-GITEA-USER'){
docker.withRegistry("https://${DOCKER_REGISTRY}", 'AMARILLO-JENKINS-GITEA-USER'){
def image = docker.image("${OWNER}/${IMAGE_NAME}:${TAG}")
image.push()
image.push('latest')
}
}
}
}
stage('Notify CD script') {
when {
branch 'mitanand'

stage('Build mitanand docker image') {
steps {
echo 'Building image'
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", 'AMARILLO-JENKINS-GITEA-USER'){
docker.build("${OWNER}/${MITANAND_IMAGE_NAME}:${TAG}",
//--no-cache to make sure plugins are updated
"-f ${MITANAND_DOCKERFILE} --no-cache --build-arg='PACKAGE_REGISTRY_URL=${env.BRANCH_NAME == 'main' ? env.PYPI_REPO_URL : env.PLUGINS_REPO_URL}' --build-arg='DOCKER_REGISTRY=${DOCKER_REGISTRY}' --secret id=AMARILLO_REGISTRY_CREDENTIALS,env=GITEA_CREDS .")
}

}
}
}
stage('Push mitanand image to container registry') {
steps {
echo 'Triggering deploy webhook'
echo 'Pushing image to registry'
script {
def response = httpRequest contentType: 'APPLICATION_JSON',
httpMode: 'POST', requestBody: '{}', authentication: 'AMARILLO-JENKINS-DEPLOY-SECRET',
url: "${DEPLOY_WEBHOOK_URL}"
docker.withRegistry("https://${DOCKER_REGISTRY}", 'AMARILLO-JENKINS-GITEA-USER'){
def image = docker.image("${OWNER}/${MITANAND_IMAGE_NAME}:${TAG}")
image.push()
image.push('latest')
}
}
}
}
// stage('Notify CD script') {
// when {
// not {
// branch 'main'
// }
// }
// steps {
// echo 'Triggering deploy webhook'
// script {
// def response = httpRequest contentType: 'APPLICATION_JSON',
// httpMode: 'POST', requestBody: '{}', authentication: 'AMARILLO-JENKINS-DEPLOY-SECRET',
// url: "${DEPLOY_WEBHOOK_URL}"
// }
// }
// }
}
}
}
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
recursive-include amarillo/static/ *
recursive-include amarillo/tests/ *
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ An Amarillo is a [yellow-dressed person](https://www.dreamstime.com/sancti-spiri

## Setup

- Python 3.9.2 with pip
- Python 3.10 with pip
- python3-venv

Create a virtual environment `python3 -m venv venv`.
Create a virtual environment:
`python3 -m venv venv`.

Activate the environment:
`. venv/bin/activate`

Install the dependencies: `pip install -r requirements.txt`.

Activate the environment and install the dependencies `pip install -r requirements.txt`.

Run `uvicorn amarillo.main:app`.

Expand Down Expand Up @@ -54,13 +59,17 @@ $ protoc --proto_path=. --python_out=../services/gtfsrt gtfs-realtime.proto real
$ sed 's/import gtfs_realtime_pb2/import amarillo.services.gtfsrt.gtfs_realtime_pb2/g' ../services/gtfsrt/realtime_extension_pb2.py | sponge ../services/gtfsrt/realtime_extension_pb2.py
```

### Develop Amarillo and its plugins together

To the develop Amarillo and its plugins concurrently, clone this repo and all of the repos you would like to make changes to. Install the local version of the plugin(s) into the virtual environment, simply using `pip install /path/to/plugin`, or `pip install -e /path/to/plugin` if your environment supports editable installs.

## Testing

In the top directory, run `pytest amarillo/tests`.

## Docker

Based on [tiangolo/uvicorn-gunicorn:python3.9-slim](https://github.com/tiangolo/uvicorn-gunicorn-docker)
Based on [tiangolo/uvicorn-gunicorn:python3.10-slim](https://github.com/tiangolo/uvicorn-gunicorn-docker)

- build `docker build -t amarillo .`
- run `docker run --rm --name amarillo -p 8000:80 -e MAX_WORKERS="1" -e ADMIN_TOKEN=$ADMIN_TOKEN -e RIDE2GO_TOKEN=$RIDE2GO_TOKEN -e TZ=Europe/Berlin -v $(pwd)/data:/app/data amarillo`
45 changes: 2 additions & 43 deletions amarillo/configuration.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
# separate file so that it can be imported without initializing FastAPI
from amarillo.routers.carpool import enhance_missing_carpools
from amarillo.utils.container import container
import json
import logging
from glob import glob

from amarillo.models.Carpool import Agency, Carpool, Region
from amarillo.services import stops
from amarillo.services import trips
from amarillo.services.agencyconf import AgencyConfService, agency_conf_directory
from amarillo.services.carpools import CarpoolService
from amarillo.services.agencies import AgencyService
from amarillo.services.regions import RegionService

from amarillo.services.config import config

from amarillo.utils.utils import assert_folder_exists
import amarillo.services.gtfs_generator as gtfs_generator

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -48,42 +42,7 @@ def configure_services():
logger.info("Loaded %d regions", len(container['regions'].regions))

create_required_directories()


def configure_enhancer_services():
configure_services()

logger.info("Load stops...")
with open(config.stop_sources_file) as stop_sources_file:
stop_sources = json.load(stop_sources_file)
stop_store = stops.StopsStore(stop_sources)

stop_store.load_stop_sources()
container['stops_store'] = stop_store
container['trips_store'] = trips.TripStore(stop_store)
container['carpools'] = CarpoolService(container['trips_store'])

logger.info("Restore carpools...")

for agency_id in container['agencies'].agencies:
for carpool_file_name in glob(f'data/carpool/{agency_id}/*.json'):
try:
with open(carpool_file_name) as carpool_file:
carpool = Carpool(**(json.load(carpool_file)))
container['carpools'].put(carpool.agency, carpool.id, carpool)
except Exception as e:
logger.warning("Issue during restore of carpool %s: %s", carpool_file_name, repr(e))

# notify carpool about carpools in trash, as delete notifications must be sent
for carpool_file_name in glob(f'data/trash/{agency_id}/*.json'):
with open(carpool_file_name) as carpool_file:
carpool = Carpool(**(json.load(carpool_file)))
container['carpools'].delete(carpool.agency, carpool.id)

logger.info("Restored carpools: %s", container['carpools'].get_all_ids())
logger.info("Starting scheduler")
gtfs_generator.start_schedule()

enhance_missing_carpools()

def configure_admin_token():
if config.admin_token is None:
Expand Down
43 changes: 36 additions & 7 deletions amarillo/main.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import logging.config

from amarillo.configuration import configure_services, configure_admin_token
from amarillo.services.config import config

logging.config.fileConfig('logging.conf', disable_existing_loggers=False)
logger = logging.getLogger("main")

import importlib
import pkgutil
import uvicorn
import mimetypes
from starlette.staticfiles import StaticFiles

from amarillo.utils.utils import copy_static_files
#this has to run before app.configuration is imported, otherwise we get validation error for config because the config file is not copied yet
copy_static_files(["data", "static", "templates", "logging.conf", "config"])

import amarillo.plugins
from amarillo.services.config import config
from amarillo.configuration import configure_services, configure_admin_token
from amarillo.routers import carpool, agency, agencyconf, region
from fastapi import FastAPI

# https://pydantic-docs.helpmanual.io/usage/settings/
from amarillo.views import home

logging.config.fileConfig('logging.conf', disable_existing_loggers=False)
logger = logging.getLogger("main")

logger.info("Hello Amarillo!")

app = FastAPI(title="Amarillo - The Carpooling Intermediary",
Expand Down Expand Up @@ -65,6 +70,10 @@
"description": "Demo server by MFDZ",
"url": "https://amarillo.mfdz.de"
},
{
"description": "Mitanand Amarillo service",
"url": "https://mitanand.mfdz.de"
},
{
"description": "Dev server for development",
"url": "https://amarillo-dev.mfdz.de"
Expand All @@ -79,10 +88,30 @@
app.include_router(region.router)


def iter_namespace(ns_pkg):
# Source: https://packaging.python.org/guides/creating-and-discovering-plugins/
return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".")

def load_plugins():
discovered_plugins = {
name: importlib.import_module(name)
for finder, name, ispkg
in iter_namespace(amarillo.plugins)
}
logger.info(f"Discovered plugins: {list(discovered_plugins.keys())}")

for name, module in discovered_plugins.items():
if hasattr(module, "setup"):
logger.info(f"Running setup function for {name}")
module.setup(app)

else: logger.info(f"Did not find setup function for {name}")

def configure():
configure_admin_token()
configure_services()
configure_routing()
load_plugins()


def configure_routing():
Expand Down
Loading