Skip to content
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

Feature/maps #23

Draft
wants to merge 19 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
493202e
Complete docstrings for MongoDB module
David-Lor Sep 13, 2021
8217f4f
Avoid logging binary response bodies
David-Lor Sep 13, 2021
4014257
Logic for requesting Google Maps Static API
David-Lor Sep 13, 2021
dd260f8
test: add new Python 3.9 version; disable mongodb container
David-Lor Sep 13, 2021
8023ab6
Support parsing Stop info from HTTP API data source
David-Lor Sep 13, 2021
3b866ef
Add endpoint for getting stop map picture
David-Lor Sep 13, 2021
61cb6b0
Set Google Maps static map request language and picture format
David-Lor Sep 13, 2021
9cc594c
Add endpoint & logic for getting stop photo (StreetView)
David-Lor Sep 13, 2021
6635901
Add endpoint & logic for getting multiple stops map
David-Lor Sep 18, 2021
4644f2f
Use custom markers on multi-stop maps
David-Lor Sep 18, 2021
0c9ac9f
Rollback custom markers on multi-stop maps; Refactor google_maps modu…
David-Lor Sep 26, 2021
8703bd3
Maps & Streetview cache persistence in MongoDB (write)
David-Lor Oct 10, 2021
e14bb34
Maps & Streetview cache read before querying Google Maps API
David-Lor Oct 11, 2021
c22e9d0
refactor: routers in different package and modules by context
David-Lor Oct 11, 2021
ba5f83c
Add endpoint & logic for setting Telegram File ID on cached map pictures
David-Lor Oct 11, 2021
72ff4e1
Return Cache ID on Map endpoints responses
David-Lor Oct 12, 2021
027f453
Change routes order (swap maps and stops-buses routers)
David-Lor Oct 12, 2021
e7acd6a
Add pytimeparse to requirements
David-Lor Oct 12, 2021
0bd8fab
Change TTL method of mongo cached stops: expiration field
David-Lor Dec 27, 2021
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
11 changes: 6 additions & 5 deletions .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ jobs:
- "3.6"
- "3.7"
- "3.8"
services:
mongodb:
image: mongo:latest
ports:
- 27017:27017
- "3.9"
# services:
# mongodb:
# image: mongo:latest
# ports:
# - 27017:27017
steps:
- uses: actions/checkout@master
- name: Setup Python
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ _La API puede devolver información de Paradas y listados en tiempo real de los
- Add endpoints for static buses info
- Add integration tests
- Add detailed install & configuration instructions
- Improve endpoint and services/controllers in-code organization

---

Expand All @@ -107,6 +108,7 @@ _La API puede devolver información de Paradas y listados en tiempo real de los
- _Añadir endpoints para consulta de información estática de buses_
- _Añadir tests de integración_
- _Añadir instrucciones detalladas de instalación y configuración_
- _Mejorar organización en código de endpoints y servicios/controladores_

## Disclaimer

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ cachetools==4.2.2 # TTLCache
pymongo==3.10.1 # MongoDB client
motor==2.1.0 # MongoDB Async client
loguru==0.5.3 # Logging
pytimeparse==1.1.8 # Parse human duration strings into seconds
20 changes: 20 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,23 @@ buses_normal_limit=5
mongo_uri=mongodb://127.0.0.1
mongo_stops_db=vigobusapi
mongo_stops_collection=stops

# # # # # # # # # # # # # # # # # # # #

### Google Maps API Settings ###

# API key for using the Google Maps API. If not set, the features involving its usage will be unavailable (usage would raise exceptions)
google_maps_api_key=

# Default values for GET Stop Map
google_maps_stop_map_default_size_x=1280
google_maps_stop_map_default_size_y=720
google_maps_stop_map_default_zoom=17
google_maps_stop_map_default_type=roadmap

# Default values for GET Photo/Streetview
stop_photo_default_size_x=2000
stop_photo_default_size_y=2000

