Run as FastAPI application
This commit is contained in:
parent
17a6888583
commit
2df0b38f03
1
amarillo-enhancer/__init__.py
Normal file
1
amarillo-enhancer/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# from .enhancer import *
|
||||||
|
|
@ -8,9 +8,9 @@ from amarillo.models.Carpool import Carpool
|
||||||
from amarillo.plugins.enhancer.services import stops
|
from amarillo.plugins.enhancer.services import stops
|
||||||
from amarillo.plugins.enhancer.services import trips
|
from amarillo.plugins.enhancer.services import trips
|
||||||
from amarillo.plugins.enhancer.services.carpools import CarpoolService
|
from amarillo.plugins.enhancer.services.carpools import CarpoolService
|
||||||
from amarillo.plugins.enhancer.services import gtfs_generator
|
|
||||||
from amarillo.services.config import config
|
from amarillo.services.config import config
|
||||||
from amarillo.configuration import configure_services
|
from amarillo.configuration import configure_services
|
||||||
|
from .services.trips import TripTransformer
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -19,22 +19,29 @@ enhancer_configured = False
|
||||||
def configure_enhancer_services():
|
def configure_enhancer_services():
|
||||||
#Make sure configuration only happens once
|
#Make sure configuration only happens once
|
||||||
global enhancer_configured
|
global enhancer_configured
|
||||||
|
global transformer
|
||||||
if enhancer_configured:
|
if enhancer_configured:
|
||||||
logger.info("Enhancer is already configured")
|
logger.info("Enhancer is already configured")
|
||||||
return
|
return
|
||||||
|
|
||||||
configure_services()
|
configure_services()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
logger.info("Load stops...")
|
logger.info("Load stops...")
|
||||||
with open(config.stop_sources_file) as stop_sources_file:
|
with open('data/stop_sources.json') as stop_sources_file:
|
||||||
stop_sources = json.load(stop_sources_file)
|
stop_sources = json.load(stop_sources_file)
|
||||||
stop_store = stops.StopsStore(stop_sources)
|
stop_store = stops.StopsStore(stop_sources)
|
||||||
|
|
||||||
stop_store.load_stop_sources()
|
stop_store.load_stop_sources()
|
||||||
|
# TODO: do we need container?
|
||||||
container['stops_store'] = stop_store
|
container['stops_store'] = stop_store
|
||||||
container['trips_store'] = trips.TripStore(stop_store)
|
container['trips_store'] = trips.TripStore(stop_store)
|
||||||
container['carpools'] = CarpoolService(container['trips_store'])
|
container['carpools'] = CarpoolService(container['trips_store'])
|
||||||
|
|
||||||
|
transformer = TripTransformer(stop_store)
|
||||||
|
|
||||||
logger.info("Restore carpools...")
|
logger.info("Restore carpools...")
|
||||||
|
|
||||||
for agency_id in container['agencies'].agencies:
|
for agency_id in container['agencies'].agencies:
|
||||||
94
amarillo-enhancer/enhancer.py
Normal file
94
amarillo-enhancer/enhancer.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
from .models.Carpool import Carpool
|
||||||
|
from .services.trips import TripTransformer
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
from fastapi import FastAPI, status, Body
|
||||||
|
from .configuration import configure_enhancer_services
|
||||||
|
from amarillo.utils.container import container
|
||||||
|
|
||||||
|
logging.config.fileConfig('logging.conf', disable_existing_loggers=False)
|
||||||
|
logger = logging.getLogger("enhancer")
|
||||||
|
|
||||||
|
#TODO: clean up metadata
|
||||||
|
app = FastAPI(title="Amarillo Enhancer",
|
||||||
|
description="This service allows carpool agencies to publish "
|
||||||
|
"their trip offers, so routing services may suggest "
|
||||||
|
"them as trip options. For carpool offers, only the "
|
||||||
|
"minimum required information (origin/destination, "
|
||||||
|
"optionally intermediate stops, departure time and a "
|
||||||
|
"deep link for booking/contacting the driver) needs to "
|
||||||
|
"be published, booking/contact exchange is to be "
|
||||||
|
"handled by the publishing agency.",
|
||||||
|
version="0.0.1",
|
||||||
|
# TODO 404
|
||||||
|
terms_of_service="http://mfdz.de/carpool-hub-terms/",
|
||||||
|
contact={
|
||||||
|
# "name": "unused",
|
||||||
|
# "url": "http://unused",
|
||||||
|
"email": "info@mfdz.de",
|
||||||
|
},
|
||||||
|
license_info={
|
||||||
|
"name": "AGPL-3.0 License",
|
||||||
|
"url": "https://www.gnu.org/licenses/agpl-3.0.de.html",
|
||||||
|
},
|
||||||
|
openapi_tags=[
|
||||||
|
{
|
||||||
|
"name": "carpool",
|
||||||
|
# "description": "Find out more about Amarillo - the carpooling intermediary",
|
||||||
|
"externalDocs": {
|
||||||
|
"description": "Find out more about Amarillo - the carpooling intermediary",
|
||||||
|
"url": "https://github.com/mfdz/amarillo",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
servers=[
|
||||||
|
{
|
||||||
|
"description": "MobiData BW Amarillo service",
|
||||||
|
"url": "https://amarillo.mobidata-bw.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "DABB bbnavi Amarillo service",
|
||||||
|
"url": "https://amarillo.bbnavi.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Demo server by MFDZ",
|
||||||
|
"url": "https://amarillo.mfdz.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Dev server for development",
|
||||||
|
"url": "https://amarillo-dev.mfdz.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Server for Mitanand project",
|
||||||
|
"url": "https://mitanand.mfdz.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Localhost for development",
|
||||||
|
"url": "http://localhost:8000"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
redoc_url=None
|
||||||
|
)
|
||||||
|
configure_enhancer_services()
|
||||||
|
stops_store = container['stops_store']
|
||||||
|
transformer : TripTransformer = TripTransformer(stops_store)
|
||||||
|
logger.info(transformer)
|
||||||
|
|
||||||
|
@app.post("/",
|
||||||
|
operation_id="enhancecarpool",
|
||||||
|
summary="Add a new or update existing carpool",
|
||||||
|
description="Carpool object to be enhanced",
|
||||||
|
response_model=Carpool, # TODO
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
responses={
|
||||||
|
status.HTTP_404_NOT_FOUND: {
|
||||||
|
"description": "Agency does not exist"},
|
||||||
|
|
||||||
|
})
|
||||||
|
#TODO: add examples
|
||||||
|
async def post_carpool(carpool: Carpool = Body(...)) -> Carpool:
|
||||||
|
|
||||||
|
logger.info(f"POST trip {carpool.agency}:{carpool.id}.")
|
||||||
|
|
||||||
|
enhanced = transformer.enhance_carpool(carpool)
|
||||||
|
|
||||||
|
return enhanced
|
||||||
407
amarillo-enhancer/models/Carpool.py
Normal file
407
amarillo-enhancer/models/Carpool.py
Normal file
|
|
@ -0,0 +1,407 @@
|
||||||
|
from datetime import time, date, datetime
|
||||||
|
from pydantic import ConfigDict, BaseModel, Field, HttpUrl, EmailStr
|
||||||
|
from typing import List, Union, Set, Optional, Tuple
|
||||||
|
from datetime import time
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from geojson_pydantic.geometries import LineString
|
||||||
|
from enum import Enum, IntEnum
|
||||||
|
|
||||||
|
NumType = Union[float, int]
|
||||||
|
|
||||||
|
MAX_STOPS_PER_TRIP = 100
|
||||||
|
|
||||||
|
class Weekday(str, Enum):
|
||||||
|
monday = "monday"
|
||||||
|
tuesday = "tuesday"
|
||||||
|
wednesday = "wednesday"
|
||||||
|
thursday = "thursday"
|
||||||
|
friday = "friday"
|
||||||
|
saturday = "saturday"
|
||||||
|
sunday = "sunday"
|
||||||
|
|
||||||
|
class PickupDropoffType(str, Enum):
|
||||||
|
pickup_and_dropoff = "pickup_and_dropoff"
|
||||||
|
only_pickup = "only_pickup"
|
||||||
|
only_dropoff = "only_dropoff"
|
||||||
|
|
||||||
|
class YesNoEnum(IntEnum):
|
||||||
|
yes = 1
|
||||||
|
no = 2
|
||||||
|
|
||||||
|
class LuggageSize(IntEnum):
|
||||||
|
small = 1
|
||||||
|
medium = 2
|
||||||
|
large = 3
|
||||||
|
|
||||||
|
class StopTime(BaseModel):
|
||||||
|
id: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Optional Stop ID. If given, it should conform to the "
|
||||||
|
"IFOPT specification. For official transit stops, "
|
||||||
|
"it should be their official IFOPT. In Germany, this is "
|
||||||
|
"the DHID which is available via the 'zentrales "
|
||||||
|
"Haltestellenverzeichnis (zHV)', published by DELFI e.V. "
|
||||||
|
"Note, that currently carpooling location.",
|
||||||
|
pattern=r"^([a-zA-Z]{2,6}):\d+:\d+(:\d*(:\w+)?)?$|^osm:[nwr]\d+$",
|
||||||
|
examples=["de:12073:900340137::2"])
|
||||||
|
|
||||||
|
name: str = Field(
|
||||||
|
description="Name of the location. Use a name that people will "
|
||||||
|
"understand in the local and tourist vernacular.",
|
||||||
|
min_length=1,
|
||||||
|
max_length=256,
|
||||||
|
examples=["Angermünde, Breitscheidstr."])
|
||||||
|
|
||||||
|
departureTime: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Departure time from a specific stop for a specific "
|
||||||
|
"carpool trip. For times occurring after midnight on the "
|
||||||
|
"service day, the time is given as a value greater than "
|
||||||
|
"24:00:00 in HH:MM:SS local time for the day on which the "
|
||||||
|
"trip schedule begins. If there are not separate times for "
|
||||||
|
"arrival and departure at a stop, the same value for arrivalTime "
|
||||||
|
"and departureTime. Note, that arrivalTime/departureTime of "
|
||||||
|
"the stops are not mandatory, and might then be estimated by "
|
||||||
|
"this service.",
|
||||||
|
pattern=r"^[0-9][0-9]:[0-5][0-9](:[0-5][0-9])?$",
|
||||||
|
examples=["17:00"]
|
||||||
|
)
|
||||||
|
|
||||||
|
arrivalTime: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Arrival time at a specific stop for a specific trip on a "
|
||||||
|
"carpool route. If there are not separate times for arrival "
|
||||||
|
"and departure at a stop, enter the same value for arrivalTime "
|
||||||
|
"and departureTime. For times occurring after midnight on the "
|
||||||
|
"service day, the time as a value greater than 24:00:00 in "
|
||||||
|
"HH:MM:SS local time for the day on which the trip schedule "
|
||||||
|
"begins. Note, that arrivalTime/departureTime of the stops "
|
||||||
|
"are not mandatory, and might then be estimated by this "
|
||||||
|
"service.",
|
||||||
|
pattern=r"^[0-9][0-9]:[0-5][0-9](:[0-5][0-9])?$",
|
||||||
|
examples=["18:00"])
|
||||||
|
|
||||||
|
lat: float = Field(
|
||||||
|
description="Latitude of the location. Should describe the location "
|
||||||
|
"where a passenger may mount/dismount the vehicle.",
|
||||||
|
ge=-90,
|
||||||
|
lt=90,
|
||||||
|
examples=["53.0137311391"])
|
||||||
|
|
||||||
|
lon: float = Field(
|
||||||
|
description="Longitude of the location. Should describe the location "
|
||||||
|
"where a passenger may mount/dismount the vehicle.",
|
||||||
|
ge=-180,
|
||||||
|
lt=180,
|
||||||
|
examples=["13.9934706687"])
|
||||||
|
|
||||||
|
pickup_dropoff: Optional[PickupDropoffType] = Field(
|
||||||
|
None, description="If passengers may be picked up, dropped off or both at this stop. "
|
||||||
|
"If not specified, this service may assign this according to some custom rules. "
|
||||||
|
"E.g. Amarillo may allow pickup only for the first third of the distance travelled, "
|
||||||
|
"and dropoff only for the last third." ,
|
||||||
|
examples=["only_pickup"]
|
||||||
|
)
|
||||||
|
model_config = ConfigDict(json_schema_extra={
|
||||||
|
"example": "{'id': 'de:12073:900340137::2', 'name': "
|
||||||
|
"'Angermünde, Breitscheidstr.', 'lat': 53.0137311391, "
|
||||||
|
"'lon': 13.9934706687}"
|
||||||
|
})
|
||||||
|
|
||||||
|
class Region(BaseModel):
|
||||||
|
id: str = Field(
|
||||||
|
description="ID of the region.",
|
||||||
|
min_length=1,
|
||||||
|
max_length=20,
|
||||||
|
pattern='^[a-zA-Z0-9]+$',
|
||||||
|
examples=["bb"])
|
||||||
|
|
||||||
|
bbox: Tuple[NumType, NumType, NumType, NumType] = Field(
|
||||||
|
description="Bounding box of the region. Format is [minLon, minLat, maxLon, maxLat]",
|
||||||
|
examples=[[10.5,49.2,11.3,51.3]])
|
||||||
|
|
||||||
|
class RidesharingInfo(BaseModel):
|
||||||
|
number_free_seats: int = Field(
|
||||||
|
description="Number of free seats",
|
||||||
|
ge=0,
|
||||||
|
examples=[3])
|
||||||
|
|
||||||
|
same_gender: Optional[YesNoEnum] = Field(
|
||||||
|
None,
|
||||||
|
description="Trip only for same gender:"
|
||||||
|
"1: Yes"
|
||||||
|
"2: No",
|
||||||
|
examples=[1])
|
||||||
|
luggage_size: Optional[LuggageSize] = Field(
|
||||||
|
None,
|
||||||
|
description="Size of the luggage:"
|
||||||
|
"1: small"
|
||||||
|
"2: medium"
|
||||||
|
"3: large",
|
||||||
|
examples=[3])
|
||||||
|
animal_car: Optional[YesNoEnum] = Field(
|
||||||
|
None,
|
||||||
|
description="Animals in Car allowed:"
|
||||||
|
"1: Yes"
|
||||||
|
"2: No",
|
||||||
|
examples=[2])
|
||||||
|
|
||||||
|
car_model: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Car model",
|
||||||
|
min_length=1,
|
||||||
|
max_length=48,
|
||||||
|
examples=["Golf"])
|
||||||
|
car_brand: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Car brand",
|
||||||
|
min_length=1,
|
||||||
|
max_length=48,
|
||||||
|
examples=["VW"])
|
||||||
|
|
||||||
|
creation_date: datetime = Field(
|
||||||
|
description="Date when trip was created",
|
||||||
|
examples=["2022-02-13T20:20:39+00:00"])
|
||||||
|
|
||||||
|
smoking: Optional[YesNoEnum] = Field(
|
||||||
|
None,
|
||||||
|
description="Smoking allowed:"
|
||||||
|
"1: Yes"
|
||||||
|
"2: No",
|
||||||
|
examples=[2])
|
||||||
|
|
||||||
|
payment_method: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Method of payment",
|
||||||
|
min_length=1,
|
||||||
|
max_length=48)
|
||||||
|
|
||||||
|
class Driver(BaseModel):
|
||||||
|
driver_id: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Identifies the driver.",
|
||||||
|
min_length=1,
|
||||||
|
max_length=256,
|
||||||
|
pattern='^[a-zA-Z0-9_-]+$',
|
||||||
|
examples=["789"])
|
||||||
|
profile_picture: Optional[HttpUrl] = Field(
|
||||||
|
None,
|
||||||
|
description="URL that contains the profile picture",
|
||||||
|
examples=["https://mfdz.de/driver/789/picture"])
|
||||||
|
rating: Optional[int] = Field(
|
||||||
|
None,
|
||||||
|
description="Rating of the driver from 1 to 5."
|
||||||
|
"0 no rating yet",
|
||||||
|
ge=0,
|
||||||
|
le=5,
|
||||||
|
examples=[5])
|
||||||
|
|
||||||
|
class Agency(BaseModel):
|
||||||
|
id: str = Field(
|
||||||
|
description="ID of the agency.",
|
||||||
|
min_length=1,
|
||||||
|
max_length=20,
|
||||||
|
pattern='^[a-zA-Z0-9]+$',
|
||||||
|
examples=["mfdz"])
|
||||||
|
|
||||||
|
name: str = Field(
|
||||||
|
description="Name",
|
||||||
|
min_length=1,
|
||||||
|
max_length=48,
|
||||||
|
pattern=r'^[\w -\.\|]+$',
|
||||||
|
examples=["MITFAHR|DE|ZENTRALE"])
|
||||||
|
|
||||||
|
url: HttpUrl = Field(
|
||||||
|
description="URL of the carpool agency.",
|
||||||
|
examples=["https://mfdz.de/"])
|
||||||
|
|
||||||
|
timezone: str = Field(
|
||||||
|
description="Timezone where the carpool agency is located.",
|
||||||
|
min_length=1,
|
||||||
|
max_length=48,
|
||||||
|
pattern=r'^[\w/]+$',
|
||||||
|
examples=["Europe/Berlin"])
|
||||||
|
|
||||||
|
lang: str = Field(
|
||||||
|
description="Primary language used by this carpool agency.",
|
||||||
|
min_length=1,
|
||||||
|
max_length=2,
|
||||||
|
pattern=r'^[a-zA-Z_]+$',
|
||||||
|
examples=["de"])
|
||||||
|
|
||||||
|
email: EmailStr = Field(
|
||||||
|
description="""Email address actively monitored by the agency’s
|
||||||
|
customer service department. This email address should be a direct
|
||||||
|
contact point where carpool riders can reach a customer service
|
||||||
|
representative at the agency.""",
|
||||||
|
examples=["info@mfdz.de"])
|
||||||
|
|
||||||
|
terms_url: Optional[HttpUrl] = Field(
|
||||||
|
None, description="""A fully qualified URL pointing to the terms of service
|
||||||
|
(also often called "terms of use" or "terms and conditions")
|
||||||
|
for the service.""",
|
||||||
|
examples=["https://mfdz.de/nutzungsbedingungen"])
|
||||||
|
|
||||||
|
privacy_url: Optional[HttpUrl] = Field(
|
||||||
|
None, description="""A fully qualified URL pointing to the privacy policy for the service.""",
|
||||||
|
examples=["https://mfdz.de/datenschutz"])
|
||||||
|
model_config = ConfigDict(json_schema_extra={
|
||||||
|
"title": "Agency",
|
||||||
|
"description": "Carpool agency.",
|
||||||
|
"example":
|
||||||
|
#"""
|
||||||
|
{
|
||||||
|
"id": "mfdz",
|
||||||
|
"name": "MITFAHR|DE|ZENTRALE",
|
||||||
|
"url": "http://mfdz.de",
|
||||||
|
"timezone": "Europe/Berlin",
|
||||||
|
"lang": "de",
|
||||||
|
"email": "info@mfdz.de",
|
||||||
|
"terms_url": "https://mfdz.de/nutzungsbedingungen",
|
||||||
|
"privacy_url": "https://mfdz.de/datenschutz",
|
||||||
|
}
|
||||||
|
#"""
|
||||||
|
})
|
||||||
|
|
||||||
|
class Carpool(BaseModel):
|
||||||
|
id: str = Field(
|
||||||
|
description="ID of the carpool. Should be supplied and managed by the "
|
||||||
|
"carpooling platform which originally published this "
|
||||||
|
"offer.",
|
||||||
|
min_length=1,
|
||||||
|
max_length=256,
|
||||||
|
pattern='^[a-zA-Z0-9_-]+$',
|
||||||
|
examples=["103361"])
|
||||||
|
|
||||||
|
agency: str = Field(
|
||||||
|
description="Short one string name of the agency, used as a namespace "
|
||||||
|
"for ids.",
|
||||||
|
min_length=1,
|
||||||
|
max_length=20,
|
||||||
|
pattern='^[a-zA-Z0-9]+$',
|
||||||
|
examples=["mfdz"])
|
||||||
|
|
||||||
|
driver: Optional[Driver] = Field(
|
||||||
|
None,
|
||||||
|
description="Driver data",
|
||||||
|
examples=["""
|
||||||
|
{
|
||||||
|
"driver_id": "123",
|
||||||
|
"profile_picture": "https://mfdz.de/driver/789/picture",
|
||||||
|
"rating": 5
|
||||||
|
}
|
||||||
|
"""])
|
||||||
|
|
||||||
|
deeplink: HttpUrl = Field(
|
||||||
|
description="Link to an information page providing detail information "
|
||||||
|
"for this offer, and, especially, an option to book the "
|
||||||
|
"trip/contact the driver.",
|
||||||
|
examples=["https://mfdz.de/trip/103361"])
|
||||||
|
|
||||||
|
stops: List[StopTime] = Field(
|
||||||
|
...,
|
||||||
|
min_length=2,
|
||||||
|
max_length=MAX_STOPS_PER_TRIP,
|
||||||
|
description="Stops which this carpool passes by and offers to pick "
|
||||||
|
"up/drop off passengers. This list must at minimum "
|
||||||
|
"include two stops, the origin and destination of this "
|
||||||
|
"carpool trip. Note that for privacy reasons, the stops "
|
||||||
|
"usually should be official locations, like meeting "
|
||||||
|
"points, carpool parkings, ridesharing benches or "
|
||||||
|
"similar.",
|
||||||
|
examples=["""[
|
||||||
|
{
|
||||||
|
"id": "03",
|
||||||
|
"name": "drei",
|
||||||
|
"lat": 45,
|
||||||
|
"lon": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "03b",
|
||||||
|
"name": "drei b",
|
||||||
|
"lat": 45,
|
||||||
|
"lon": 9
|
||||||
|
}
|
||||||
|
]"""])
|
||||||
|
|
||||||
|
# TODO can be removed, as first stop has departureTime as well
|
||||||
|
departureTime: time = Field(
|
||||||
|
description="Time when the carpool leaves at the first stop. Note, "
|
||||||
|
"that this API currently does not support flexible time "
|
||||||
|
"windows for departure, though drivers might be flexible."
|
||||||
|
"For recurring trips, the weekdays this trip will run. ",
|
||||||
|
examples=["17:00"])
|
||||||
|
|
||||||
|
# TODO think about using googlecal Format
|
||||||
|
departureDate: Union[date, Set[Weekday]] = Field(
|
||||||
|
description="Date when the trip will start, in case it is a one-time "
|
||||||
|
"trip. For recurring trips, specify weekdays. "
|
||||||
|
"Note, that when for different weekdays different "
|
||||||
|
"departureTimes apply, multiple carpool offers should be "
|
||||||
|
"published.",
|
||||||
|
examples=['A single date 2022-04-04 or a list of weekdays ["saturday", '
|
||||||
|
'"sunday"]'])
|
||||||
|
route_color: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
pattern='^([0-9A-Fa-f]{6})$',
|
||||||
|
description="Route color designation that matches public facing material. "
|
||||||
|
"The color difference between route_color and route_text_color "
|
||||||
|
"should provide sufficient contrast when viewed on a black and "
|
||||||
|
"white screen.",
|
||||||
|
examples=["0039A6"])
|
||||||
|
route_text_color: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
pattern='^([0-9A-Fa-f]{6})$',
|
||||||
|
description="Legible color to use for text drawn against a background of "
|
||||||
|
"route_color. The color difference between route_color and "
|
||||||
|
"route_text_color should provide sufficient contrast when "
|
||||||
|
"viewed on a black and white screen.",
|
||||||
|
examples=["D4D2D2"])
|
||||||
|
path: Optional[LineString] = Field(
|
||||||
|
None, description="Optional route geometry as json LineString.")
|
||||||
|
|
||||||
|
lastUpdated: Optional[datetime] = Field(
|
||||||
|
None,
|
||||||
|
description="LastUpdated should reflect the last time, the user "
|
||||||
|
"providing this offer, made an update or confirmed, "
|
||||||
|
"the offer is still valid. Note that this service might "
|
||||||
|
"purge outdated offers (e.g. older than 180 days). If not "
|
||||||
|
"passed, the service may assume 'now'",
|
||||||
|
examples=["2022-02-13T20:20:39+00:00"])
|
||||||
|
additional_ridesharing_info: Optional[RidesharingInfo] = Field(
|
||||||
|
None,
|
||||||
|
description="Extension of GRFS to the GTFS standard",
|
||||||
|
examples=["""
|
||||||
|
{
|
||||||
|
"number_free_seats": 2,
|
||||||
|
"creation_date": "2022-02-13T20:20:39+00:00",
|
||||||
|
"same_gender": 2,
|
||||||
|
"smoking": 1,
|
||||||
|
"luggage_size": 3
|
||||||
|
}
|
||||||
|
"""])
|
||||||
|
model_config = ConfigDict(json_schema_extra={
|
||||||
|
"title": "Carpool",
|
||||||
|
# description ...
|
||||||
|
"example":
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"id": "1234",
|
||||||
|
"agency": "mfdz",
|
||||||
|
"deeplink": "http://mfdz.de",
|
||||||
|
"stops": [
|
||||||
|
{
|
||||||
|
"id": "de:12073:900340137::2", "name": "ABC",
|
||||||
|
"lat": 45, "lon": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "de:12073:900340137::3", "name": "XYZ",
|
||||||
|
"lat": 45, "lon": 9
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"departureTime": "12:34",
|
||||||
|
"departureDate": "2022-03-30",
|
||||||
|
"lastUpdated": "2022-03-30T12:34:00+00:00"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
})
|
||||||
|
|
@ -73,7 +73,7 @@ class Trip:
|
||||||
return self.bbox.intersects(box(*bbox))
|
return self.bbox.intersects(box(*bbox))
|
||||||
|
|
||||||
|
|
||||||
class TripStore():
|
class TripStoreX():
|
||||||
"""
|
"""
|
||||||
TripStore maintains the currently valid trips. A trip is a
|
TripStore maintains the currently valid trips. A trip is a
|
||||||
carpool offer enhanced with all stops this
|
carpool offer enhanced with all stops this
|
||||||
|
|
@ -91,6 +91,7 @@ class TripStore():
|
||||||
self.recent_trips = {}
|
self.recent_trips = {}
|
||||||
|
|
||||||
|
|
||||||
|
#TODO: move file handling to main Amarillo
|
||||||
def put_carpool(self, carpool: Carpool):
|
def put_carpool(self, carpool: Carpool):
|
||||||
"""
|
"""
|
||||||
Adds carpool to the TripStore.
|
Adds carpool to the TripStore.
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from .enhancer import *
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
import json
|
|
||||||
from threading import Thread
|
|
||||||
import logging
|
|
||||||
import logging.config
|
|
||||||
from watchdog.observers import Observer
|
|
||||||
from watchdog.events import FileSystemEventHandler
|
|
||||||
|
|
||||||
from amarillo.plugins.enhancer.configuration import configure_enhancer_services
|
|
||||||
from amarillo.utils.container import container
|
|
||||||
from amarillo.models.Carpool import Carpool
|
|
||||||
from amarillo.utils.utils import agency_carpool_ids_from_filename
|
|
||||||
|
|
||||||
logging.config.fileConfig('logging.conf', disable_existing_loggers=False)
|
|
||||||
logger = logging.getLogger("enhancer")
|
|
||||||
|
|
||||||
class EventHandler(FileSystemEventHandler):
|
|
||||||
# TODO FG HB should watch for both carpools and agencies
|
|
||||||
# in data/agency, data/agencyconf, see AgencyConfService
|
|
||||||
|
|
||||||
def on_closed(self, event):
|
|
||||||
|
|
||||||
logger.info("CLOSE_WRITE: Created %s", event.src_path)
|
|
||||||
try:
|
|
||||||
with open(event.src_path, 'r', encoding='utf-8') as f:
|
|
||||||
dict = json.load(f)
|
|
||||||
carpool = Carpool(**dict)
|
|
||||||
|
|
||||||
container['carpools'].put(carpool.agency, carpool.id, carpool)
|
|
||||||
except FileNotFoundError as e:
|
|
||||||
logger.error("Carpool could not be added, as already deleted (%s)", event.src_path)
|
|
||||||
except:
|
|
||||||
logger.exception("Eventhandler on_closed encountered exception")
|
|
||||||
|
|
||||||
def on_deleted(self, event):
|
|
||||||
try:
|
|
||||||
logger.info("DELETE: Removing %s", event.src_path)
|
|
||||||
(agency_id, carpool_id) = agency_carpool_ids_from_filename(event.src_path)
|
|
||||||
container['carpools'].delete(agency_id, carpool_id)
|
|
||||||
except:
|
|
||||||
logger.exception("Eventhandler on_deleted encountered exception")
|
|
||||||
|
|
||||||
|
|
||||||
def run_enhancer():
|
|
||||||
logger.info("Hello Enhancer")
|
|
||||||
|
|
||||||
configure_enhancer_services()
|
|
||||||
|
|
||||||
observer = Observer() # Watch Manager
|
|
||||||
|
|
||||||
observer.schedule(EventHandler(), 'data/carpool', recursive=True)
|
|
||||||
observer.start()
|
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
try:
|
|
||||||
# TODO FG Is this really needed?
|
|
||||||
cnt = 0
|
|
||||||
ENHANCER_LOG_INTERVAL_IN_S = 600
|
|
||||||
while True:
|
|
||||||
if cnt == ENHANCER_LOG_INTERVAL_IN_S:
|
|
||||||
logger.debug("Currently stored carpool ids: %s", container['carpools'].get_all_ids())
|
|
||||||
cnt = 0
|
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
cnt += 1
|
|
||||||
finally:
|
|
||||||
observer.stop()
|
|
||||||
observer.join()
|
|
||||||
|
|
||||||
logger.info("Goodbye Enhancer")
|
|
||||||
|
|
||||||
def setup(app):
|
|
||||||
thread = Thread(target=run_enhancer, daemon=True)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_enhancer()
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
# TODO: move to enhancer
|
|
||||||
from collections import namedtuple
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
GtfsFeedInfo = namedtuple('GtfsFeedInfo', 'feed_id feed_publisher_name feed_publisher_url feed_lang feed_version')
|
|
||||||
GtfsAgency = namedtuple('GtfsAgency', 'agency_id agency_name agency_url agency_timezone agency_lang agency_email')
|
|
||||||
GtfsRoute = namedtuple('GtfsRoute', 'agency_id route_id route_long_name route_type route_url route_short_name')
|
|
||||||
GtfsStop = namedtuple('GtfsStop', 'stop_id stop_lat stop_lon stop_name')
|
|
||||||
GtfsStopTime = namedtuple('GtfsStopTime', 'trip_id departure_time arrival_time stop_id stop_sequence pickup_type drop_off_type timepoint')
|
|
||||||
GtfsTrip = namedtuple('GtfsTrip', 'route_id trip_id service_id shape_id trip_headsign bikes_allowed')
|
|
||||||
GtfsCalendar = namedtuple('GtfsCalendar', 'service_id start_date end_date monday tuesday wednesday thursday friday saturday sunday')
|
|
||||||
GtfsCalendarDate = namedtuple('GtfsCalendarDate', 'service_id date exception_type')
|
|
||||||
GtfsShape = namedtuple('GtfsShape','shape_id shape_pt_lon shape_pt_lat shape_pt_sequence')
|
|
||||||
|
|
||||||
# TODO Move to utils
|
|
||||||
class GtfsTimeDelta(timedelta):
|
|
||||||
def __str__(self):
|
|
||||||
seconds = self.total_seconds()
|
|
||||||
hours = seconds // 3600
|
|
||||||
minutes = (seconds % 3600) // 60
|
|
||||||
seconds = seconds % 60
|
|
||||||
str = '{:02d}:{:02d}:{:02d}'.format(int(hours), int(minutes), int(seconds))
|
|
||||||
return (str)
|
|
||||||
|
|
||||||
def __add__(self, other):
|
|
||||||
if isinstance(other, timedelta):
|
|
||||||
return self.__class__(self.days + other.days,
|
|
||||||
self.seconds + other.seconds,
|
|
||||||
self.microseconds + other.microseconds)
|
|
||||||
return NotImplemented
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# Constants
|
|
||||||
|
|
||||||
NO_BIKES_ALLOWED = 2
|
|
||||||
RIDESHARING_ROUTE_TYPE = 1551
|
|
||||||
CALENDAR_DATES_EXCEPTION_TYPE_ADDED = 1
|
|
||||||
CALENDAR_DATES_EXCEPTION_TYPE_REMOVED = 2
|
|
||||||
STOP_TIMES_STOP_TYPE_REGULARLY = 0
|
|
||||||
STOP_TIMES_STOP_TYPE_NONE = 1
|
|
||||||
STOP_TIMES_STOP_TYPE_PHONE_AGENCY = 2
|
|
||||||
STOP_TIMES_STOP_TYPE_COORDINATE_DRIVER = 3
|
|
||||||
STOP_TIMES_TIMEPOINT_APPROXIMATE = 0
|
|
||||||
STOP_TIMES_TIMEPOINT_EXACT = 1
|
|
||||||
|
|
||||||
MFDZ_DEFAULT_UNCERTAINITY = 600
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
from amarillo.plugins.enhancer.services import stops
|
|
||||||
from amarillo.models.Carpool import StopTime
|
|
||||||
|
|
||||||
def test_load_stops_from_file():
|
|
||||||
store = stops.StopsStore([{"url": "amarillo/plugins/enhancer/tests/stops.csv", "vicinity": 50}])
|
|
||||||
store.load_stop_sources()
|
|
||||||
assert len(store.stopsDataFrames[0]['stops']) > 0
|
|
||||||
|
|
||||||
def test_load_csv_stops_from_web_():
|
|
||||||
store = stops.StopsStore([{"url": "https://data.mfdz.de/mfdz/stops/custom.csv", "vicinity": 50}])
|
|
||||||
store.load_stop_sources()
|
|
||||||
assert len(store.stopsDataFrames[0]['stops']) > 0
|
|
||||||
|
|
||||||
def test_load_geojson_stops_from_web_():
|
|
||||||
store = stops.StopsStore([{"url": "https://datahub.bbnavi.de/export/rideshare_points.geojson", "vicinity": 50}])
|
|
||||||
store.load_stop_sources()
|
|
||||||
assert len(store.stopsDataFrames[0]['stops']) > 0
|
|
||||||
|
|
||||||
def test_find_closest_stop():
|
|
||||||
store = stops.StopsStore([{"url": "amarillo/plugins/enhancer/tests/stops.csv", "vicinity": 50}])
|
|
||||||
store.load_stop_sources()
|
|
||||||
carpool_stop = StopTime(name="start", lat=53.1191, lon=14.01577)
|
|
||||||
stop = store.find_closest_stop(carpool_stop, 1000)
|
|
||||||
assert stop.name=='Mitfahrbank Biesenbrow'
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
from amarillo.tests.sampledata import cp1, carpool_repeating
|
|
||||||
from amarillo.plugins.enhancer.services.trips import TripStore
|
|
||||||
from amarillo.plugins.enhancer.services.stops import StopsStore
|
|
||||||
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
def test_trip_store_put_one_time_carpool():
|
|
||||||
trip_store = TripStore(StopsStore())
|
|
||||||
|
|
||||||
t = trip_store.put_carpool(cp1)
|
|
||||||
assert t != None
|
|
||||||
assert len(t.stop_times) >= 2
|
|
||||||
assert t.stop_times[0].stop_id == 'mfdz:12073:001'
|
|
||||||
assert t.stop_times[-1].stop_id == 'de:12073:900340137::3'
|
|
||||||
|
|
||||||
def test_trip_store_put_repeating_carpool():
|
|
||||||
trip_store = TripStore(StopsStore())
|
|
||||||
|
|
||||||
t = trip_store.put_carpool(carpool_repeating)
|
|
||||||
assert t != None
|
|
||||||
assert len(t.stop_times) >= 2
|
|
||||||
28
logging.conf
Normal file
28
logging.conf
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
[loggers]
|
||||||
|
keys=root
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys=consoleHandler, fileHandler
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys=simpleFormatter
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level=INFO
|
||||||
|
handlers=consoleHandler, fileHandler
|
||||||
|
propagate=yes
|
||||||
|
|
||||||
|
[handler_consoleHandler]
|
||||||
|
class=StreamHandler
|
||||||
|
level=DEBUG
|
||||||
|
formatter=simpleFormatter
|
||||||
|
args=(sys.stdout,)
|
||||||
|
|
||||||
|
[handler_fileHandler]
|
||||||
|
class=handlers.RotatingFileHandler
|
||||||
|
level=ERROR
|
||||||
|
formatter=simpleFormatter
|
||||||
|
args=('error.log', 'a', 1000000, 3) # Filename, mode, maxBytes, backupCount
|
||||||
|
|
||||||
|
[formatter_simpleFormatter]
|
||||||
|
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||||
Loading…
Reference in a new issue