diff --git a/README.md b/README.md index 92c2ebf..a260581 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,16 @@ that iteratively build on each other. ### Data Cleaning The UDP provided its raw data as a PostgreSQL dump. -This [notebook](https://nbviewer.jupyter.org/github/webartifex/urban-meal-delivery/blob/develop/research/01_clean_data.ipynb) +This [notebook](https://nbviewer.jupyter.org/github/webartifex/urban-meal-delivery/blob/main/research/01_clean_data.ipynb) cleans the data extensively -and maps them onto the [ORM models](https://github.com/webartifex/urban-meal-delivery/tree/develop/src/urban_meal_delivery/db) +and maps them onto the [ORM models](https://github.com/webartifex/urban-meal-delivery/tree/main/src/urban_meal_delivery/db) defined in the `urban-meal-delivery` package -that is developed in the [src/](https://github.com/webartifex/urban-meal-delivery/tree/develop/src) folder +that is developed in the [src/](https://github.com/webartifex/urban-meal-delivery/tree/main/src) folder and contains all source code to drive the analyses. Due to a non-disclosure agreement with the UDP, neither the raw nor the cleaned data are published as of now. -However, previews of the data can be seen throughout the [research/](https://github.com/webartifex/urban-meal-delivery/tree/develop/research) folder. +However, previews of the data can be seen throughout the [research/](https://github.com/webartifex/urban-meal-delivery/tree/main/research) folder. ### Tactical Demand Forecasting @@ -34,9 +34,9 @@ Before any optimizations of the UDP's operations are done, a **demand forecasting** system for *tactical* purposes is implemented. To achieve that, the cities first undergo a **gridification** step where each *pickup* location is assigned into a pixel on a "checker board"-like grid. -The main part of the source code that implements that is in this [file](https://github.com/webartifex/urban-meal-delivery/blob/develop/src/urban_meal_delivery/db/grids.py#L60). -Visualizations of the various grids can be found in the [visualizations/](https://github.com/webartifex/urban-meal-delivery/tree/develop/research/visualizations) folder -and in this [notebook](https://nbviewer.jupyter.org/github/webartifex/urban-meal-delivery/blob/develop/research/03_grid_visualizations.ipynb). +The main part of the source code that implements that is in this [file](https://github.com/webartifex/urban-meal-delivery/blob/main/src/urban_meal_delivery/db/grids.py#L60). +Visualizations of the various grids can be found in the [visualizations/](https://github.com/webartifex/urban-meal-delivery/tree/main/research/visualizations) folder +and in this [notebook](https://nbviewer.jupyter.org/github/webartifex/urban-meal-delivery/blob/main/research/03_grid_visualizations.ipynb). Then, demand is aggregated on a per-pixel level and different kinds of order time series are generated. @@ -47,7 +47,7 @@ The details of how this works can be found in the first academic paper published in the context of this research project and titled "*Real-time Demand Forecasting for an Urban Delivery Platform*" (cf., the [repository](https://github.com/webartifex/urban-meal-delivery-demand-forecasting) with the LaTeX files). -All demand forecasting related code is in the [forecasts/](https://github.com/webartifex/urban-meal-delivery/tree/develop/src/urban_meal_delivery/forecasts) sub-package. +All demand forecasting related code is in the [forecasts/](https://github.com/webartifex/urban-meal-delivery/tree/main/src/urban_meal_delivery/forecasts) sub-package. ### Predictive Routing @@ -71,11 +71,11 @@ and `poetry install --extras research` The `--extras` option is necessary as the non-develop dependencies -are structured in the [pyproject.toml](https://github.com/webartifex/urban-meal-delivery/blob/develop/pyproject.toml) file +are structured in the [pyproject.toml](https://github.com/webartifex/urban-meal-delivery/blob/main/pyproject.toml) file into dependencies related to only the `urban-meal-delivery` source code package and dependencies used to run the [Jupyter](https://jupyter.org/) environment with the analyses. Contributions are welcome. Use the [issues](https://github.com/webartifex/urban-meal-delivery/issues) tab. -The project is licensed under the [MIT license](https://github.com/webartifex/urban-meal-delivery/blob/develop/LICENSE.txt). +The project is licensed under the [MIT license](https://github.com/webartifex/urban-meal-delivery/blob/main/LICENSE.txt). diff --git a/migrations/versions/rev_20210914_13_81aa304d7a6e_add_simulation_data.py b/migrations/versions/rev_20210914_13_81aa304d7a6e_add_simulation_data.py deleted file mode 100644 index 81c8b67..0000000 --- a/migrations/versions/rev_20210914_13_81aa304d7a6e_add_simulation_data.py +++ /dev/null @@ -1,413 +0,0 @@ -"""Add simulation data. - -Revision: # 81aa304d7a6e at 2021-09-14 13:47:03 -Revises: # b4dd0b8903a5 -""" - -import os - -import sqlalchemy as sa -from alembic import op - -from urban_meal_delivery import configuration - - -revision = '81aa304d7a6e' -down_revision = 'b4dd0b8903a5' -branch_labels = None -depends_on = None - - -config = configuration.make_config('testing' if os.getenv('TESTING') else 'production') - - -def upgrade(): - """Upgrade to revision 81aa304d7a6e.""" - # Drop unnecessary check constraint. - op.execute( # `.delivery_at` is not set for `.cancelled` orders anyways. - f""" - ALTER TABLE {config.CLEAN_SCHEMA}.orders - DROP CONSTRAINT ck_orders_on_ordered_timestamps_21; - """, - ) # noqa:WPS355 - - # Add forgotten check constraints to the `orders` table. - op.execute( - f""" - ALTER TABLE {config.CLEAN_SCHEMA}.orders - ADD CONSTRAINT check_orders_on_ordered_timestamps_placed_at_before_pickup_at - CHECK (placed_at < pickup_at); - """, - ) # noqa:WPS355 - op.execute( - f""" - ALTER TABLE {config.CLEAN_SCHEMA}.orders - ADD CONSTRAINT check_orders_on_scheduled_orders_must_be_at_quarters_of_an_hour - CHECK ( - ( - EXTRACT(MINUTES FROM scheduled_delivery_at)::INTEGER - % 15 = 0 - ) - AND - ( - EXTRACT(SECONDS FROM scheduled_delivery_at)::INTEGER - = 0 - ) - ); - """, - ) # noqa:WPS355 - - # This `UniqueConstraint` is needed by the `replayed_orders` table below. - op.create_unique_constraint( - 'uq_orders_on_id_ad_hoc', - 'orders', - ['id', 'ad_hoc'], - schema=config.CLEAN_SCHEMA, - ) - - op.create_table( - 'replay_simulations', - sa.Column('id', sa.Integer, autoincrement=True), - sa.Column('city_id', sa.SmallInteger, nullable=False), - sa.Column('strategy', sa.Unicode(length=100), nullable=False), - sa.Column('run', sa.SmallInteger, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('pk_replay_simulations')), - sa.ForeignKeyConstraint( - ['city_id'], - [f'{config.CLEAN_SCHEMA}.cities.id'], - name=op.f('fk_replay_simulations_to_cities_via_city_id'), - onupdate='RESTRICT', - ondelete='RESTRICT', - ), - sa.UniqueConstraint( - 'city_id', - 'strategy', - 'run', - name=op.f('uq_replay_simulations_on_city_id_strategy_run'), - ), - sa.CheckConstraint('run >= 0', name=op.f('run_is_a_count')), - schema=config.CLEAN_SCHEMA, - ) - - op.create_table( - 'replayed_orders', - sa.Column('simulation_id', sa.Integer, primary_key=True), - sa.Column('actual_order_id', sa.Integer, primary_key=True), - sa.Column('ad_hoc', sa.Boolean, nullable=False), - sa.Column('placed_at', sa.DateTime, nullable=False), - sa.Column('scheduled_delivery_at', sa.DateTime), - sa.Column('cancelled_at', sa.DateTime), - sa.Column('estimated_prep_duration', sa.SmallInteger), - sa.Column('restaurant_notified_at', sa.DateTime), - sa.Column('restaurant_confirmed_at', sa.DateTime), - sa.Column('restaurant_ready_at', sa.DateTime), - sa.Column('dispatch_at', sa.DateTime), - sa.Column('first_estimated_delivery_at', sa.DateTime), - sa.Column('courier_id', sa.Integer), - sa.Column('courier_notified_at', sa.DateTime), - sa.Column('courier_accepted_at', sa.DateTime), - sa.Column('utilization', sa.SmallInteger), - sa.Column('reached_pickup_at', sa.DateTime), - sa.Column('pickup_at', sa.DateTime), - sa.Column('left_pickup_at', sa.DateTime), - sa.Column('reached_delivery_at', sa.DateTime), - sa.Column('delivery_at', sa.DateTime), - sa.PrimaryKeyConstraint( - 'simulation_id', 'actual_order_id', name=op.f('pk_replayed_orders'), - ), - sa.ForeignKeyConstraint( - ['simulation_id'], - [f'{config.CLEAN_SCHEMA}.replay_simulations.id'], - name=op.f('fk_replayed_orders_to_replay_simulations_via_simulation_id'), - onupdate='RESTRICT', - ondelete='RESTRICT', - ), - sa.ForeignKeyConstraint( - # Needs the `UniqueConstraint` from above. - ['actual_order_id', 'ad_hoc'], - [ - f'{config.CLEAN_SCHEMA}.orders.id', - f'{config.CLEAN_SCHEMA}.orders.ad_hoc', - ], - name=op.f('fk_replayed_orders_to_orders_via_actual_order_id_ad_hoc'), - onupdate='RESTRICT', - ondelete='RESTRICT', - ), - sa.ForeignKeyConstraint( - ['courier_id'], - [f'{config.CLEAN_SCHEMA}.couriers.id'], - name=op.f('fk_replayed_orders_to_couriers_via_courier_id'), - onupdate='RESTRICT', - ondelete='RESTRICT', - ), - sa.CheckConstraint( - """ - ( - ad_hoc IS TRUE - AND - scheduled_delivery_at IS NULL - ) - OR - ( - ad_hoc IS FALSE - AND - scheduled_delivery_at IS NOT NULL - ) - """, - name=op.f('either_ad_hoc_or_scheduled_order'), - ), - sa.CheckConstraint( - """ - NOT ( - ad_hoc IS TRUE - AND ( - EXTRACT(HOUR FROM placed_at) < 11 - OR - EXTRACT(HOUR FROM placed_at) > 22 - ) - ) - """, - name=op.f('ad_hoc_orders_within_business_hours'), - ), - sa.CheckConstraint( - """ - NOT ( - ad_hoc IS FALSE - AND ( - ( - EXTRACT(HOUR FROM scheduled_delivery_at) <= 11 - AND - NOT ( - EXTRACT(HOUR FROM scheduled_delivery_at) = 11 - AND - EXTRACT(MINUTE FROM scheduled_delivery_at) = 45 - ) - ) - OR - EXTRACT(HOUR FROM scheduled_delivery_at) > 22 - ) - ) - """, - name=op.f('scheduled_orders_within_business_hours'), - ), - sa.CheckConstraint( - """ - ( - EXTRACT(MINUTES FROM scheduled_delivery_at)::INTEGER - % 15 = 0 - ) - AND - ( - EXTRACT(SECONDS FROM scheduled_delivery_at)::INTEGER - = 0 - ) - """, - name=op.f('scheduled_orders_must_be_at_quarters_of_an_hour'), - ), - sa.CheckConstraint( - """ - NOT ( - EXTRACT(EPOCH FROM scheduled_delivery_at - placed_at) < 1800 - ) - """, - name=op.f('scheduled_orders_not_within_30_minutes'), - ), - sa.CheckConstraint( - """ - cancelled_at IS NOT NULL - OR - ( - restaurant_notified_at IS NOT NULL - AND - restaurant_confirmed_at IS NOT NULL - AND - restaurant_ready_at IS NOT NULL - AND - dispatch_at IS NOT NULL - AND - courier_id IS NOT NULL - AND - courier_notified_at IS NOT NULL - AND - courier_accepted_at IS NOT NULL - AND - reached_pickup_at IS NOT NULL - AND - pickup_at IS NOT NULL - AND - left_pickup_at IS NOT NULL - AND - reached_delivery_at IS NOT NULL - AND - delivery_at IS NOT NULL - ) - """, - name=op.f('orders_must_be_either_cancelled_or_fully_simulated'), - ), - sa.CheckConstraint( - """ - NOT ( -- Only occurred in 528 of 660,608 orders in the actual data. - cancelled_at IS NOT NULL - AND - pickup_at IS NOT NULL - ) - AND - NOT ( -- Only occurred in 176 of 660,608 orders in the actual data. - cancelled_at IS NOT NULL - AND - left_pickup_at IS NOT NULL - ) - AND - NOT ( -- Never occurred in the actual data. - cancelled_at IS NOT NULL - AND - reached_delivery_at IS NOT NULL - ) - AND - NOT ( -- Never occurred in the actual data. - cancelled_at IS NOT NULL - AND - delivery_at IS NOT NULL - ) - """, - name=op.f('cancellations_may_only_occur_before_pickup'), - ), - sa.CheckConstraint( - '0 <= estimated_prep_duration AND estimated_prep_duration <= 3600', - name=op.f('estimated_prep_duration_between_0_and_3600'), - ), - sa.CheckConstraint( - 'estimated_prep_duration % 60 = 0', - name=op.f('estimated_prep_duration_must_be_whole_minutes'), - ), - sa.CheckConstraint( - '0 <= utilization AND utilization <= 100', - name=op.f('utilization_between_0_and_100'), - ), - sa.CheckConstraint( - """ - NOT ( - EXTRACT(HOUR FROM restaurant_notified_at) < 11 - OR - EXTRACT(HOUR FROM dispatch_at) < 11 - ) - """, - name=op.f('orders_dispatched_in_business_hours'), - ), - *( - sa.CheckConstraint( - constraint, name='ordered_timestamps_{index}'.format(index=index), - ) - for index, constraint in enumerate( - ( - 'placed_at < scheduled_delivery_at', - 'placed_at < cancelled_at', - 'placed_at < restaurant_notified_at', - 'placed_at < restaurant_confirmed_at', - 'placed_at < restaurant_ready_at', - 'placed_at < dispatch_at', - 'placed_at < first_estimated_delivery_at', - 'placed_at < courier_notified_at', - 'placed_at < courier_accepted_at', - 'placed_at < reached_pickup_at', - 'placed_at < pickup_at', - 'placed_at < left_pickup_at', - 'placed_at < reached_delivery_at', - 'placed_at < delivery_at', - 'cancelled_at > restaurant_notified_at', - 'cancelled_at > restaurant_confirmed_at', - 'cancelled_at > restaurant_ready_at', - 'cancelled_at > dispatch_at', - 'cancelled_at > courier_notified_at', - 'cancelled_at > courier_accepted_at', - 'cancelled_at > reached_pickup_at', - 'restaurant_notified_at < restaurant_confirmed_at', - 'restaurant_notified_at < restaurant_ready_at', - 'restaurant_notified_at < pickup_at', - 'restaurant_confirmed_at < restaurant_ready_at', - 'restaurant_confirmed_at < pickup_at', - 'restaurant_ready_at < pickup_at', - 'dispatch_at < first_estimated_delivery_at', - 'dispatch_at < courier_notified_at', - 'dispatch_at < courier_accepted_at', - 'dispatch_at < reached_pickup_at', - 'dispatch_at < pickup_at', - 'dispatch_at < left_pickup_at', - 'dispatch_at < reached_delivery_at', - 'dispatch_at < delivery_at', - 'courier_notified_at < courier_accepted_at', - 'courier_notified_at < reached_pickup_at', - 'courier_notified_at < pickup_at', - 'courier_notified_at < left_pickup_at', - 'courier_notified_at < reached_delivery_at', - 'courier_notified_at < delivery_at', - 'courier_accepted_at < reached_pickup_at', - 'courier_accepted_at < pickup_at', - 'courier_accepted_at < left_pickup_at', - 'courier_accepted_at < reached_delivery_at', - 'courier_accepted_at < delivery_at', - 'reached_pickup_at < pickup_at', - 'reached_pickup_at < left_pickup_at', - 'reached_pickup_at < reached_delivery_at', - 'reached_pickup_at < delivery_at', - 'pickup_at < left_pickup_at', - 'pickup_at < reached_delivery_at', - 'pickup_at < delivery_at', - 'left_pickup_at < reached_delivery_at', - 'left_pickup_at < delivery_at', - 'reached_delivery_at < delivery_at', - ), - ) - ), - schema=config.CLEAN_SCHEMA, - ) - - op.create_index( - op.f('ix_replay_simulations_on_city_id'), - 'replay_simulations', - ['city_id'], - unique=False, - schema=config.CLEAN_SCHEMA, - ) - op.create_index( - op.f('ix_replay_simulations_on_strategy'), - 'replay_simulations', - ['strategy'], - unique=False, - schema=config.CLEAN_SCHEMA, - ) - op.create_index( - op.f('ix_replayed_orders_on_courier_id'), - 'replayed_orders', - ['courier_id'], - unique=False, - schema=config.CLEAN_SCHEMA, - ) - - -def downgrade(): - """Downgrade to revision b4dd0b8903a5.""" - op.drop_table('replayed_orders', schema=config.CLEAN_SCHEMA) - op.drop_table('replay_simulations', schema=config.CLEAN_SCHEMA) - op.drop_constraint( - 'uq_orders_on_id_ad_hoc', 'orders', type_=None, schema=config.CLEAN_SCHEMA, - ) - op.execute( - f""" - ALTER TABLE {config.CLEAN_SCHEMA}.orders - DROP CONSTRAINT check_orders_on_scheduled_orders_must_be_at_quarters_of_an_hour; - """, - ) # noqa:WPS355 - op.execute( - f""" - ALTER TABLE {config.CLEAN_SCHEMA}.orders - DROP CONSTRAINT check_orders_on_ordered_timestamps_placed_at_before_pickup_at; - """, - ) # noqa:WPS355 - op.execute( - f""" - ALTER TABLE {config.CLEAN_SCHEMA}.orders - ADD CONSTRAINT ck_orders_on_ordered_timestamps_21 - CHECK (cancelled_at > delivery_at); - """, - ) # noqa:WPS355 diff --git a/noxfile.py b/noxfile.py index 05106a6..f79025c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -384,7 +384,7 @@ def test_suite(session): @nox.session(name='fix-branch-references', python=PYTHON, venv_backend='none') -def fix_branch_references(session): # noqa:WPS210 +def fix_branch_references(session): # noqa:WPS210,WPS231 """Replace branch references with the current branch. Intended to be run as a pre-commit hook. @@ -511,7 +511,7 @@ def init_project(session): @nox.session(name='clean-pwd', python=PYTHON, venv_backend='none') -def clean_pwd(session): +def clean_pwd(session): # noqa:WPS231 """Remove (almost) all glob patterns listed in .gitignore. The difference compared to `git clean -X` is that this task diff --git a/pyproject.toml b/pyproject.toml index 477f970..0e301da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ target-version = ["py38"] [tool.poetry] name = "urban-meal-delivery" -version = "0.5.0.dev0" +version = "0.4.0" authors = ["Alexander Hess "] description = "Optimizing an urban meal delivery platform" diff --git a/research/01_clean_data.ipynb b/research/01_clean_data.ipynb index 1d9f6fb..c3de005 100644 --- a/research/01_clean_data.ipynb +++ b/research/01_clean_data.ipynb @@ -19,7 +19,7 @@ "- numeric columns are checked for plausibility\n", "- foreign key relationships are strictly enforced\n", "\n", - "The structure of the data can be viewed at the [ORM layer](https://github.com/webartifex/urban-meal-delivery/tree/develop/src/urban_meal_delivery/db) in the package." + "The structure of the data can be viewed at the [ORM layer](https://github.com/webartifex/urban-meal-delivery/tree/main/src/urban_meal_delivery/db) in the package." ] }, { diff --git a/research/08_google_maps.ipynb b/research/08_google_maps.ipynb index fb4d324..9c6e0aa 100644 --- a/research/08_google_maps.ipynb +++ b/research/08_google_maps.ipynb @@ -621,7 +621,7 @@ "id": "fb6a035d", "metadata": {}, "source": [ - "In the application's code base, the above API calls and the related data are modeled as `Path` objects connecting two `Address` objects (cf. [Path class](https://github.com/webartifex/urban-meal-delivery/blob/develop/src/urban_meal_delivery/db/addresses_addresses.py) in the code).\n", + "In the application's code base, the above API calls and the related data are modeled as `Path` objects connecting two `Address` objects (cf. [Path class](https://github.com/webartifex/urban-meal-delivery/blob/main/src/urban_meal_delivery/db/addresses_addresses.py) in the code).\n", "\n", "Let's look at two examples addresses, one from a `Restaurant` and one from a `Customer`." ] diff --git a/setup.cfg b/setup.cfg index 4d571dd..e91e277 100644 --- a/setup.cfg +++ b/setup.cfg @@ -93,15 +93,8 @@ extend-ignore = # until after being processed by Sphinx Napoleon. # Source: https://github.com/peterjc/flake8-rst-docstrings/issues/17 RST201,RST203,RST210,RST213,RST301, - # The "too deep nesting" violation fires off "too early" and cannot be configured. - WPS220, # String constant over-use is checked visually by the programmer. WPS226, - # Function complexity is checked by `mccabe` (=C901), - # which yields the same result in most cases. - WPS231, - # Modules as a whole are assumed to be not "too complex". - WPS232, # Allow underscores in numbers. WPS303, # f-strings are ok. @@ -111,8 +104,6 @@ extend-ignore = # Let's be modern: The Walrus is ok. WPS332, # Let's not worry about the number of noqa's. - # Multi-line loops are ok. - WPS352, WPS402, # Putting logic into __init__.py files may be justified. WPS412, @@ -150,9 +141,24 @@ per-file-ignores = src/urban_meal_delivery/configuration.py: # Allow upper case class variables within classes. WPS115, + src/urban_meal_delivery/console/forecasts.py: + # The module is not too complex. + WPS232, src/urban_meal_delivery/db/addresses_addresses.py: # The module does not have too many imports. WPS201, + src/urban_meal_delivery/db/customers.py: + # The module is not too complex. + WPS232, + src/urban_meal_delivery/db/restaurants.py: + # The module is not too complex. + WPS232, + src/urban_meal_delivery/forecasts/methods/decomposition.py: + # The module is not too complex. + WPS232, + src/urban_meal_delivery/forecasts/methods/extrapolate_season.py: + # The module is not too complex. + WPS232, src/urban_meal_delivery/forecasts/models/tactical/horizontal.py: # The many noqa's are ok. WPS403, @@ -172,8 +178,6 @@ per-file-ignores = S311, # Shadowing outer scopes occurs naturally with mocks. WPS442, - # `utils` should be a valid module name. - WPS100, # Test names may be longer than 40 characters. WPS118, # Modules may have many test cases. diff --git a/src/urban_meal_delivery/console/forecasts.py b/src/urban_meal_delivery/console/forecasts.py index c91a7fd..5d2a20d 100644 --- a/src/urban_meal_delivery/console/forecasts.py +++ b/src/urban_meal_delivery/console/forecasts.py @@ -24,7 +24,7 @@ from urban_meal_delivery.forecasts import timify @click.argument('time_step', default=60, type=int) @click.argument('train_horizon', default=8, type=int) @decorators.db_revision('8bfb928a31f8') -def tactical_heuristic( # noqa:C901,WPS213,WPS216 +def tactical_heuristic( # noqa:C901,WPS213,WPS216,WPS231 city: str, side_length: int, time_step: int, train_horizon: int, ) -> None: # pragma: no cover """Predict demand for all pixels and days in a city. diff --git a/src/urban_meal_delivery/db/__init__.py b/src/urban_meal_delivery/db/__init__.py index 7f69d6d..6161657 100644 --- a/src/urban_meal_delivery/db/__init__.py +++ b/src/urban_meal_delivery/db/__init__.py @@ -14,6 +14,4 @@ from urban_meal_delivery.db.grids import Grid from urban_meal_delivery.db.meta import Base from urban_meal_delivery.db.orders import Order from urban_meal_delivery.db.pixels import Pixel -from urban_meal_delivery.db.replay import ReplayedOrder -from urban_meal_delivery.db.replay import ReplaySimulation from urban_meal_delivery.db.restaurants import Restaurant diff --git a/src/urban_meal_delivery/db/cities.py b/src/urban_meal_delivery/db/cities.py index dd8b734..926b3ae 100644 --- a/src/urban_meal_delivery/db/cities.py +++ b/src/urban_meal_delivery/db/cities.py @@ -39,7 +39,6 @@ class City(meta.Base): # Relationships addresses = orm.relationship('Address', back_populates='city') grids = orm.relationship('Grid', back_populates='city') - replays = orm.relationship('ReplaySimulation', back_populates='city') # We do not implement a `.__init__()` method and use SQLAlchemy's default. # The uninitialized attribute `._map` is computed on the fly. note:d334120ei @@ -118,7 +117,7 @@ class City(meta.Base): return self._map - def draw_restaurants( + def draw_restaurants( # noqa:WPS231 self, order_counts: bool = False, # pragma: no cover ) -> folium.Map: """Draw all restaurants on the`.map`. @@ -170,15 +169,15 @@ class City(meta.Base): ) # ... and adjust the size of the red dot on the `.map`. if n_orders >= 1000: - radius = 20 + radius = 20 # noqa:WPS220 elif n_orders >= 500: - radius = 15 + radius = 15 # noqa:WPS220 elif n_orders >= 100: - radius = 10 + radius = 10 # noqa:WPS220 elif n_orders >= 10: - radius = 5 + radius = 5 # noqa:WPS220 else: - radius = 1 + radius = 1 # noqa:WPS220 tooltip += f' | n_orders={n_orders}' # noqa:WPS336 diff --git a/src/urban_meal_delivery/db/customers.py b/src/urban_meal_delivery/db/customers.py index 384c003..fcbfd58 100644 --- a/src/urban_meal_delivery/db/customers.py +++ b/src/urban_meal_delivery/db/customers.py @@ -42,7 +42,7 @@ class Customer(meta.Base): """Shortcut to the `...city.map` object.""" return self.orders[0].pickup_address.city.map # noqa:WPS219 - def draw( # noqa:C901,WPS210 + def draw( # noqa:C901,WPS210,WPS231 self, restaurants: bool = True, order_counts: bool = False, # pragma: no cover ) -> folium.Map: """Draw all the customer's delivery addresses on the `...city.map`. @@ -90,15 +90,15 @@ class Customer(meta.Base): .count() ) if n_orders >= 25: - radius = 20 + radius = 20 # noqa:WPS220 elif n_orders >= 10: - radius = 15 + radius = 15 # noqa:WPS220 elif n_orders >= 5: - radius = 10 + radius = 10 # noqa:WPS220 elif n_orders > 1: - radius = 5 + radius = 5 # noqa:WPS220 else: - radius = 1 + radius = 1 # noqa:WPS220 address.draw( radius=radius, @@ -156,15 +156,15 @@ class Customer(meta.Base): .count() ) if n_orders >= 25: - radius = 20 + radius = 20 # noqa:WPS220 elif n_orders >= 10: - radius = 15 + radius = 15 # noqa:WPS220 elif n_orders >= 5: - radius = 10 + radius = 10 # noqa:WPS220 elif n_orders > 1: - radius = 5 + radius = 5 # noqa:WPS220 else: - radius = 1 + radius = 1 # noqa:WPS220 tooltip += f' | n_orders={n_orders}' # noqa:WPS336 diff --git a/src/urban_meal_delivery/db/orders.py b/src/urban_meal_delivery/db/orders.py index 4c4a2f7..1b1341b 100644 --- a/src/urban_meal_delivery/db/orders.py +++ b/src/urban_meal_delivery/db/orders.py @@ -105,8 +105,6 @@ class Order(meta.Base): # noqa:WPS214 onupdate='RESTRICT', ondelete='RESTRICT', ), - # Needed by a `ForeignKeyConstraint` in `ReplayedOrder`. - sa.UniqueConstraint('id', 'ad_hoc'), sa.CheckConstraint( """ (ad_hoc IS TRUE AND scheduled_delivery_at IS NULL) @@ -149,20 +147,6 @@ class Order(meta.Base): # noqa:WPS214 """, name='scheduled_orders_within_business_hours', ), - sa.CheckConstraint( - """ - ( - EXTRACT(MINUTES FROM scheduled_delivery_at)::INTEGER - % 15 = 0 - ) - AND - ( - EXTRACT(SECONDS FROM scheduled_delivery_at)::INTEGER - = 0 - ) - """, - name='scheduled_orders_must_be_at_quarters_of_an_hour', - ), sa.CheckConstraint( """ NOT ( @@ -272,7 +256,6 @@ class Order(meta.Base): # noqa:WPS214 'placed_at < courier_notified_at', 'placed_at < courier_accepted_at', 'placed_at < reached_pickup_at', - 'placed_at < pickup_at', 'placed_at < left_pickup_at', 'placed_at < reached_delivery_at', 'placed_at < delivery_at', @@ -285,6 +268,7 @@ class Order(meta.Base): # noqa:WPS214 'cancelled_at > pickup_at', 'cancelled_at > left_pickup_at', 'cancelled_at > reached_delivery_at', + 'cancelled_at > delivery_at', 'restaurant_notified_at < restaurant_confirmed_at', 'restaurant_notified_at < pickup_at', 'restaurant_confirmed_at < pickup_at', @@ -339,7 +323,6 @@ class Order(meta.Base): # noqa:WPS214 back_populates='orders_delivered', foreign_keys='[Order.delivery_address_id]', ) - replays = orm.relationship('ReplayedOrder', back_populates='actual') # Convenience properties diff --git a/src/urban_meal_delivery/db/pixels.py b/src/urban_meal_delivery/db/pixels.py index 327e21d..f063634 100644 --- a/src/urban_meal_delivery/db/pixels.py +++ b/src/urban_meal_delivery/db/pixels.py @@ -136,7 +136,7 @@ class Pixel(meta.Base): """Shortcut to the `.city.map` object.""" return self.grid.city.map - def draw( # noqa:C901,WPS210 + def draw( # noqa:C901,WPS210,WPS231 self, restaurants: bool = True, order_counts: bool = False, # pragma: no cover ) -> folium.Map: """Draw the pixel on the `.grid.city.map`. @@ -226,15 +226,15 @@ class Pixel(meta.Base): ) # ... and adjust the size of the red dot on the `.map`. if n_orders >= 1000: - radius = 20 + radius = 20 # noqa:WPS220 elif n_orders >= 500: - radius = 15 + radius = 15 # noqa:WPS220 elif n_orders >= 100: - radius = 10 + radius = 10 # noqa:WPS220 elif n_orders >= 10: - radius = 5 + radius = 5 # noqa:WPS220 else: - radius = 1 + radius = 1 # noqa:WPS220 tooltip += f' | n_orders={n_orders}' # noqa:WPS336 diff --git a/src/urban_meal_delivery/db/replay/__init__.py b/src/urban_meal_delivery/db/replay/__init__.py deleted file mode 100644 index 592d821..0000000 --- a/src/urban_meal_delivery/db/replay/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Provide the ORM models regarding the replay simulations.""" - -from urban_meal_delivery.db.replay.orders import ReplayedOrder -from urban_meal_delivery.db.replay.simulations import ReplaySimulation diff --git a/src/urban_meal_delivery/db/replay/orders.py b/src/urban_meal_delivery/db/replay/orders.py deleted file mode 100644 index ba8b2e2..0000000 --- a/src/urban_meal_delivery/db/replay/orders.py +++ /dev/null @@ -1,364 +0,0 @@ -"""Provide the ORM's `ReplayedOrder` model for the replay simulations.""" - -from __future__ import annotations - -import sqlalchemy as sa -from sqlalchemy import orm - -from urban_meal_delivery.db import meta - - -class ReplayedOrder(meta.Base): - """A simulated order by a `Customer` of the UDP.""" - - __tablename__ = 'replayed_orders' - - # Generic columns - simulation_id = sa.Column(sa.Integer, primary_key=True) - actual_order_id = sa.Column(sa.Integer, primary_key=True) - - # `Order`-type related columns - # `.ad_hoc` is part of a `ForeignKeyConstraint` to ensure the type - # of the `ReplayedOrder` (i.e., ad-hoc or scheduled) is in line - # with the `.actual` order. - ad_hoc = sa.Column(sa.Boolean, nullable=False) - placed_at = sa.Column(sa.DateTime, nullable=False) - # Depending on `.ad_hoc`, `.scheduled_delivery_at` is either filled in or not. - scheduled_delivery_at = sa.Column(sa.DateTime) - # If an order is cancelled in a simulation, - # some of the columns below do not apply any more. - cancelled_at = sa.Column(sa.DateTime) - - # Restaurant-related columns - estimated_prep_duration = sa.Column(sa.SmallInteger) - restaurant_notified_at = sa.Column(sa.DateTime) - restaurant_confirmed_at = sa.Column(sa.DateTime) - restaurant_ready_at = sa.Column(sa.DateTime) - - # Dispatch-related columns - dispatch_at = sa.Column(sa.DateTime) - first_estimated_delivery_at = sa.Column(sa.DateTime) - courier_id = sa.Column(sa.Integer, index=True) - courier_notified_at = sa.Column(sa.DateTime) - courier_accepted_at = sa.Column(sa.DateTime) - utilization = sa.Column(sa.SmallInteger) - - # Pickup-related columns - reached_pickup_at = sa.Column(sa.DateTime) - pickup_at = sa.Column(sa.DateTime) - left_pickup_at = sa.Column(sa.DateTime) - - # Delivery-related columns - reached_delivery_at = sa.Column(sa.DateTime) - delivery_at = sa.Column(sa.DateTime) - - # Constraints - __table_args__ = ( - sa.ForeignKeyConstraint( - ['simulation_id'], - ['replay_simulations.id'], - onupdate='RESTRICT', - ondelete='RESTRICT', - ), - sa.ForeignKeyConstraint( - ['actual_order_id', 'ad_hoc'], - ['orders.id', 'orders.ad_hoc'], - onupdate='RESTRICT', - ondelete='RESTRICT', - ), - sa.ForeignKeyConstraint( - ['courier_id'], ['couriers.id'], onupdate='RESTRICT', ondelete='RESTRICT', - ), - # The check constraints model some of the assumptions made in the simulations. - sa.CheckConstraint( - # Align the order's type, ad-hoc or scheduled, with the type of the - # `.actual` order and ensure the corresponding datetime column is set - # or not set. - """ - ( - ad_hoc IS TRUE - AND - scheduled_delivery_at IS NULL - ) - OR - ( - ad_hoc IS FALSE - AND - scheduled_delivery_at IS NOT NULL - ) - """, - name='either_ad_hoc_or_scheduled_order', - ), - sa.CheckConstraint( - """ - NOT ( - ad_hoc IS TRUE - AND ( - EXTRACT(HOUR FROM placed_at) < 11 - OR - EXTRACT(HOUR FROM placed_at) > 22 - ) - ) - """, - name='ad_hoc_orders_within_business_hours', - ), - sa.CheckConstraint( - """ - NOT ( - ad_hoc IS FALSE - AND ( - ( - EXTRACT(HOUR FROM scheduled_delivery_at) <= 11 - AND - NOT ( - EXTRACT(HOUR FROM scheduled_delivery_at) = 11 - AND - EXTRACT(MINUTE FROM scheduled_delivery_at) = 45 - ) - ) - OR - EXTRACT(HOUR FROM scheduled_delivery_at) > 22 - ) - ) - """, - name='scheduled_orders_within_business_hours', - ), - sa.CheckConstraint( - """ - ( - EXTRACT(MINUTES FROM scheduled_delivery_at)::INTEGER - % 15 = 0 - ) - AND - ( - EXTRACT(SECONDS FROM scheduled_delivery_at)::INTEGER - = 0 - ) - """, - name='scheduled_orders_must_be_at_quarters_of_an_hour', - ), - sa.CheckConstraint( - """ - NOT ( - EXTRACT(EPOCH FROM scheduled_delivery_at - placed_at) < 1800 - ) - """, - name='scheduled_orders_not_within_30_minutes', - ), - sa.CheckConstraint( - # A simulation must fill in at least all the timing and dispatch related - # columns, unless an order is cancelled in a run. - # Only `.estimated_prep_duration`, `.first_estimated_delivery_at`, and - # `.utilization` are truly optional. - # This constraint does allow missing columns for cancelled orders - # in any combination of the below columns. So, no precedence constraints - # are enforced (e.g., `.restaurant_notified_at` and `.restaurant_ready_at` - # could be set without `.restaurant_confirmed_at`). That is acceptable - # as cancelled orders are left out in the optimized KPIs. - """ - cancelled_at IS NOT NULL - OR - ( - restaurant_notified_at IS NOT NULL - AND - restaurant_confirmed_at IS NOT NULL - AND - restaurant_ready_at IS NOT NULL - AND - dispatch_at IS NOT NULL - AND - courier_id IS NOT NULL - AND - courier_notified_at IS NOT NULL - AND - courier_accepted_at IS NOT NULL - AND - reached_pickup_at IS NOT NULL - AND - pickup_at IS NOT NULL - AND - left_pickup_at IS NOT NULL - AND - reached_delivery_at IS NOT NULL - AND - delivery_at IS NOT NULL - ) - """, - name='orders_must_be_either_cancelled_or_fully_simulated', - ), - sa.CheckConstraint( - # 23,487 out of the 660,608 orders were cancelled. In 685 of them, the - # cancellation occurred after the pickup (as modeled by the `.pickup_at` - # and `.left_pickup_at` columns, where the latter may be set with the - # former being unset). As a simplification, we only allow cancellations - # before the pickup in the simulations. Then, the restaurant throws away - # the meal and the courier is free for other orders right away. Orders - # cancelled after pickup in the actual data are assumed to be delivered - # to the customer's door (and then still accepted or thrown away by the - # customer). So, the courier becomes free only after the "fake" delivery. - """ - NOT ( -- Only occurred in 528 of 660,608 orders in the actual data. - cancelled_at IS NOT NULL - AND - pickup_at IS NOT NULL - ) - AND - NOT ( -- Only occurred in 176 of 660,608 orders in the actual data. - cancelled_at IS NOT NULL - AND - left_pickup_at IS NOT NULL - ) - AND - NOT ( -- Never occurred in the actual data. - cancelled_at IS NOT NULL - AND - reached_delivery_at IS NOT NULL - ) - AND - NOT ( -- Never occurred in the actual data. - cancelled_at IS NOT NULL - AND - delivery_at IS NOT NULL - ) - """, - name='cancellations_may_only_occur_before_pickup', - ), - sa.CheckConstraint( - # The actual `.estimated_prep_duration` and `.estimated_prep_buffer` - # are modeled together as only one value. Therefore, the individual - # upper limits of 2700 and 900 are added and result in 3600. - '0 <= estimated_prep_duration AND estimated_prep_duration <= 3600', - name='estimated_prep_duration_between_0_and_3600', - ), - sa.CheckConstraint( - # We still round estimates of the preparation time to whole minutes. - # Other estimates are unlikely to change the simulation results in - # a significant way. - 'estimated_prep_duration % 60 = 0', - name='estimated_prep_duration_must_be_whole_minutes', - ), - sa.CheckConstraint( - # If a simulation's `.strategy` models `.utilization`, it must be - # realistic. The value can be deduced from the actual order's - # courier's `.capacity` and the actual order's `.utilization`. - '0 <= utilization AND utilization <= 100', - name='utilization_between_0_and_100', - ), - sa.CheckConstraint( - # The UDP is open from 11 am to 11 pm. So, before 11 am there is no - # activity. After 11 pm, the last orders of a day are all assumed to be - # dispatched before midnight. - """ - NOT ( - EXTRACT(HOUR FROM restaurant_notified_at) < 11 - OR - EXTRACT(HOUR FROM dispatch_at) < 11 - ) - """, - name='orders_dispatched_in_business_hours', - ), - *( - # The timestamps must be in a logically correct order. That is the same as - # in the actual `Order` model with an extra `restaurant_ready_at` column - # and the non-simulated columns removed. - sa.CheckConstraint( - constraint, name='ordered_timestamps_{index}'.format(index=index), - ) - for index, constraint in enumerate( - ( - 'placed_at < scheduled_delivery_at', - 'placed_at < cancelled_at', - 'placed_at < restaurant_notified_at', - 'placed_at < restaurant_confirmed_at', - 'placed_at < restaurant_ready_at', - 'placed_at < dispatch_at', - 'placed_at < first_estimated_delivery_at', - 'placed_at < courier_notified_at', - 'placed_at < courier_accepted_at', - 'placed_at < reached_pickup_at', - 'placed_at < pickup_at', - 'placed_at < left_pickup_at', - 'placed_at < reached_delivery_at', - 'placed_at < delivery_at', - 'cancelled_at > restaurant_notified_at', - 'cancelled_at > restaurant_confirmed_at', - 'cancelled_at > restaurant_ready_at', - 'cancelled_at > dispatch_at', - 'cancelled_at > courier_notified_at', - 'cancelled_at > courier_accepted_at', - 'cancelled_at > reached_pickup_at', - 'restaurant_notified_at < restaurant_confirmed_at', - 'restaurant_notified_at < restaurant_ready_at', - 'restaurant_notified_at < pickup_at', - 'restaurant_confirmed_at < restaurant_ready_at', - 'restaurant_confirmed_at < pickup_at', - 'restaurant_ready_at < pickup_at', - 'dispatch_at < first_estimated_delivery_at', - 'dispatch_at < courier_notified_at', - 'dispatch_at < courier_accepted_at', - 'dispatch_at < reached_pickup_at', - 'dispatch_at < pickup_at', - 'dispatch_at < left_pickup_at', - 'dispatch_at < reached_delivery_at', - 'dispatch_at < delivery_at', - 'courier_notified_at < courier_accepted_at', - 'courier_notified_at < reached_pickup_at', - 'courier_notified_at < pickup_at', - 'courier_notified_at < left_pickup_at', - 'courier_notified_at < reached_delivery_at', - 'courier_notified_at < delivery_at', - 'courier_accepted_at < reached_pickup_at', - 'courier_accepted_at < pickup_at', - 'courier_accepted_at < left_pickup_at', - 'courier_accepted_at < reached_delivery_at', - 'courier_accepted_at < delivery_at', - 'reached_pickup_at < pickup_at', - 'reached_pickup_at < left_pickup_at', - 'reached_pickup_at < reached_delivery_at', - 'reached_pickup_at < delivery_at', - 'pickup_at < left_pickup_at', - 'pickup_at < reached_delivery_at', - 'pickup_at < delivery_at', - 'left_pickup_at < reached_delivery_at', - 'left_pickup_at < delivery_at', - 'reached_delivery_at < delivery_at', - ), - ) - ), - ) - - # Relationships - simulation = orm.relationship('ReplaySimulation', back_populates='orders') - actual = orm.relationship('Order', back_populates='replays') - courier = orm.relationship('Courier') - - def __repr__(self) -> str: - """Non-literal text representation.""" - return '<{cls}(#{order_id})>'.format( - cls=self.__class__.__name__, order_id=self.actual.id, - ) - - # Convenience properties - - @property - def customer(self) -> db.Customer: - """`.customer` of the actual `Order`.""" - return self.actual.customer - - @property - def restaurant(self) -> db.Restaurant: - """`.restaurant` of the actual `Order`.""" - return self.actual.restaurant - - @property - def pickup_address(self) -> db.Address: - """`.pickup_address` of the actual `Order`.""" - return self.actual.pickup_address - - @property - def delivery_address(self) -> db.Address: - """`.delivery_address` of the actual `Order`.""" - return self.actual.delivery_address - - -from urban_meal_delivery import db # noqa:E402 isort:skip diff --git a/src/urban_meal_delivery/db/replay/simulations.py b/src/urban_meal_delivery/db/replay/simulations.py deleted file mode 100644 index 8e4802d..0000000 --- a/src/urban_meal_delivery/db/replay/simulations.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Provide the ORM's `ReplaySimulation` model for the replay simulations.""" - - -import sqlalchemy as sa -from sqlalchemy import orm - -from urban_meal_delivery.db import meta - - -class ReplaySimulation(meta.Base): - """A simulation of the UDP's routing given a strategy ... - - ... for the orders as they arrived in reality. - """ - - __tablename__ = 'replay_simulations' - - # Columns - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # noqa:WPS125 - city_id = sa.Column(sa.SmallInteger, nullable=False, index=True) - strategy = sa.Column(sa.Unicode(length=100), nullable=False, index=True) - # `.run` may be used as random seed. - run = sa.Column(sa.SmallInteger, nullable=False) - - # Constraints - __table_args__ = ( - sa.ForeignKeyConstraint( - ['city_id'], ['cities.id'], onupdate='RESTRICT', ondelete='RESTRICT', - ), - # A `.strategy` can be replayed several times per `.city`. - sa.UniqueConstraint('city_id', 'strategy', 'run'), - sa.CheckConstraint('run >= 0', name='run_is_a_count'), - ) - - # Relationships - city = orm.relationship('City', back_populates='replays') - orders = orm.relationship('ReplayedOrder', back_populates='simulation') - - def __repr__(self) -> str: - """Non-literal text representation.""" - return '<{cls}({strategy} #{run} in {city})>'.format( - cls=self.__class__.__name__, - strategy=self.strategy, - run=self.run, - city=self.city.name, - ) diff --git a/src/urban_meal_delivery/db/restaurants.py b/src/urban_meal_delivery/db/restaurants.py index fd838ab..31df7f8 100644 --- a/src/urban_meal_delivery/db/restaurants.py +++ b/src/urban_meal_delivery/db/restaurants.py @@ -69,7 +69,7 @@ class Restaurant(meta.Base): """Shortcut to the `.address.city.map` object.""" return self.address.city.map - def draw( + def draw( # noqa:WPS231 self, customers: bool = True, order_counts: bool = False, # pragma: no cover ) -> folium.Map: """Draw the restaurant on the `.address.city.map`. @@ -116,15 +116,15 @@ class Restaurant(meta.Base): .count() ) if n_orders >= 25: - radius = 20 + radius = 20 # noqa:WPS220 elif n_orders >= 10: - radius = 15 + radius = 15 # noqa:WPS220 elif n_orders >= 5: - radius = 10 + radius = 10 # noqa:WPS220 elif n_orders > 1: - radius = 5 + radius = 5 # noqa:WPS220 else: - radius = 1 + radius = 1 # noqa:WPS220 address.draw( radius=radius, diff --git a/src/urban_meal_delivery/forecasts/methods/decomposition.py b/src/urban_meal_delivery/forecasts/methods/decomposition.py index 3a96ac9..c71f6be 100644 --- a/src/urban_meal_delivery/forecasts/methods/decomposition.py +++ b/src/urban_meal_delivery/forecasts/methods/decomposition.py @@ -11,7 +11,7 @@ from rpy2 import robjects from rpy2.robjects import pandas2ri -def stl( # noqa:C901,WPS210,WPS211 +def stl( # noqa:C901,WPS210,WPS211,WPS231 time_series: pd.Series, *, frequency: int, diff --git a/tests/config.py b/tests/config.py index b99c8a9..2af0d60 100644 --- a/tests/config.py +++ b/tests/config.py @@ -7,8 +7,6 @@ from urban_meal_delivery import config # The day on which most test cases take place. YEAR, MONTH, DAY = 2016, 7, 1 -# Same day as a `tuple`. -DATE = (YEAR, MONTH, DAY) # The hour when most test cases take place. NOON = 12 diff --git a/tests/conftest.py b/tests/conftest.py index 673c251..d4b1b25 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -106,7 +106,6 @@ make_address = fake_data.make_address make_courier = fake_data.make_courier make_customer = fake_data.make_customer make_order = fake_data.make_order -make_replay_order = fake_data.make_replay_order make_restaurant = fake_data.make_restaurant address = fake_data.address @@ -115,10 +114,6 @@ city_data = fake_data.city_data courier = fake_data.courier customer = fake_data.customer order = fake_data.order -pre_order = fake_data.pre_order -replayed_order = fake_data.replayed_order restaurant = fake_data.restaurant grid = fake_data.grid pixel = fake_data.pixel -simulation = fake_data.simulation -simulation_data = fake_data.simulation_data diff --git a/tests/db/fake_data/__init__.py b/tests/db/fake_data/__init__.py index 0fc9433..80a7be3 100644 --- a/tests/db/fake_data/__init__.py +++ b/tests/db/fake_data/__init__.py @@ -4,7 +4,6 @@ from tests.db.fake_data.fixture_makers import make_address from tests.db.fake_data.fixture_makers import make_courier from tests.db.fake_data.fixture_makers import make_customer from tests.db.fake_data.fixture_makers import make_order -from tests.db.fake_data.fixture_makers import make_replay_order from tests.db.fake_data.fixture_makers import make_restaurant from tests.db.fake_data.static_fixtures import address from tests.db.fake_data.static_fixtures import city @@ -14,8 +13,4 @@ from tests.db.fake_data.static_fixtures import customer from tests.db.fake_data.static_fixtures import grid from tests.db.fake_data.static_fixtures import order from tests.db.fake_data.static_fixtures import pixel -from tests.db.fake_data.static_fixtures import pre_order -from tests.db.fake_data.static_fixtures import replayed_order from tests.db.fake_data.static_fixtures import restaurant -from tests.db.fake_data.static_fixtures import simulation -from tests.db.fake_data.static_fixtures import simulation_data diff --git a/tests/db/fake_data/factories/orders/ad_hoc.py b/tests/db/fake_data/factories.py similarity index 51% rename from tests/db/fake_data/factories/orders/ad_hoc.py rename to tests/db/fake_data/factories.py index 12fc611..46f2ff3 100644 --- a/tests/db/fake_data/factories/orders/ad_hoc.py +++ b/tests/db/fake_data/factories.py @@ -1,17 +1,114 @@ -"""Factory to create ad-hoc `Order` instances.""" +"""Factories to create instances for the SQLAlchemy models.""" import datetime as dt import random +import string import factory +import faker from factory import alchemy from geopy import distance from tests import config as test_config -from tests.db.fake_data.factories import utils from urban_meal_delivery import db +def _random_timespan( # noqa:WPS211 + *, + min_hours=0, + min_minutes=0, + min_seconds=0, + max_hours=0, + max_minutes=0, + max_seconds=0, +): + """A randomized `timedelta` object between the specified arguments.""" + total_min_seconds = min_hours * 3600 + min_minutes * 60 + min_seconds + total_max_seconds = max_hours * 3600 + max_minutes * 60 + max_seconds + return dt.timedelta(seconds=random.randint(total_min_seconds, total_max_seconds)) + + +def _early_in_the_morning(): + """A randomized `datetime` object early in the morning.""" + early = dt.datetime(test_config.YEAR, test_config.MONTH, test_config.DAY, 3, 0) + return early + _random_timespan(max_hours=2) + + +class AddressFactory(alchemy.SQLAlchemyModelFactory): + """Create instances of the `db.Address` model.""" + + class Meta: + model = db.Address + sqlalchemy_get_or_create = ('id',) + + id = factory.Sequence(lambda num: num) # noqa:WPS125 + created_at = factory.LazyFunction(_early_in_the_morning) + + # When testing, all addresses are considered primary ones. + # As non-primary addresses have no different behavior and + # the property is only kept from the original dataset for + # completeness sake, that is ok to do. + primary_id = factory.LazyAttribute(lambda obj: obj.id) + + # Mimic a Google Maps Place ID with just random characters. + place_id = factory.LazyFunction( + lambda: ''.join(random.choice(string.ascii_lowercase) for _ in range(20)), + ) + + # Place the addresses somewhere in downtown Paris. + latitude = factory.Faker('coordinate', center=48.855, radius=0.01) + longitude = factory.Faker('coordinate', center=2.34, radius=0.03) + # city -> set by the `make_address` fixture as there is only one `city` + city_name = 'Paris' + zip_code = factory.LazyFunction(lambda: random.randint(75001, 75020)) + street = factory.Faker('street_address', locale='fr_FR') + + +class CourierFactory(alchemy.SQLAlchemyModelFactory): + """Create instances of the `db.Courier` model.""" + + class Meta: + model = db.Courier + sqlalchemy_get_or_create = ('id',) + + id = factory.Sequence(lambda num: num) # noqa:WPS125 + created_at = factory.LazyFunction(_early_in_the_morning) + vehicle = 'bicycle' + historic_speed = 7.89 + capacity = 100 + pay_per_hour = 750 + pay_per_order = 200 + + +class CustomerFactory(alchemy.SQLAlchemyModelFactory): + """Create instances of the `db.Customer` model.""" + + class Meta: + model = db.Customer + sqlalchemy_get_or_create = ('id',) + + id = factory.Sequence(lambda num: num) # noqa:WPS125 + + +_restaurant_names = faker.Faker() + + +class RestaurantFactory(alchemy.SQLAlchemyModelFactory): + """Create instances of the `db.Restaurant` model.""" + + class Meta: + model = db.Restaurant + sqlalchemy_get_or_create = ('id',) + + id = factory.Sequence(lambda num: num) # noqa:WPS125 + created_at = factory.LazyFunction(_early_in_the_morning) + name = factory.LazyFunction( + lambda: f"{_restaurant_names.first_name()}'s Restaurant", + ) + # address -> set by the `make_restaurant` fixture as there is only one `city` + estimated_prep_duration = 1000 + + class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory): """Create instances of the `db.Order` model. @@ -48,7 +145,7 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory): cancel_=True, cancelled_at=factory.LazyAttribute( lambda obj: obj.dispatch_at - + utils.random_timespan( + + _random_timespan( max_seconds=(obj.pickup_at - obj.dispatch_at).total_seconds(), ), ), @@ -57,7 +154,7 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory): cancel_=True, cancelled_at=factory.LazyAttribute( lambda obj: obj.pickup_at - + utils.random_timespan( + + _random_timespan( max_seconds=(obj.delivery_at - obj.pickup_at).total_seconds(), ), ), @@ -70,8 +167,10 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory): # Attributes regarding the specialization of an `Order`: ad-hoc or scheduled. # Ad-hoc `Order`s are placed between 11.45 and 14.15. placed_at = factory.LazyFunction( - lambda: dt.datetime(*test_config.DATE, 11, 45) - + utils.random_timespan(max_hours=2, max_minutes=30), + lambda: dt.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, 11, 45, + ) + + _random_timespan(max_hours=2, max_minutes=30), ) ad_hoc = True scheduled_delivery_at = None @@ -95,19 +194,14 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory): # Restaurant-related attributes # restaurant -> set by the `make_order` fixture for better control restaurant_notified_at = factory.LazyAttribute( - lambda obj: obj.placed_at - + utils.random_timespan(min_seconds=30, max_seconds=90), - ) - restaurant_notified_at_corrected = factory.LazyAttribute( - lambda obj: False if obj.restaurant_notified_at else None, + lambda obj: obj.placed_at + _random_timespan(min_seconds=30, max_seconds=90), ) + restaurant_notified_at_corrected = False restaurant_confirmed_at = factory.LazyAttribute( lambda obj: obj.restaurant_notified_at - + utils.random_timespan(min_seconds=30, max_seconds=150), - ) - restaurant_confirmed_at_corrected = factory.LazyAttribute( - lambda obj: False if obj.restaurant_confirmed_at else None, + + _random_timespan(min_seconds=30, max_seconds=150), ) + restaurant_confirmed_at_corrected = False # Use the database defaults of the historic data. estimated_prep_duration = 900 estimated_prep_duration_corrected = False @@ -116,26 +210,19 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory): # Dispatch-related columns # courier -> set by the `make_order` fixture for better control dispatch_at = factory.LazyAttribute( - lambda obj: obj.placed_at - + utils.random_timespan(min_seconds=600, max_seconds=1080), - ) - dispatch_at_corrected = factory.LazyAttribute( - lambda obj: False if obj.dispatch_at else None, + lambda obj: obj.placed_at + _random_timespan(min_seconds=600, max_seconds=1080), ) + dispatch_at_corrected = False courier_notified_at = factory.LazyAttribute( lambda obj: obj.dispatch_at - + utils.random_timespan(min_seconds=100, max_seconds=140), - ) - courier_notified_at_corrected = factory.LazyAttribute( - lambda obj: False if obj.courier_notified_at else None, + + _random_timespan(min_seconds=100, max_seconds=140), ) + courier_notified_at_corrected = False courier_accepted_at = factory.LazyAttribute( lambda obj: obj.courier_notified_at - + utils.random_timespan(min_seconds=15, max_seconds=45), - ) - courier_accepted_at_corrected = factory.LazyAttribute( - lambda obj: False if obj.courier_accepted_at else None, + + _random_timespan(min_seconds=15, max_seconds=45), ) + courier_accepted_at_corrected = False # Sample a realistic utilization. utilization = factory.LazyFunction(lambda: random.choice([50, 60, 70, 80, 90, 100])) @@ -143,37 +230,30 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory): # pickup_address -> aligned with `restaurant.address` by the `make_order` fixture reached_pickup_at = factory.LazyAttribute( lambda obj: obj.courier_accepted_at - + utils.random_timespan(min_seconds=300, max_seconds=600), + + _random_timespan(min_seconds=300, max_seconds=600), ) pickup_at = factory.LazyAttribute( lambda obj: obj.reached_pickup_at - + utils.random_timespan(min_seconds=120, max_seconds=600), - ) - pickup_at_corrected = factory.LazyAttribute( - lambda obj: False if obj.pickup_at else None, + + _random_timespan(min_seconds=120, max_seconds=600), ) + pickup_at_corrected = False pickup_not_confirmed = False left_pickup_at = factory.LazyAttribute( - lambda obj: obj.pickup_at - + utils.random_timespan(min_seconds=60, max_seconds=180), - ) - left_pickup_at_corrected = factory.LazyAttribute( - lambda obj: False if obj.left_pickup_at else None, + lambda obj: obj.pickup_at + _random_timespan(min_seconds=60, max_seconds=180), ) + left_pickup_at_corrected = False # Delivery-related attributes # delivery_address -> set by the `make_order` fixture as there is only one `city` reached_delivery_at = factory.LazyAttribute( lambda obj: obj.left_pickup_at - + utils.random_timespan(min_seconds=240, max_seconds=480), + + _random_timespan(min_seconds=240, max_seconds=480), ) delivery_at = factory.LazyAttribute( lambda obj: obj.reached_delivery_at - + utils.random_timespan(min_seconds=240, max_seconds=660), - ) - delivery_at_corrected = factory.LazyAttribute( - lambda obj: False if obj.delivery_at else None, + + _random_timespan(min_seconds=240, max_seconds=660), ) + delivery_at_corrected = False delivery_not_confirmed = False _courier_waited_at_delivery = factory.LazyAttribute( lambda obj: False if obj.delivery_at else None, @@ -200,7 +280,9 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory): ) @factory.post_generation - def post(obj, _create, _extracted, **_kwargs): # noqa:B902,C901,N805 + def post( # noqa:C901,WPS231 + obj, create, extracted, **kwargs, # noqa:B902,N805 + ): """Discard timestamps that occur after cancellation.""" if obj.cancelled: if obj.cancelled_at <= obj.restaurant_notified_at: @@ -234,3 +316,63 @@ class AdHocOrderFactory(alchemy.SQLAlchemyModelFactory): obj.delivery_at_corrected = None obj.delivery_not_confirmed = None obj._courier_waited_at_delivery = None + + +class ScheduledOrderFactory(AdHocOrderFactory): + """Create instances of the `db.Order` model. + + This class takes care of the various timestamps for pre-orders. + + Pre-orders are placed long before the test day's lunch time starts. + All timestamps are relative to either `.dispatch_at` or `.restaurant_notified_at` + and calculated backwards from `.scheduled_delivery_at`. + """ + + # Attributes regarding the specialization of an `Order`: ad-hoc or scheduled. + placed_at = factory.LazyFunction(_early_in_the_morning) + ad_hoc = False + # Discrete `datetime` objects in the "core" lunch time are enough. + scheduled_delivery_at = factory.LazyFunction( + lambda: random.choice( + [ + dt.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 0, + ), + dt.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 15, + ), + dt.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 30, + ), + dt.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 45, + ), + dt.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, 13, 0, + ), + dt.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, 13, 15, + ), + dt.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, 13, 30, + ), + ], + ), + ) + scheduled_delivery_at_corrected = False + # Assume the `Order` is on time. + first_estimated_delivery_at = factory.LazyAttribute( + lambda obj: obj.scheduled_delivery_at, + ) + + # Restaurant-related attributes + restaurant_notified_at = factory.LazyAttribute( + lambda obj: obj.scheduled_delivery_at + - _random_timespan(min_minutes=45, max_minutes=50), + ) + + # Dispatch-related attributes + dispatch_at = factory.LazyAttribute( + lambda obj: obj.scheduled_delivery_at + - _random_timespan(min_minutes=40, max_minutes=45), + ) diff --git a/tests/db/fake_data/factories/__init__.py b/tests/db/fake_data/factories/__init__.py deleted file mode 100644 index 3d153ae..0000000 --- a/tests/db/fake_data/factories/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Factories to create instances for the SQLAlchemy models.""" - -from tests.db.fake_data.factories.addresses import AddressFactory -from tests.db.fake_data.factories.couriers import CourierFactory -from tests.db.fake_data.factories.customers import CustomerFactory -from tests.db.fake_data.factories.orders import AdHocOrderFactory -from tests.db.fake_data.factories.orders import ReplayedOrderFactory -from tests.db.fake_data.factories.orders import ScheduledOrderFactory -from tests.db.fake_data.factories.restaurants import RestaurantFactory diff --git a/tests/db/fake_data/factories/addresses.py b/tests/db/fake_data/factories/addresses.py deleted file mode 100644 index 39ac266..0000000 --- a/tests/db/fake_data/factories/addresses.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Factory to create `Address` instances.""" - -import random -import string - -import factory -from factory import alchemy - -from tests.db.fake_data.factories import utils -from urban_meal_delivery import db - - -class AddressFactory(alchemy.SQLAlchemyModelFactory): - """Create instances of the `db.Address` model.""" - - class Meta: - model = db.Address - sqlalchemy_get_or_create = ('id',) - - id = factory.Sequence(lambda num: num) # noqa:WPS125 - created_at = factory.LazyFunction(utils.early_in_the_morning) - - # When testing, all addresses are considered primary ones. - # As non-primary addresses have no different behavior and - # the property is only kept from the original dataset for - # completeness sake, that is ok to do. - primary_id = factory.LazyAttribute(lambda obj: obj.id) - - # Mimic a Google Maps Place ID with just random characters. - place_id = factory.LazyFunction( - lambda: ''.join(random.choice(string.ascii_lowercase) for _ in range(20)), - ) - - # Place the addresses somewhere in downtown Paris. - latitude = factory.Faker('coordinate', center=48.855, radius=0.01) - longitude = factory.Faker('coordinate', center=2.34, radius=0.03) - # city -> set by the `make_address` fixture as there is only one `city` - city_name = 'Paris' - zip_code = factory.LazyFunction(lambda: random.randint(75001, 75020)) - street = factory.Faker('street_address', locale='fr_FR') diff --git a/tests/db/fake_data/factories/couriers.py b/tests/db/fake_data/factories/couriers.py deleted file mode 100644 index 1f1e751..0000000 --- a/tests/db/fake_data/factories/couriers.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Factory to create `Courier` instances.""" - - -import factory -from factory import alchemy - -from tests.db.fake_data.factories import utils -from urban_meal_delivery import db - - -class CourierFactory(alchemy.SQLAlchemyModelFactory): - """Create instances of the `db.Courier` model.""" - - class Meta: - model = db.Courier - sqlalchemy_get_or_create = ('id',) - - id = factory.Sequence(lambda num: num) # noqa:WPS125 - created_at = factory.LazyFunction(utils.early_in_the_morning) - vehicle = 'bicycle' - historic_speed = 7.89 - capacity = 100 - pay_per_hour = 750 - pay_per_order = 200 diff --git a/tests/db/fake_data/factories/customers.py b/tests/db/fake_data/factories/customers.py deleted file mode 100644 index 55954e3..0000000 --- a/tests/db/fake_data/factories/customers.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Factory to create `Customers` instances.""" - -import factory -from factory import alchemy - -from urban_meal_delivery import db - - -class CustomerFactory(alchemy.SQLAlchemyModelFactory): - """Create instances of the `db.Customer` model.""" - - class Meta: - model = db.Customer - sqlalchemy_get_or_create = ('id',) - - id = factory.Sequence(lambda num: num) # noqa:WPS125 diff --git a/tests/db/fake_data/factories/orders/__init__.py b/tests/db/fake_data/factories/orders/__init__.py deleted file mode 100644 index d854b6c..0000000 --- a/tests/db/fake_data/factories/orders/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Factory to create `Order` instances.""" - -from tests.db.fake_data.factories.orders.ad_hoc import AdHocOrderFactory -from tests.db.fake_data.factories.orders.replayed import ReplayedOrderFactory -from tests.db.fake_data.factories.orders.scheduled import ScheduledOrderFactory diff --git a/tests/db/fake_data/factories/orders/replayed.py b/tests/db/fake_data/factories/orders/replayed.py deleted file mode 100644 index 857bc4c..0000000 --- a/tests/db/fake_data/factories/orders/replayed.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Factory to create `Order` and `ReplayedOrder` instances.""" - -import datetime as dt - -import factory -from factory import alchemy - -from tests.db.fake_data.factories import utils -from urban_meal_delivery import db - - -class ReplayedOrderFactory(alchemy.SQLAlchemyModelFactory): - """Create instances of the `db.ReplayedOrder` model. - - For simplicity, we assume the underlying `.order` to be an ad-hoc `Order`. - """ - - class Meta: - model = db.ReplayedOrder - sqlalchemy_get_or_create = ('simulation_id', 'order_id') - - # Generic columns - # simulation -> set by the `make_replay_order` fixture for better control - # actual (`Order`) -> set by the `make_replay_order` fixture for better control - - # `Order`-type related columns - ad_hoc = factory.LazyAttribute(lambda obj: obj.actual.ad_hoc) - placed_at = factory.LazyAttribute(lambda obj: obj.actual.placed_at) - scheduled_delivery_at = factory.LazyAttribute( - lambda obj: obj.actual.scheduled_delivery_at, - ) - cancelled_at = None - - # Restaurant-related columns - estimated_prep_duration = 1200 - restaurant_notified_at = factory.LazyAttribute( - lambda obj: obj.placed_at - + utils.random_timespan(min_seconds=1, max_seconds=30), - ) - restaurant_confirmed_at = factory.LazyAttribute( - lambda obj: obj.restaurant_notified_at - + utils.random_timespan(min_seconds=1, max_seconds=60), - ) - restaurant_ready_at = factory.LazyAttribute( - lambda obj: obj.restaurant_confirmed_at - + dt.timedelta(seconds=obj.estimated_prep_duration) - + utils.random_timespan(min_seconds=300, max_seconds=300), - ) - - # Dispatch-related columns - dispatch_at = factory.LazyAttribute( - lambda obj: obj.actual.placed_at - + utils.random_timespan(min_seconds=30, max_seconds=60), - ) - first_estimated_delivery_at = factory.LazyAttribute( - lambda obj: obj.restaurant_notified_at - + dt.timedelta(seconds=obj.estimated_prep_duration) - + dt.timedelta(minutes=10), - ) - # courier -> set by the `make_replay_order` fixture for better control - courier_notified_at = factory.LazyAttribute( - lambda obj: obj.dispatch_at - + utils.random_timespan(min_seconds=1, max_seconds=30), - ) - courier_accepted_at = factory.LazyAttribute( - lambda obj: obj.courier_notified_at - + utils.random_timespan(min_seconds=1, max_seconds=60), - ) - utilization = None - - # Pickup-related columns - reached_pickup_at = factory.LazyAttribute( - lambda obj: obj.restaurant_ready_at - + utils.random_timespan(min_seconds=1, max_seconds=60), - ) - pickup_at = factory.LazyAttribute( - lambda obj: obj.reached_pickup_at - + utils.random_timespan(min_seconds=30, max_seconds=60), - ) - left_pickup_at = factory.LazyAttribute( - lambda obj: obj.pickup_at - + utils.random_timespan(min_seconds=30, max_seconds=60), - ) - - # Delivery-related columns - reached_delivery_at = factory.LazyAttribute( - lambda obj: obj.left_pickup_at - + utils.random_timespan(min_minutes=5, max_minutes=10), - ) - delivery_at = factory.LazyAttribute( - lambda obj: obj.reached_delivery_at - + utils.random_timespan(min_seconds=30, max_seconds=60), - ) - - @factory.post_generation - def post(obj, _create, _extracted, **_kwargs): # noqa:B902,C901,N805 - """Discard timestamps that occur after cancellation.""" - if obj.cancelled_at: - if obj.cancelled_at <= obj.restaurant_notified_at: - obj.restaurant_notified_at = None - if obj.cancelled_at <= obj.restaurant_confirmed_at: - obj.restaurant_confirmed_at = None - if obj.cancelled_at <= obj.restaurant_ready_at: - obj.restaurant_ready_at = None - if obj.cancelled_at <= obj.dispatch_at: - obj.dispatch_at = None - if obj.cancelled_at <= obj.courier_notified_at: - obj.courier_notified_at = None - if obj.cancelled_at <= obj.courier_accepted_at: - obj.courier_accepted_at = None - if obj.cancelled_at <= obj.reached_pickup_at: - obj.reached_pickup_at = None - if obj.cancelled_at <= obj.pickup_at: - obj.pickup_at = None - if obj.cancelled_at <= obj.left_pickup_at: - obj.left_pickup_at = None - if obj.cancelled_at <= obj.reached_delivery_at: - obj.reached_delivery_at = None - if obj.cancelled_at <= obj.delivery_at: - obj.delivery_at = None diff --git a/tests/db/fake_data/factories/orders/scheduled.py b/tests/db/fake_data/factories/orders/scheduled.py deleted file mode 100644 index 27c1798..0000000 --- a/tests/db/fake_data/factories/orders/scheduled.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Factory to create scheduled `Order` instances.""" - -import datetime as dt -import random - -import factory - -from tests import config as test_config -from tests.db.fake_data.factories import utils -from tests.db.fake_data.factories.orders import ad_hoc - - -class ScheduledOrderFactory(ad_hoc.AdHocOrderFactory): - """Create instances of the `db.Order` model. - - This class takes care of the various timestamps for pre-orders. - - Pre-orders are placed long before the test day's lunch time starts. - All timestamps are relative to either `.dispatch_at` or `.restaurant_notified_at` - and calculated backwards from `.scheduled_delivery_at`. - """ - - # Attributes regarding the specialization of an `Order`: ad-hoc or scheduled. - placed_at = factory.LazyFunction(utils.early_in_the_morning) - ad_hoc = False - # Discrete `datetime` objects in the "core" lunch time are enough. - scheduled_delivery_at = factory.LazyFunction( - lambda: random.choice( - [ - dt.datetime(*test_config.DATE, 12, 0), - dt.datetime(*test_config.DATE, 12, 15), - dt.datetime(*test_config.DATE, 12, 30), - dt.datetime(*test_config.DATE, 12, 45), - dt.datetime(*test_config.DATE, 13, 0), - dt.datetime(*test_config.DATE, 13, 15), - dt.datetime(*test_config.DATE, 13, 30), - ], - ), - ) - scheduled_delivery_at_corrected = False - # Assume the `Order` is on time. - first_estimated_delivery_at = factory.LazyAttribute( - lambda obj: obj.scheduled_delivery_at, - ) - - # Restaurant-related attributes - restaurant_notified_at = factory.LazyAttribute( - lambda obj: obj.scheduled_delivery_at - - utils.random_timespan(min_minutes=45, max_minutes=50), - ) - - # Dispatch-related attributes - dispatch_at = factory.LazyAttribute( - lambda obj: obj.scheduled_delivery_at - - utils.random_timespan(min_minutes=40, max_minutes=45), - ) diff --git a/tests/db/fake_data/factories/restaurants.py b/tests/db/fake_data/factories/restaurants.py deleted file mode 100644 index add3976..0000000 --- a/tests/db/fake_data/factories/restaurants.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Factory to create `Restaurant` instances.""" - -import factory -import faker -from factory import alchemy - -from tests.db.fake_data.factories import utils -from urban_meal_delivery import db - - -_restaurant_names = faker.Faker() - - -class RestaurantFactory(alchemy.SQLAlchemyModelFactory): - """Create instances of the `db.Restaurant` model.""" - - class Meta: - model = db.Restaurant - sqlalchemy_get_or_create = ('id',) - - id = factory.Sequence(lambda num: num) # noqa:WPS125 - created_at = factory.LazyFunction(utils.early_in_the_morning) - name = factory.LazyFunction( - lambda: f"{_restaurant_names.first_name()}'s Restaurant", - ) - # address -> set by the `make_restaurant` fixture as there is only one `city` - estimated_prep_duration = 1000 diff --git a/tests/db/fake_data/factories/utils.py b/tests/db/fake_data/factories/utils.py deleted file mode 100644 index 28142a9..0000000 --- a/tests/db/fake_data/factories/utils.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Utilities used in all `*Factory` classes.""" - -import datetime as dt -import random - -from tests import config as test_config - - -def random_timespan( # noqa:WPS211 - *, - min_hours=0, - min_minutes=0, - min_seconds=0, - max_hours=0, - max_minutes=0, - max_seconds=0, -): - """A randomized `timedelta` object between the specified arguments.""" - total_min_seconds = min_hours * 3600 + min_minutes * 60 + min_seconds - total_max_seconds = max_hours * 3600 + max_minutes * 60 + max_seconds - return dt.timedelta(seconds=random.randint(total_min_seconds, total_max_seconds)) - - -def early_in_the_morning(): - """A randomized `datetime` object early in the morning.""" - early = dt.datetime(*test_config.DATE, 3, 0) - return early + random_timespan(max_hours=2) diff --git a/tests/db/fake_data/fixture_makers.py b/tests/db/fake_data/fixture_makers.py index 8f6f891..9a5419b 100644 --- a/tests/db/fake_data/fixture_makers.py +++ b/tests/db/fake_data/fixture_makers.py @@ -103,41 +103,3 @@ def make_order(make_address, make_courier, make_customer, make_restaurant): ) return func - - -@pytest.fixture -def make_replay_order(make_order, simulation): - """Replaces `ReplayedOrderFactory.build()`: Create a `ReplayedOrder`.""" - # Reset the identifiers before every test. - factories.ReplayedOrderFactory.reset_sequence(1) - - def func(scheduled=False, order=None, **kwargs): - """Create a new `ReplayedOrder` object. - - Each `ReplayOrder` is made by a new `Customer` with a unique `Address`. - - Args: - scheduled: if an `Order` is a pre-order - order: the `Order` that is replayed - kwargs: keyword arguments forwarded to the `ReplayedOrderFactory` - - Returns: - order - - Raises: - RuntimeError: if `scheduled=True` is passed in - together with an ad-hoc `order` - """ - if scheduled: - if order and order.ad_hoc is False: - raise RuntimeError('`order` must be scheduled') - elif order is None: - order = make_order(scheduled=True) - elif order is None: - order = make_order() - - return factories.ReplayedOrderFactory.build( - simulation=simulation, actual=order, courier=order.courier, **kwargs, - ) - - return func diff --git a/tests/db/fake_data/static_fixtures.py b/tests/db/fake_data/static_fixtures.py index 4be94c5..60d4181 100644 --- a/tests/db/fake_data/static_fixtures.py +++ b/tests/db/fake_data/static_fixtures.py @@ -54,22 +54,10 @@ def restaurant(address, make_restaurant): @pytest.fixture def order(make_order, restaurant): - """An ad-hoc `Order` object for the `restaurant`.""" + """An `Order` object for the `restaurant`.""" return make_order(restaurant=restaurant) -@pytest.fixture -def pre_order(make_order, restaurant): - """A scheduled `Order` object for the `restaurant`.""" - return make_order(restaurant=restaurant, scheduled=True) - - -@pytest.fixture -def replayed_order(make_replay_order, order): - """A `ReplayedOrder` object for the `restaurant`.""" - return make_replay_order(order=order) - - @pytest.fixture def grid(city): """A `Grid` with a pixel area of 1 square kilometer.""" @@ -80,20 +68,3 @@ def grid(city): def pixel(grid): """The `Pixel` in the lower-left corner of the `grid`.""" return db.Pixel(id=1, grid=grid, n_x=0, n_y=0) - - -@pytest.fixture -def simulation_data(city): - """The data for the one and only `ReplaySimulation` object as a `dict`.""" - return { - 'id': 1, - 'city': city, - 'strategy': 'best_possible_routing', - 'run': 0, - } - - -@pytest.fixture -def simulation(simulation_data): - """The one and only `ReplaySimulation` object.""" - return db.ReplaySimulation(**simulation_data) diff --git a/tests/db/replay/__init__.py b/tests/db/replay/__init__.py deleted file mode 100644 index cd9da80..0000000 --- a/tests/db/replay/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test the replay simulation models in the ORM layer.""" diff --git a/tests/db/replay/test_orders.py b/tests/db/replay/test_orders.py deleted file mode 100644 index 0ad62eb..0000000 --- a/tests/db/replay/test_orders.py +++ /dev/null @@ -1,585 +0,0 @@ -"""Test the ORM's `ReplayedOrder` model.""" - -import datetime as dt - -import pytest -import sqlalchemy as sqla -from sqlalchemy import exc as sa_exc - -from tests import config as test_config -from urban_meal_delivery import db - - -class TestSpecialMethods: - """Test special methods in `ReplayedOrder`.""" - - def test_create_order(self, replayed_order): - """Test instantiation of a new `ReplayedOrder` object.""" - assert replayed_order is not None - - def test_text_representation(self, replayed_order): - """`ReplayedOrder` has a non-literal text representation.""" - result = repr(replayed_order) - - assert result == f'' - - -@pytest.mark.db -@pytest.mark.no_cover -class TestConstraints: - """Test the database constraints defined in `ReplayedOrder`.""" - - def test_insert_into_into_database(self, db_session, replayed_order): - """Insert an instance into the (empty) database.""" - assert db_session.query(db.ReplayedOrder).count() == 0 - - db_session.add(replayed_order) - db_session.commit() - - assert db_session.query(db.ReplayedOrder).count() == 1 - - def test_delete_a_referenced_simulation(self, db_session, replayed_order): - """Remove a record that is referenced with a FK.""" - db_session.add(replayed_order) - db_session.commit() - - # Must delete without ORM as otherwise an UPDATE statement is emitted. - stmt = sqla.delete(db.ReplaySimulation).where( - db.ReplaySimulation.id == replayed_order.simulation.id, - ) - - with pytest.raises( - sa_exc.IntegrityError, match='fk_replayed_orders_to_replay_simulations', - ): - db_session.execute(stmt) - - def test_delete_a_referenced_actual_order(self, db_session, replayed_order): - """Remove a record that is referenced with a FK.""" - db_session.add(replayed_order) - db_session.commit() - - # Must delete without ORM as otherwise an UPDATE statement is emitted. - stmt = sqla.delete(db.Order).where(db.Order.id == replayed_order.actual.id) - - with pytest.raises(sa_exc.IntegrityError, match='fk_replayed_orders_to_orders'): - db_session.execute(stmt) - - def test_delete_a_referenced_courier( - self, db_session, replayed_order, make_courier, - ): - """Remove a record that is referenced with a FK.""" - # Need a second courier, one that is - # not associated with the `.actual` order. - replayed_order.courier = make_courier() - - db_session.add(replayed_order) - db_session.commit() - - # Must delete without ORM as otherwise an UPDATE statement is emitted. - stmt = sqla.delete(db.Courier).where(db.Courier.id == replayed_order.courier.id) - - with pytest.raises( - sa_exc.IntegrityError, match='fk_replayed_orders_to_couriers', - ): - db_session.execute(stmt) - - def test_ad_hoc_order_with_scheduled_delivery_at(self, db_session, replayed_order): - """Insert an instance with invalid data.""" - assert replayed_order.ad_hoc is True - - replayed_order.scheduled_delivery_at = dt.datetime(*test_config.DATE, 18, 0) - - db_session.add(replayed_order) - - with pytest.raises( - sa_exc.IntegrityError, match='either_ad_hoc_or_scheduled_order', - ): - db_session.commit() - - def test_scheduled_order_without_scheduled_delivery_at( - self, db_session, make_replay_order, - ): - """Insert an instance with invalid data.""" - replay_order = make_replay_order(scheduled=True) - - assert replay_order.ad_hoc is False - - replay_order.scheduled_delivery_at = None - - db_session.add(replay_order) - - with pytest.raises( - sa_exc.IntegrityError, match='either_ad_hoc_or_scheduled_order', - ): - db_session.commit() - - def test_ad_hoc_order_too_early(self, db_session, make_replay_order): - """Insert an instance with invalid data.""" - replay_order = make_replay_order( - placed_at=dt.datetime(*test_config.DATE, 10, 0), - ) - - db_session.add(replay_order) - - with pytest.raises( - sa_exc.IntegrityError, match='ad_hoc_orders_within_business_hours', - ): - db_session.commit() - - def test_ad_hoc_order_too_late(self, db_session, make_replay_order): - """Insert an instance with invalid data.""" - replay_order = make_replay_order( - placed_at=dt.datetime(*test_config.DATE, 23, 0), - ) - - db_session.add(replay_order) - - with pytest.raises( - sa_exc.IntegrityError, match='ad_hoc_orders_within_business_hours', - ): - db_session.commit() - - def test_scheduled_order_way_too_early(self, db_session, make_replay_order): - """Insert an instance with invalid data.""" - scheduled_delivery_at = dt.datetime(*test_config.DATE, 10, 0) - replay_pre_order = make_replay_order( - scheduled=True, - placed_at=scheduled_delivery_at - dt.timedelta(hours=12), - scheduled_delivery_at=scheduled_delivery_at, - restaurant_notified_at=scheduled_delivery_at + dt.timedelta(hours=1), - dispatch_at=scheduled_delivery_at + dt.timedelta(hours=1), - ) - - db_session.add(replay_pre_order) - - with pytest.raises( - sa_exc.IntegrityError, match='scheduled_orders_within_business_hours', - ): - db_session.commit() - - def test_scheduled_order_a_bit_too_early(self, db_session, make_replay_order): - """Insert an instance with invalid data.""" - scheduled_delivery_at = dt.datetime(*test_config.DATE, 11, 30) - replay_pre_order = make_replay_order( - scheduled=True, - placed_at=scheduled_delivery_at - dt.timedelta(hours=14), - scheduled_delivery_at=scheduled_delivery_at, - restaurant_notified_at=scheduled_delivery_at + dt.timedelta(hours=1), - dispatch_at=scheduled_delivery_at + dt.timedelta(hours=1), - ) - - db_session.add(replay_pre_order) - - with pytest.raises( - sa_exc.IntegrityError, match='scheduled_orders_within_business_hours', - ): - db_session.commit() - - def test_scheduled_order_not_too_early(self, db_session, make_replay_order): - """Insert an instance with invalid data. - - 11.45 is the only time outside noon to 11 pm when a scheduled order is allowed. - """ - scheduled_delivery_at = dt.datetime(*test_config.DATE, 11, 45) - replay_pre_order = make_replay_order( - scheduled=True, - placed_at=scheduled_delivery_at - dt.timedelta(hours=14), - scheduled_delivery_at=scheduled_delivery_at, - restaurant_notified_at=scheduled_delivery_at + dt.timedelta(hours=1), - dispatch_at=scheduled_delivery_at + dt.timedelta(hours=1), - ) - - assert db_session.query(db.Order).count() == 0 - - db_session.add(replay_pre_order) - db_session.commit() - - assert db_session.query(db.Order).count() == 1 - - def test_scheduled_order_too_late(self, db_session, make_replay_order): - """Insert an instance with invalid data.""" - scheduled_delivery_at = dt.datetime(*test_config.DATE, 23, 0) - replay_pre_order = make_replay_order( - scheduled=True, - placed_at=scheduled_delivery_at - dt.timedelta(hours=10), - scheduled_delivery_at=scheduled_delivery_at, - restaurant_notified_at=scheduled_delivery_at + dt.timedelta(minutes=30), - dispatch_at=scheduled_delivery_at + dt.timedelta(minutes=30), - ) - - db_session.add(replay_pre_order) - - with pytest.raises( - sa_exc.IntegrityError, match='scheduled_orders_within_business_hours', - ): - db_session.commit() - - @pytest.mark.parametrize('minute', [min_ for min_ in range(60) if min_ % 15 != 0]) - def test_scheduled_order_at_non_quarter_of_an_hour( - self, db_session, make_replay_order, minute, - ): - """Insert an instance with invalid data.""" - scheduled_delivery_at = dt.datetime( # `minute` is not 0, 15, 30, or 45 - *test_config.DATE, test_config.NOON, minute, - ) - replay_pre_order = make_replay_order( - scheduled=True, - placed_at=scheduled_delivery_at - dt.timedelta(hours=10), - scheduled_delivery_at=scheduled_delivery_at, - restaurant_notified_at=scheduled_delivery_at + dt.timedelta(minutes=30), - dispatch_at=scheduled_delivery_at + dt.timedelta(minutes=30), - ) - - db_session.add(replay_pre_order) - - with pytest.raises( - sa_exc.IntegrityError, - match='scheduled_orders_must_be_at_quart', # constraint name too long - ): - db_session.commit() - - @pytest.mark.parametrize('second', list(range(1, 60))) - def test_scheduled_order_at_non_quarter_of_an_hour_by_seconds( - self, db_session, make_replay_order, second, - ): - """Insert an instance with invalid data.""" - scheduled_delivery_at = dt.datetime( - *test_config.DATE, test_config.NOON, 0, second, - ) - replay_pre_order = make_replay_order( - scheduled=True, - placed_at=scheduled_delivery_at - dt.timedelta(hours=10), - scheduled_delivery_at=scheduled_delivery_at, - restaurant_notified_at=scheduled_delivery_at + dt.timedelta(minutes=30), - dispatch_at=scheduled_delivery_at + dt.timedelta(minutes=30), - ) - - db_session.add(replay_pre_order) - - with pytest.raises( - sa_exc.IntegrityError, - match='scheduled_orders_must_be_at_quart', # constraint name too long - ): - db_session.commit() - - def test_scheduled_order_too_soon(self, db_session, make_replay_order): - """Insert an instance with invalid data. - - Scheduled orders must be at least 30 minutes into the future. - """ - # Create an ad-hoc order first and then make that a scheduled order. - # This way, it is less work to keep the timestamps consistent. - replay_pre_order = make_replay_order(scheduled=False) - - # Make the `scheduled_delivery_at` the quarter of an hour - # following the next quarter of an hour (i.e., the timestamp - # is between 15 and 30 minutes into the future). - replay_pre_order.ad_hoc = False - replay_pre_order.actual.ad_hoc = False - minutes_to_next_quarter = 15 - (replay_pre_order.placed_at.minute % 15) - scheduled_delivery_at = ( - # `.placed_at` may have non-0 seconds. - replay_pre_order.placed_at.replace(second=0) - + dt.timedelta(minutes=(minutes_to_next_quarter + 15)) - ) - replay_pre_order.scheduled_delivery_at = scheduled_delivery_at - replay_pre_order.actual.scheduled_delivery_at = scheduled_delivery_at - replay_pre_order.actual.scheduled_delivery_at_corrected = False - - db_session.add(replay_pre_order) - - with pytest.raises( - sa_exc.IntegrityError, match='scheduled_orders_not_within_30_minutes', - ): - db_session.commit() - - @pytest.mark.parametrize( - 'column', - [ - 'restaurant_notified_at', - 'restaurant_confirmed_at', - 'restaurant_ready_at', - 'dispatch_at', - 'courier', # not `.courier_id` - 'courier_notified_at', - 'courier_accepted_at', - 'reached_pickup_at', - 'pickup_at', - 'left_pickup_at', - 'reached_delivery_at', - 'delivery_at', - ], - ) - def test_not_fully_simulated_order(self, db_session, replayed_order, column): - """Insert an instance with invalid data.""" - assert replayed_order.cancelled_at is None - - setattr(replayed_order, column, None) - - db_session.add(replayed_order) - - with pytest.raises( - sa_exc.IntegrityError, - match='either_cancelled_o', # constraint name too long - ): - db_session.commit() - - @pytest.mark.parametrize( - 'column', - [ - 'restaurant_notified_at', - 'restaurant_confirmed_at', - 'restaurant_ready_at', - 'dispatch_at', - 'courier_id', - 'courier_notified_at', - 'courier_accepted_at', - 'reached_pickup_at', - ], - ) - def test_simulated_cancellation(self, db_session, replayed_order, column): - """Insert an instance with invalid data. - - Cancelled orders may have missing timestamps - """ - replayed_order.cancelled_at = replayed_order.pickup_at - - replayed_order.pickup_at = None - replayed_order.left_pickup_at = None - replayed_order.reached_delivery_at = None - replayed_order.delivery_at = None - - setattr(replayed_order, column, None) - - db_session.add(replayed_order) - db_session.commit() - - @pytest.mark.parametrize( - 'column', ['pickup_at', 'left_pickup_at', 'reached_delivery_at', 'delivery_at'], - ) - def test_no_simulated_cancellation_after_pickup( - self, db_session, replayed_order, column, - ): - """Insert an instance with invalid data.""" - # Setting `.cancelled_at` to the end of a day - # ensures the timestamps are logically ok. - replayed_order.cancelled_at = dt.datetime(*test_config.DATE, 23) - - # Set all timestamps after `.reached_pickup_at` to NULL - # except the one under test. - for unset_column in ( - 'pickup_at', - 'left_pickup_at', - 'reached_delivery_at', - 'delivery_at', - ): - if unset_column != column: - setattr(replayed_order, unset_column, None) - - db_session.add(replayed_order) - - with pytest.raises( - sa_exc.IntegrityError, - match='cancellations_may_only_occur_befo', # constraint name too long - ): - db_session.commit() - - @pytest.mark.parametrize('duration', [-1, 3601]) - def test_estimated_prep_duration_out_of_range( - self, db_session, replayed_order, duration, - ): - """Insert an instance with invalid data.""" - replayed_order.estimated_prep_duration = duration - db_session.add(replayed_order) - - with pytest.raises( - sa_exc.IntegrityError, match='between_0', # constraint name too long - ): - db_session.commit() - - @pytest.mark.parametrize('duration', [1, 59, 119, 3599]) - def test_estimated_prep_duration_not_whole_minute( - self, db_session, replayed_order, duration, - ): - """Insert an instance with invalid data.""" - replayed_order.estimated_prep_duration = duration - db_session.add(replayed_order) - - with pytest.raises( - sa_exc.IntegrityError, match='must_be_w', # constraint name too long - ): - db_session.commit() - - @pytest.mark.parametrize('utilization', [-1, 101]) - def test_utilization_out_of_range(self, db_session, replayed_order, utilization): - """Insert an instance with invalid data.""" - replayed_order.utilization = utilization - db_session.add(replayed_order) - - with pytest.raises( - sa_exc.IntegrityError, match='between_0_and_100', - ): - db_session.commit() - - @pytest.mark.parametrize('column', ['restaurant_notified_at', 'dispatch_at']) - @pytest.mark.parametrize('hour', [0, 10]) - def test_order_dispatched_in_non_business_hour( - self, db_session, replayed_order, column, hour, - ): - """Insert an instance with invalid data.""" - orig_timestamp = getattr(replayed_order, column) - new_timestamp = orig_timestamp.replace(hour=hour) - - replayed_order.placed_at = new_timestamp - dt.timedelta(minutes=1) - setattr(replayed_order, column, new_timestamp) - - db_session.add(replayed_order) - - with pytest.raises( - sa_exc.IntegrityError, match='business_hours', - ): - db_session.commit() - - @pytest.mark.parametrize( - 'comparison', - [ - 'placed_at < restaurant_notified_at', - 'placed_at < restaurant_confirmed_at', - 'placed_at < restaurant_ready_at', - 'placed_at < dispatch_at', - 'placed_at < first_estimated_delivery_at', - 'placed_at < courier_notified_at', - 'placed_at < courier_accepted_at', - 'placed_at < reached_pickup_at', - 'placed_at < pickup_at', - 'placed_at < left_pickup_at', - 'placed_at < reached_delivery_at', - 'placed_at < delivery_at', - 'restaurant_notified_at < restaurant_confirmed_at', - 'restaurant_notified_at < restaurant_ready_at', - 'restaurant_notified_at < pickup_at', - 'restaurant_confirmed_at < restaurant_ready_at', - 'restaurant_confirmed_at < pickup_at', - 'restaurant_ready_at < pickup_at', - 'dispatch_at < first_estimated_delivery_at', - 'dispatch_at < courier_notified_at', - 'dispatch_at < courier_accepted_at', - 'dispatch_at < reached_pickup_at', - 'dispatch_at < pickup_at', - 'dispatch_at < left_pickup_at', - 'dispatch_at < reached_delivery_at', - 'dispatch_at < delivery_at', - 'courier_notified_at < courier_accepted_at', - 'courier_notified_at < reached_pickup_at', - 'courier_notified_at < pickup_at', - 'courier_notified_at < left_pickup_at', - 'courier_notified_at < reached_delivery_at', - 'courier_notified_at < delivery_at', - 'courier_accepted_at < reached_pickup_at', - 'courier_accepted_at < pickup_at', - 'courier_accepted_at < left_pickup_at', - 'courier_accepted_at < reached_delivery_at', - 'courier_accepted_at < delivery_at', - 'reached_pickup_at < pickup_at', - 'reached_pickup_at < left_pickup_at', - 'reached_pickup_at < reached_delivery_at', - 'reached_pickup_at < delivery_at', - 'pickup_at < left_pickup_at', - 'pickup_at < reached_delivery_at', - 'pickup_at < delivery_at', - 'left_pickup_at < reached_delivery_at', - 'left_pickup_at < delivery_at', - 'reached_delivery_at < delivery_at', - ], - ) - def test_timestamps_unordered( - self, db_session, replayed_order, comparison, - ): - """Insert an instance with invalid data. - - There are two special cases for this test case below, - where other attributes on `replayed_order` must be unset. - """ - smaller, bigger = comparison.split(' < ') - - assert smaller is not None - - violating_timestamp = getattr(replayed_order, smaller) - dt.timedelta(seconds=1) - setattr(replayed_order, bigger, violating_timestamp) - - db_session.add(replayed_order) - - with pytest.raises(sa_exc.IntegrityError, match='ordered_timestamps'): - db_session.commit() - - def test_timestamps_unordered_scheduled(self, db_session, make_replay_order): - """Insert an instance with invalid data. - - This is one of two special cases. See the generic case above. - """ - replayed_pre_order = make_replay_order(scheduled=True) - # As we subtract 1 second in the generic case, - # choose one second after a quarter of an hour. - replayed_pre_order.placed_at = dt.datetime(*test_config.DATE, 11, 45, 1) - self.test_timestamps_unordered( - db_session, replayed_pre_order, 'placed_at < scheduled_delivery_at', - ) - - @pytest.mark.parametrize( - 'comparison', - [ - 'placed_at < cancelled_at', - 'restaurant_notified_at < cancelled_at', - 'restaurant_confirmed_at < cancelled_at', - 'restaurant_ready_at < cancelled_at', - 'dispatch_at < cancelled_at', - 'courier_notified_at < cancelled_at', - 'courier_accepted_at < cancelled_at', - 'reached_pickup_at < cancelled_at', - ], - ) - def test_timestamps_unordered_cancelled( - self, db_session, replayed_order, comparison, - ): - """Insert an instance with invalid data. - - This is one of two special cases. See the generic case above. - """ - replayed_order.pickup_at = None - replayed_order.left_pickup_at = None - replayed_order.reached_delivery_at = None - replayed_order.delivery_at = None - - self.test_timestamps_unordered(db_session, replayed_order, comparison) - - -class TestProperties: - """Test properties in `ReplayedOrder`. - - The `replayed_order` fixture uses the defaults specified in - `factories.ReplayedOrderFactory` and provided by the `make_replay_order` fixture. - """ - - def test_has_customer(self, replayed_order): - """Test `ReplayedOrder.customer` property.""" - result = replayed_order.customer - - assert result is replayed_order.actual.customer - - def test_has_restaurant(self, replayed_order): - """Test `ReplayedOrder.restaurant` property.""" - result = replayed_order.restaurant - - assert result is replayed_order.actual.restaurant - - def test_has_pickup_address(self, replayed_order): - """Test `ReplayedOrder.pickup_address` property.""" - result = replayed_order.pickup_address - - assert result is replayed_order.actual.pickup_address - - def test_has_delivery_address(self, replayed_order): - """Test `ReplayedOrder.delivery_address` property.""" - result = replayed_order.delivery_address - - assert result is replayed_order.actual.delivery_address diff --git a/tests/db/replay/test_simulations.py b/tests/db/replay/test_simulations.py deleted file mode 100644 index 5d2ce08..0000000 --- a/tests/db/replay/test_simulations.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Test the ORM's `ReplaySimulation` model.""" - - -class TestSpecialMethods: - """Test special methods in `ReplaySimulation`.""" - - def test_create_simulation(self, simulation): - """Test instantiation of a new `ReplaySimulation` object.""" - assert simulation is not None - - def test_text_representation(self, simulation): - """`ReplaySimulation` has a non-literal text representation.""" - result = repr(simulation) - - assert result.startswith(' dt.timedelta(0) + assert result > datetime.timedelta(0) def test_time_to_react_no_courier_notified(self, order): """Test `Order.time_to_react` property.""" @@ -755,7 +157,7 @@ class TestProperties: """Test `Order.time_to_react` property.""" result = order.time_to_react - assert result > dt.timedelta(0) + assert result > datetime.timedelta(0) def test_time_to_pickup_no_reached_pickup_at(self, order): """Test `Order.time_to_pickup` property.""" @@ -775,7 +177,7 @@ class TestProperties: """Test `Order.time_to_pickup` property.""" result = order.time_to_pickup - assert result > dt.timedelta(0) + assert result > datetime.timedelta(0) def test_time_at_pickup_no_reached_pickup_at(self, order): """Test `Order.time_at_pickup` property.""" @@ -795,7 +197,7 @@ class TestProperties: """Test `Order.time_at_pickup` property.""" result = order.time_at_pickup - assert result > dt.timedelta(0) + assert result > datetime.timedelta(0) def test_scheduled_pickup_at_no_restaurant_notified(self, order): # noqa:WPS118 """Test `Order.scheduled_pickup_at` property.""" @@ -907,7 +309,7 @@ class TestProperties: """Test `Order.time_to_delivery` property.""" result = order.time_to_delivery - assert result > dt.timedelta(0) + assert result > datetime.timedelta(0) def test_time_at_delivery_no_reached_delivery_at(self, order): # noqa:WPS118 """Test `Order.time_at_delivery` property.""" @@ -927,9 +329,9 @@ class TestProperties: """Test `Order.time_at_delivery` property.""" result = order.time_at_delivery - assert result > dt.timedelta(0) + assert result > datetime.timedelta(0) - def test_courier_waited_at_delivery(self, order): + def test_courier_waited_at_delviery(self, order): """Test `Order.courier_waited_at_delivery` property.""" order._courier_waited_at_delivery = True @@ -955,7 +357,7 @@ class TestProperties: """Test `Order.delivery_early` property.""" order = make_order(scheduled=True) # Schedule the order to a lot later. - order.scheduled_delivery_at += dt.timedelta(hours=2) + order.scheduled_delivery_at += datetime.timedelta(hours=2) result = order.delivery_early @@ -965,7 +367,7 @@ class TestProperties: """Test `Order.delivery_early` property.""" order = make_order(scheduled=True) # Schedule the order to a lot earlier. - order.scheduled_delivery_at -= dt.timedelta(hours=2) + order.scheduled_delivery_at -= datetime.timedelta(hours=2) result = order.delivery_early @@ -981,7 +383,7 @@ class TestProperties: """Test `Order.delivery_early` property.""" order = make_order(scheduled=True) # Schedule the order to a lot earlier. - order.scheduled_delivery_at -= dt.timedelta(hours=2) + order.scheduled_delivery_at -= datetime.timedelta(hours=2) result = order.delivery_late @@ -991,7 +393,7 @@ class TestProperties: """Test `Order.delivery_early` property.""" order = make_order(scheduled=True) # Schedule the order to a lot later. - order.scheduled_delivery_at += dt.timedelta(hours=2) + order.scheduled_delivery_at += datetime.timedelta(hours=2) result = order.delivery_late @@ -1015,17 +417,17 @@ class TestProperties: """Test `Order.total_time` property.""" result = order.total_time - assert result > dt.timedelta(0) + assert result > datetime.timedelta(0) @pytest.mark.db @pytest.mark.no_cover -def test_make_random_orders( # noqa:C901,WPS211,WPS213 +def test_make_random_orders( # noqa:C901,WPS211,WPS213,WPS231 db_session, make_address, make_courier, make_restaurant, make_order, ): """Sanity check the all the `make_*` fixtures. - Ensure that all generated `Address`, `Courier`, `Customer`, `Restaurant`, + Ensure that all generated `Address`, `Courier`, `Customer`, `Restauarant`, and `Order` objects adhere to the database constraints. """ # noqa:D202 # Generate a large number of `Order`s to obtain a large variance of data. diff --git a/tests/forecasts/conftest.py b/tests/forecasts/conftest.py index 522eb0f..f258a3c 100644 --- a/tests/forecasts/conftest.py +++ b/tests/forecasts/conftest.py @@ -17,7 +17,9 @@ def horizontal_datetime_index(): The times resemble a horizontal time series with a `frequency` of `7`. All observations take place at `NOON`. """ - first_start_at = dt.datetime(*test_config.DATE, test_config.NOON, 0) + first_start_at = dt.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, test_config.NOON, 0, + ) gen = ( start_at diff --git a/tests/forecasts/timify/test_aggregate_orders.py b/tests/forecasts/timify/test_aggregate_orders.py index b515e45..325db74 100644 --- a/tests/forecasts/timify/test_aggregate_orders.py +++ b/tests/forecasts/timify/test_aggregate_orders.py @@ -73,7 +73,9 @@ class TestAggregateOrders: order = make_order( scheduled=False, restaurant=restaurant, - placed_at=datetime.datetime(*test_config.DATE, hour, 11), + placed_at=datetime.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, hour, 11, + ), ) db_session.add(order) @@ -102,7 +104,9 @@ class TestAggregateOrders: order = make_order( scheduled=False, restaurant=restaurant, - placed_at=datetime.datetime(*test_config.DATE, hour, 11), + placed_at=datetime.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, hour, 11, + ), ) db_session.add(order) @@ -133,7 +137,9 @@ class TestAggregateOrders: order = make_order( scheduled=False, restaurant=restaurant, - placed_at=datetime.datetime(*test_config.DATE, hour, 11), + placed_at=datetime.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, hour, 11, + ), ) db_session.add(order) @@ -163,15 +169,21 @@ class TestAggregateOrders: ad_hoc_order = make_order( scheduled=False, restaurant=restaurant, - placed_at=datetime.datetime(*test_config.DATE, 11, 11), + placed_at=datetime.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, 11, 11, + ), ) db_session.add(ad_hoc_order) pre_order = make_order( scheduled=True, restaurant=restaurant, - placed_at=datetime.datetime(*test_config.DATE, 9, 0), - scheduled_delivery_at=datetime.datetime(*test_config.DATE, 12, 0), + placed_at=datetime.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, 9, 0, + ), + scheduled_delivery_at=datetime.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, 12, 0, + ), ) db_session.add(pre_order) @@ -203,7 +215,9 @@ class TestAggregateOrders: order = make_order( scheduled=False, restaurant=restaurant, - placed_at=datetime.datetime(*test_config.DATE, hour, 11), + placed_at=datetime.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, hour, 11, + ), ) db_session.add(order) @@ -238,7 +252,9 @@ class TestAggregateOrders: order = make_order( scheduled=False, restaurant=restaurant, - placed_at=datetime.datetime(*test_config.DATE, hour, 11), + placed_at=datetime.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, hour, 11, + ), ) db_session.add(order) @@ -317,7 +333,9 @@ class TestAggregateOrders: order = make_order( scheduled=False, restaurant=restaurant1, - placed_at=datetime.datetime(*test_config.DATE, hour, 11), + placed_at=datetime.datetime( + test_config.YEAR, test_config.MONTH, test_config.DAY, hour, 11, + ), ) db_session.add(order)