Skip to content

Commit

Permalink
Merge pull request #59 from openstreetmap-polska/dev
Browse files Browse the repository at this point in the history
Release to main
  • Loading branch information
Zaczero authored Feb 19, 2024
2 parents 18df5fc + 4948380 commit 4db5e31
Show file tree
Hide file tree
Showing 35 changed files with 528 additions and 700 deletions.
34 changes: 28 additions & 6 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,42 @@ jobs:
uses: actions/checkout@v4

- name: Install Nix
uses: cachix/install-nix-action@v24
uses: cachix/install-nix-action@v25
with:
nix_path: nixpkgs=channel:nixpkgs-23.11-darwin
extra_nix_config: |
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
substituters = https://cache.nixos.org/

- name: Extract nixpkgs hash
run: |
nixpkgs_hash=$(grep -o -P '(?<=archive/)[0-9a-f]{40}(?=\.tar\.gz)' shell.nix)
echo "NIXPKGS_HASH=$nixpkgs_hash" >> $GITHUB_ENV
- name: Cache Nix store
uses: actions/cache@v4
id: nix-cache
with:
key: nix-${{ runner.os }}-${{ env.NIXPKGS_HASH }}
path: /tmp/nix-cache

- name: Import Nix store cache
if: steps.nix-cache.outputs.cache-hit == 'true'
run: |
nix-store --import < /tmp/nix-cache
- name: Cache Python packages
uses: actions/cache@v4
with:
key: python-${{ runner.os }}-${{ hashFiles('poetry.lock') }}
path: .venv

- name: Install dependencies
run: |
nix-shell --pure --run true
nix-shell --pure --arg isDevelopment false --run true
- name: Build Cython modules
- name: Export Nix store cache
if: steps.nix-cache.outputs.cache-hit != 'true'
run: |
nix-shell --pure --run cython-build
nix-store --export $(find /nix/store -maxdepth 1 -name '*-*') > /tmp/nix-cache
- name: Build container image
run: |
Expand Down
2 changes: 1 addition & 1 deletion api/v1/api.py → api/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import api.v1.photos as photos
import api.v1.tile as tile

router = APIRouter()
router = APIRouter(prefix='/api/v1')
router.include_router(countries.router)
router.include_router(node.router)
router.include_router(photos.router)
Expand Down
45 changes: 17 additions & 28 deletions api/v1/countries.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,35 @@
from datetime import timedelta
from typing import Annotated

import anyio
from anyio.streams.memory import MemoryObjectSendStream
from anyio import create_task_group
from fastapi import APIRouter, Path, Response
from sentry_sdk import start_span
from shapely.geometry import mapping

from middlewares.cache_middleware import configure_cache
from models.country import Country
from states.aed_state import AEDState, AEDStateDep
from states.country_state import CountryStateDep
from states.aed_state import AEDState
from states.country_state import CountryState

router = APIRouter(prefix='/countries')


async def _count_aed_in_country(country: Country, aed_state: AEDState, send_stream: MemoryObjectSendStream) -> None:
count = await aed_state.count_aeds_by_country_code(country.code)
await send_stream.send((country, count))


@router.get('/names')
@configure_cache(timedelta(hours=1), stale=timedelta(days=7))
async def get_names(
country_state: CountryStateDep,
aed_state: AEDStateDep,
language: str | None = None,
):
countries = await country_state.get_all_countries()
async def get_names(language: str | None = None):
countries = await CountryState.get_all_countries()
country_count_map: dict[str, int] = {}

send_stream, receive_stream = anyio.create_memory_object_stream()
country_count_map = {}
with start_span(description='Counting AEDs'):

async with anyio.create_task_group() as tg, send_stream, receive_stream:
for country in countries:
tg.start_soon(_count_aed_in_country, country, aed_state, send_stream)

for _ in range(len(countries)):
country, count = await receive_stream.receive()
async def count_task(country: Country) -> None:
count = await AEDState.count_aeds_by_country_code(country.code)
country_count_map[country.name] = count

async with create_task_group() as tg:
for country in countries:
tg.start_soon(count_task, country)

def limit_country_names(names: dict[str, str]):
if language and (name := names.get(language)):
return {language: name}
Expand Down Expand Up @@ -67,13 +58,11 @@ def limit_country_names(names: dict[str, str]):
async def get_geojson(
response: Response,
country_code: Annotated[str, Path(min_length=2, max_length=5)],
country_state: CountryStateDep,
aed_state: AEDStateDep,
):
if country_code == 'WORLD':
aeds = await aed_state.get_all_aeds()
aeds = await AEDState.get_all_aeds()
else:
aeds = await aed_state.get_aeds_by_country_code(country_code)
aeds = await AEDState.get_aeds_by_country_code(country_code)

