Add CLI script to gridify all cities

- reorganize `urban_meal_delivery.console` into a sub-package
- move `tests.db.conftest` fixtures into `tests.conftest`
  => some integration tests regarding CLI scripts need a database
- add `urban_meal_delivery.console.decorators.db_revision` decorator
  to ensure the database is at a certain state before a CLI script runs
- refactor the `urban_meal_delivery.db.grids.Grid.gridify()` constructor:
  - bug fix: even empty `Pixel`s end up in the database temporarily
    => create `Pixel` objects only if an `Address` is to be assigned
       to it
  - streamline code and docstring
  - add further test cases
This commit is contained in:
Alexander Hess 2021-01-06 16:17:05 +01:00
commit 54ff377579
Signed by: alexander
GPG key ID: 344EA5AB10D868E0
15 changed files with 372 additions and 160 deletions

View file

@ -10,15 +10,17 @@ That is the case on the CI server.
import os
import sqlalchemy as sa
from sqlalchemy import engine as engine_mod
from sqlalchemy import orm
import urban_meal_delivery
if os.getenv('TESTING'):
engine = None
connection = None
session = None
# Specify the types explicitly to make mypy happy.
engine: engine_mod.Engine = None
connection: engine_mod.Connection = None
session: orm.Session = None
else: # pragma: no cover
engine = sa.create_engine(urban_meal_delivery.config.DATABASE_URI)

View file

@ -57,8 +57,8 @@ class Grid(meta.Base):
def gridify(cls, city: db.City, side_length: int) -> db.Grid:
"""Create a fully populated `Grid` for a `city`.
The created `Grid` contains only the `Pixel`s for which
there is at least one `Address` in it.
The `Grid` contains only `Pixel`s that have at least one `Address`.
`Address` objects outside the `city`'s viewport are discarded.
Args:
city: city for which the grid is created
@ -69,28 +69,30 @@ class Grid(meta.Base):
"""
grid = cls(city=city, side_length=side_length)
# Create `Pixel` objects covering the entire `city`.
# Note: `+1` so that `city.northeast` corner is on the grid.
possible_pixels = [
db.Pixel(n_x=n_x, n_y=n_y)
for n_x in range((city.total_x // side_length) + 1)
for n_y in range((city.total_y // side_length) + 1)
]
# For convenient lookup by `.n_x`-`.n_y` coordinates.
pixel_map = {(pixel.n_x, pixel.n_y): pixel for pixel in possible_pixels}
# `Pixel`s grouped by `.n_x`-`.n_y` coordinates.
pixels = {}
for address in city.addresses:
# Determine which `pixel` the `address` belongs to.
n_x = address.x // side_length
n_y = address.y // side_length
pixel = pixel_map[n_x, n_y]
# Check if an `address` is not within the `city`'s viewport, ...
not_within_city_viewport = (
address.x < 0
or address.x > city.total_x
or address.y < 0
or address.y > city.total_y
)
# ... and, if so, the `address` does not belong to any `Pixel`.
if not_within_city_viewport:
continue
# Determine which `pixel` the `address` belongs to ...
n_x, n_y = address.x // side_length, address.y // side_length
# ... and create a new `Pixel` object if necessary.
if (n_x, n_y) not in pixels:
pixels[(n_x, n_y)] = db.Pixel(grid=grid, n_x=n_x, n_y=n_y)
pixel = pixels[(n_x, n_y)]
# Create an association between the `address` and `pixel`.
assoc = db.AddressPixelAssociation(address=address, pixel=pixel)
pixel.addresses.append(assoc)
# Only keep `pixel`s that contain at least one `Address`.
grid.pixels = [pixel for pixel in pixel_map.values() if pixel.addresses]
return grid