# Language in which print texts in static Maps pictures (2 characters country code)
google_maps_language=es
46 changes: 46 additions & 0 deletions tests/unit/test_checksumable_objects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import pytest
from vigobusapi.services.google_maps import GoogleMapRequest
from vigobusapi.utils import new_hash_values


@pytest.mark.parametrize("obj", [
GoogleMapRequest(location_x=1, location_y=2, size_x=1, size_y=2, zoom=1,
map_type=GoogleMapRequest.MapTypes.roadmap),
GoogleMapRequest(location_x=1, location_y=2, size_x=1, size_y=2, zoom=2,
map_type=GoogleMapRequest.MapTypes.terrain),
GoogleMapRequest(location_x=1, location_y=2, size_x=1, size_y=2, zoom=2,
map_type=GoogleMapRequest.MapTypes.hybrid),
GoogleMapRequest(location_x=1, location_y=2, size_x=1, size_y=2, zoom=2,
map_type=GoogleMapRequest.MapTypes.satellite,
tags=[GoogleMapRequest.Tag(label="A", location_x=10, location_y=20),
GoogleMapRequest.Tag(label="B", location_x=30, location_y=40)])
])
def test_google_map_request(obj: GoogleMapRequest):
tags_hash_value = "NoTags"
if obj.tags:
tags_checksums = list()
for tag in obj.tags:
tag_hash = new_hash_values(
tag.label,
tag.location_x,
tag.location_y,
algorithm="md5"
)
tags_checksums.append(tag_hash.hexdigest())

tags_hash_value = sorted(tags_checksums)

_hash = new_hash_values(
obj.location_x,
obj.location_y,
obj.size_x,
obj.size_y,
obj.zoom,
obj.map_type.value,
tags_hash_value,
algorithm="sha256"
)

expected_checksum = _hash.hexdigest()
obj_checksum = obj.checksum_value
assert obj_checksum == expected_checksum
45 changes: 45 additions & 0 deletions tests/unit/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import hashlib
import pytest
from vigobusapi.utils import new_hash_values, update_hash_values


@pytest.mark.parametrize("algorithm", ["md5", "sha256"])
def test_new_hash_values(algorithm: str):
hashes = {
"md5": hashlib.md5,
"sha256": hashlib.sha256
}
data = ["a string", 1, True, None, [1, "other string", False, None]]

_hash = hashes[algorithm]()
for chunk in data:
_hash.update(str(chunk).encode())

expected_hexdigest = _hash.hexdigest()
result_hexdigest = new_hash_values(*data, algorithm=algorithm).hexdigest()

assert result_hexdigest == expected_hexdigest


@pytest.mark.parametrize("algorithm", ["md5", "sha256"])
def test_update_hash_values(algorithm: str):
hashes = {
"md5": hashlib.md5,
"sha256": hashlib.sha256
}
full_hash = hashes[algorithm]()
original_hash = hashes[algorithm]()

original_data = ["initial string", 0.0]
for chunk in original_data:
original_hash.update(str(chunk).encode())
full_hash.update(str(chunk).encode())

new_data = ["a string", 1, True, None, [1, "other string", False, None]]
for chunk in new_data:
full_hash.update(str(chunk).encode())

expected_hexdigest = full_hash.hexdigest()
result_hexdigest = update_hash_values(*new_data, _hash=original_hash).hexdigest()

assert result_hexdigest == expected_hexdigest
63 changes: 4 additions & 59 deletions vigobusapi/app.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
"""APP
Module with all the available endpoints and the FastAPI initialization.
FastAPI initialization.
"""

# # Native # #
from typing import Optional, Set

# # Installed # #
import uvicorn
from fastapi import FastAPI, Response, Query, HTTPException
from fastapi import FastAPI

# # Project # #
from vigobusapi.entities import Stop, Stops, BusesResponse
from vigobusapi.routes import setup_routes
from vigobusapi.request_handler import request_handler
from vigobusapi.settings import settings
from vigobusapi.vigobus_getters import get_stop, get_stops, get_buses, search_stops
from vigobusapi.services import MongoDB
from vigobusapi.logger import logger