response.headers['Content-Disposition'] = 'attachment'
response.headers['Content-Type'] = 'application/geo+json; charset=utf-8'
Expand All @@ -82,7 +71,7 @@ async def get_geojson(
'features': [
{
'type': 'Feature',
'geometry': mapping(aed.position.shapely),
'geometry': mapping(aed.position),
'properties': {
'@osm_type': 'node',
'@osm_id': aed.id,
Expand Down
24 changes: 12 additions & 12 deletions api/v1/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@

from fastapi import APIRouter, HTTPException
from pytz import timezone
from shapely import Point
from tzfpy import get_tz

from middlewares.cache_middleware import configure_cache
from models.lonlat import LonLat
from states.aed_state import AEDStateDep
from states.photo_state import PhotoStateDep
from states.aed_state import AEDState
from states.photo_state import PhotoState
from utils import get_wikimedia_commons_url

router = APIRouter()

photo_id_re = re.compile(r'view/(?P<id>\S+)\.')


def _get_timezone(lonlat: LonLat) -> tuple[str | None, str | None]:
timezone_name: str | None = get_tz(lonlat.lon, lonlat.lat)
def _get_timezone(position: Point) -> tuple[str | None, str | None]:
timezone_name: str | None = get_tz(position.x, position.y)
timezone_offset = None

if timezone_name:
Expand All @@ -32,14 +32,14 @@ def _get_timezone(lonlat: LonLat) -> tuple[str | None, str | None]:
return timezone_name, timezone_offset


async def _get_image_data(tags: dict[str, str], photo_state: PhotoStateDep) -> dict:
async def _get_image_data(tags: dict[str, str]) -> dict:
image_url: str = tags.get('image', '')

if (
image_url
and (photo_id_match := photo_id_re.search(image_url))
and (photo_id := photo_id_match.group('id'))
and (photo_info := await photo_state.get_photo_by_id(photo_id))
and (photo_info := await PhotoState.get_photo_by_id(photo_id))
):
return {
'@photo_id': photo_info.id,
Expand Down Expand Up @@ -72,13 +72,13 @@ async def _get_image_data(tags: dict[str, str], photo_state: PhotoStateDep) -> d

@router.get('/node/{node_id}')
@configure_cache(timedelta(minutes=1), stale=timedelta(minutes=5))
async def get_node(node_id: int, aed_state: AEDStateDep, photo_state: PhotoStateDep):
aed = await aed_state.get_aed_by_id(node_id)
async def get_node(node_id: int):
aed = await AEDState.get_aed_by_id(node_id)

if aed is None:
raise HTTPException(404, f'Node {node_id} not found')

photo_dict = await _get_image_data(aed.tags, photo_state)
photo_dict = await _get_image_data(aed.tags)

timezone_name, timezone_offset = _get_timezone(aed.position)
timezone_dict = {
Expand All @@ -97,8 +97,8 @@ async def get_node(node_id: int, aed_state: AEDStateDep, photo_state: PhotoState
**timezone_dict,
'type': 'node',
'id': aed.id,
'lat': aed.position.lat,
'lon': aed.position.lon,
'lat': aed.position.y,
'lon': aed.position.x,
'tags': aed.tags,
'version': aed.version,
}
Expand Down
33 changes: 12 additions & 21 deletions api/v1/photos.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
from middlewares.cache_middleware import configure_cache
from openstreetmap import OpenStreetMap, osm_user_has_active_block
from osm_change import update_node_tags_osm_change
from states.aed_state import AEDStateDep
from states.photo_report_state import PhotoReportStateDep
from states.photo_state import PhotoStateDep
from states.aed_state import AEDState
from states.photo_report_state import PhotoReportState
from states.photo_state import PhotoState
from utils import get_http_client, get_wikimedia_commons_url

router = APIRouter(prefix='/photos')
Expand Down Expand Up @@ -51,8 +51,8 @@ async def _fetch_image(url: str) -> tuple[bytes, str]:

@router.get('/view/{id}.webp')
@configure_cache(timedelta(days=365), stale=timedelta(days=365))
async def view(id: str, photo_state: PhotoStateDep) -> FileResponse:
info = await photo_state.get_photo_by_id(id)
async def view(id: str) -> FileResponse:
info = await PhotoState.get_photo_by_id(id)

if info is None:
raise HTTPException(404, f'Photo {id!r} not found')
Expand Down Expand Up @@ -94,8 +94,6 @@ async def upload(
file_license: Annotated[str, Form()],
file: Annotated[UploadFile, File()],
oauth2_credentials: Annotated[str, Form()],
aed_state: AEDStateDep,
photo_state: PhotoStateDep,
) -> bool:
file_license = file_license.upper()
accept_licenses = ('CC0',)
Expand All @@ -117,7 +115,7 @@ async def upload(
if 'access_token' not in oauth2_credentials_:
raise HTTPException(400, 'OAuth2 credentials must contain an access_token field')

aed = await aed_state.get_aed_by_id(node_id)
aed = await AEDState.get_aed_by_id(node_id)
if aed is None:
raise HTTPException(404, f'Node {node_id} not found, perhaps it is not an AED?')

Expand All @@ -129,7 +127,7 @@ async def upload(
if osm_user_has_active_block(osm_user):
raise HTTPException(403, 'User has an active block on OpenStreetMap')

photo_info = await photo_state.set_photo(node_id, osm_user['id'], file)
photo_info = await PhotoState.set_photo(node_id, osm_user['id'], file)
photo_url = f'{request.base_url}api/v1/photos/view/{photo_info.id}.webp'

node_xml = await osm.get_node_xml(node_id)
Expand All @@ -147,26 +145,19 @@ async def upload(


@router.post('/report')
async def report(
id: Annotated[str, Form()],
photo_report_state: PhotoReportStateDep,
) -> bool:
return await photo_report_state.report_by_photo_id(id)
async def report(id: Annotated[str, Form()]) -> bool:
return await PhotoReportState.report_by_photo_id(id)


@router.get('/report/rss.xml')
async def report_rss(
request: Request,
photo_state: PhotoStateDep,
photo_report_state: PhotoReportStateDep,
) -> Response:
async def report_rss(request: Request) -> Response:
fg = FeedGenerator()
fg.title('AED Photo Reports')
fg.description('This feed contains a list of recent AED photo reports')
fg.link(href=str(request.url), rel='self')

for report in await photo_report_state.get_recent_reports():
info = await photo_state.get_photo_by_id(report.photo_id)
for report in await PhotoReportState.get_recent_reports():
info = await PhotoState.get_photo_by_id(report.photo_id)

if info is None:
continue
Expand Down
Loading

0 comments on commit 4db5e31

Please sign in to comment.