Expand All @@ -23,6 +19,7 @@
title=settings.api_name
)
app.middleware("http")(request_handler)
setup_routes(app)


@app.on_event("startup")
Expand All @@ -32,58 +29,6 @@ async def app_setup():
await MongoDB.initialize()


@app.get("/status")
async def endpoint_status():
return Response(
content="OK",
media_type="text/plain",
status_code=200
)


@app.get("/stops", response_model=Stops)
async def endpoint_get_stops(
stop_name: Optional[str] = Query(None),
limit: Optional[int] = Query(None),
stops_ids: Optional[Set[int]] = Query(None, alias="stop_id")
):
"""Endpoint to search/list stops by different filters. Only one filter can be used.
Returns 400 if no filters given.
The filters available are:

- stop_name: search by a single string in stop names. "limit" can be used for limiting results size.
- stop_id: repeatable param for getting multiple stops by id on a single request. Not found errors are ignored.
"""
with logger.contextualize(**locals()):
if stop_name is not None:
stops = await search_stops(stop_name=stop_name, limit=limit)
elif stops_ids:
stops = await get_stops(stops_ids)
else:
raise HTTPException(status_code=400, detail="No filters given")
return [stop.dict() for stop in stops]


@app.get("/stop/{stop_id}", response_model=Stop)
async def endpoint_get_stop(stop_id: int):
"""Endpoint to get information of a Stop giving the Stop ID
"""
with logger.contextualize(**locals()):
stop = await get_stop(stop_id)
return stop.dict()


@app.get("/buses/{stop_id}", response_model=BusesResponse)
@app.get("/stop/{stop_id}/buses", response_model=BusesResponse)
async def endpoint_get_buses(stop_id: int, get_all_buses: bool = False):
"""Endpoint to get a list of Buses coming to a Stop giving the Stop ID.
By default the shortest available list of buses is returned, unless 'get_all_buses' param is True
"""
with logger.contextualize(**locals()):
buses_result = await get_buses(stop_id, get_all_buses=get_all_buses)
return buses_result.dict()


def run():
"""Run the API using Uvicorn
"""
Expand Down
28 changes: 27 additions & 1 deletion vigobusapi/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# # Package # #
from vigobusapi.exceptions import StopNotExist

__all__ = ("Stop", "Stops", "OptionalStop", "StopOrNotExist", "Bus", "Buses", "BusesResponse")
__all__ = ("BaseMongoModel", "Stop", "Stops", "OptionalStop", "StopOrNotExist", "Bus", "Buses", "BusesResponse")


class BaseModel(pydantic.BaseModel):
Expand All @@ -24,6 +24,28 @@ def dict(self, *args, skip_none=True, **kwargs):
return {k: v for k, v in d.items() if (not skip_none or v is not None)}


class BaseMongoModel(pydantic.BaseModel):
# TODO Use in Stop models
class Config(pydantic.BaseModel.Config):
id_field: Optional[str] = None

def to_mongo(self, **kwargs) -> dict:
d = self.dict(**kwargs)
if self.Config.id_field is None:
return d

d["_id"] = d.pop(self.Config.id_field)
return d

@classmethod
def from_mongo(cls, d: dict):
if cls.Config.id_field is not None:
d[cls.Config.id_field] = d.pop("_id")

# noinspection PyArgumentList
return cls(**d)


class Bus(BaseModel):
line: str
route: str
Expand Down Expand Up @@ -77,6 +99,10 @@ def get_mongo_dict(self):
d.pop("source")
return d

@property
def has_location(self):
return self.lat is not None and self.lon is not None


OptionalStop = Optional[Stop]
StopOrNotExist = Union[Stop, StopNotExist]
Expand Down
1 change: 1 addition & 0 deletions vigobusapi/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .bootstrap import *
18 changes: 18 additions & 0 deletions vigobusapi/routes/_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""API "General" Routes
"""

# # Installed # #
from fastapi import APIRouter, Response

__all__ = ("router",)

router = APIRouter()


@router.get("/status")
async def endpoint_status():
return Response(
content="OK",
media_type="text/plain",
status_code=200
)
Loading