diff --git a/apps/shipments/serializers/shipment.py b/apps/shipments/serializers/shipment.py index e53ecddc..2dcb7b5f 100644 --- a/apps/shipments/serializers/shipment.py +++ b/apps/shipments/serializers/shipment.py @@ -396,8 +396,8 @@ class ShipmentVaultSerializer(NullableFieldsMixin, serializers.ModelSerializer): class Meta: model = Shipment - exclude = ('owner_id', 'storage_credentials_id', 'background_data_hash_interval', - 'vault_id', 'vault_uri', 'shipper_wallet_id', 'carrier_wallet_id', 'manual_update_hash_interval', + exclude = ('owner_id', 'storage_credentials_id', 'background_data_hash_interval', 'vault_id', 'vault_uri', + 'shipper_wallet_id', 'carrier_wallet_id', 'moderator_wallet_id', 'manual_update_hash_interval', 'contract_version', 'device', 'updated_by', 'state', 'exception', 'delayed', 'expected_delay_hours', 'geofences', 'assignee_id', 'gtx_required', 'gtx_validation', 'gtx_validation_timestamp', 'aftership_tracking', 'carrier_abbv', 'arrival_est') diff --git a/bin/create_shipments b/bin/create_shipments new file mode 100755 index 00000000..4c2b11dd --- /dev/null +++ b/bin/create_shipments @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +bin/ddo bin/dev-tools/create_shipments.py "$@" diff --git a/bin/dev-tools/create_shipments.py b/bin/dev-tools/create_shipments.py new file mode 100755 index 00000000..e73eaa15 --- /dev/null +++ b/bin/dev-tools/create_shipments.py @@ -0,0 +1,468 @@ +#! /usr/bin/env python3 + +import argparse +import json +import logging +import sys +from copy import deepcopy +from datetime import datetime, timezone, timedelta +from enum import Enum +from json.decoder import JSONDecodeError +from random import randint +from urllib.parse import urlparse +from uuid import uuid4 + +import requests +from faker import Faker + +# pylint:disable=invalid-name +logger = logging.getLogger('transmission') +console = logging.StreamHandler() + + +class CriticalError(Exception): + def __init__(self, message): + logger.critical(message) + logger.removeHandler(console) + self.parameter = message + + def __str__(self): + return repr(self.parameter) + + +class NonCriticalError(Exception): + def __init__(self, message): + logger.warning(message) + self.parameter = message + + def __str__(self): + return repr(self.parameter) + + +class LogLevels(Enum): + critical = 'CRITICAL' + error = 'ERROR' + warning = 'WARNING' + info = 'INFO' + debug = 'DEBUG' + + # pylint: disable=invalid-str-returned + def __str__(self): + return self.value + + +parser = argparse.ArgumentParser() +parser.add_argument("--count", "-t", help="Total amount of shipments to be created, defaults to 10", type=int, + default=10) +parser.add_argument("--startnumber", "-n", help="Number to start at for sequential attributes.", type=int, + default=0) +parser.add_argument("--partition", "-p", help="Maximum number of shipments to be created per wallet, defaults to 10.", + type=int, default=10) +parser.add_argument("--carrier", "-c", help="Set carrier wallet owner by passing in the username. Defaults to user1.", + choices=['user1@shipchain.io', 'user2@shipchain.io'], default='user1@shipchain.io') +parser.add_argument("--shipper", "-s", help="Set shipper wallet owner by passing in the username. Defaults to user1.", + choices=['user1@shipchain.io', 'user2@shipchain.io'], default='user1@shipchain.io') +parser.add_argument("--moderator", "-m", + help="Set moderator wallet owner by passing in the username. Defaults to user1.", + choices=['user1@shipchain.io', 'user2@shipchain.io']) +parser.add_argument("--loglevel", "-l", help="Set the logging level for the creator. Defaults to info.", + choices=list(LogLevels.__members__), default='info') +parser.add_argument("--device", "-d", help="Add devices to shipment creation, defaults to false", + action="store_true") +parser.add_argument("--attributes", "-a", action='append', + help="Attributes to be sent in shipment creation (defaults to required values). " + "Setting a key to ##RAND## sets the value as new uuid per creation, " + "and ##NUMB## is replaced with a number starting from the --startnumber " + "Format should be either {\"key\": \"value\"} or key = value. " + "EX: \'{\"forwarders_shipper_id\": \"##RAND##\", \"pro_number\": \"PRO_##NUMB##\"}\'") +parser.add_argument("--add_tracking", help="Adds tracking data to shipments (requires --device or -d)", + action='store_true') +parser.add_argument("--add_telemetry", help="Adds telemetry data to shipments (requires --device or -d)", + action='store_true') +parser.add_argument("--profiles_url", + help="Sets the profiles url for the creator. Defaults to http://profiles-runserver:8000", + default='http://profiles-runserver:8000') +parser.add_argument("--transmission_url", + help="Sets the transmission url for the creator. Defaults to http://transmission-runserver:8000", + default='http://transmission-runserver:8000') +parser.add_argument("--reuse_wallets", help="Reuse wallets instead of creating new ones, defaults to True", + action="store_true", + default=True) +parser.add_argument("--reuse_storage_credentials", + help="Reuse storage credentials instead of creating a new one, defaults to True", + action="store_true", + default=True) + + +# pylint:disable=too-many-instance-attributes +class ShipmentCreator: + # Default variables for local use + client_id = 892633 + users = { + 'user1@shipchain.io': { + 'username': 'user1@shipchain.io', + 'password': 'user1Password', + 'token': None, + 'token_exp': None, + 'wallets': [] + }, 'user2@shipchain.io': { + 'username': 'user2@shipchain.io', + 'password': 'user2Password', + 'token': None, + 'token_exp': None, + 'wallets': [] + } + } + tracking_data = { + "position": { + "source": "gps", + }, + "version": "1.2.4", + "timestamp": datetime.now(timezone.utc).isoformat(), + } + telemetry_data = { + "version": "1.2.4", + "timestamp": datetime.now(timezone.utc).isoformat(), + } + + # pylint: disable=too-many-arguments, too-many-locals + def __init__(self): + self.attributes = {} + self.errors = [] + self.shipments = [] + + # pylint: disable=too-many-arguments, too-many-locals, attribute-defined-outside-init + def set_shipment_fields(self, carrier='user1@shipchain.io', shipper='user1@shipchain.io', moderator=False, + startnumber=0, count=10, loglevel='warning', device=False, partition=10, attributes=None, + add_tracking=False, add_telemetry=False, profiles_url='http://profiles-runserver:8000', + transmission_url='http://transmission-runserver:9000', reuse_wallets=False, + reuse_storage_credentials=False): + self.carrier = self.users[carrier] + self.shipper = self.users[shipper] + self.moderator = self.users[moderator] if moderator else None + self.sequence_number = startnumber + self.total = count + console.setLevel(LogLevels[loglevel].value) + logger.addHandler(console) + logger.setLevel(LogLevels[loglevel].value) + self.device = device + self.partition = partition + self.add_tracking = add_tracking + self.add_telemetry = add_telemetry + self.reuse_storage_credentials = reuse_storage_credentials + self.reuse_wallets = reuse_wallets + self.profiles_url = self._validate_url(profiles_url, 'profiles') + self.transmission_url = self._validate_url(transmission_url, 'transmission') + if attributes: + self._validate_attributes(attributes) + + def _chunker(self, array): + return (array[i:i + self.partition] for i in range(0, len(array), self.partition)) + + def _validate_url(self, url, url_base): + try: + result = urlparse(url) + if not result.scheme or not result.netloc: + raise CriticalError(f'Invalid url supplied for {url_base}: {url}') + return url + except ValueError: + raise CriticalError(f'Invalid url supplied for {url_base}: {url}') + + def _validate_attributes(self, attributes): + logger.info(f'Validating attributes: {attributes}') + for attribute in attributes: + try: + attribute = json.loads(attribute) + except JSONDecodeError: + logger.debug(f'Non json attribute recieved: {attribute}') + if isinstance(attribute, dict): + logger.debug(f'Dict attribute recieved: {attribute}') + self.attributes.update(attribute) + else: + try: + key, value = attribute.split("=") + except ValueError: + raise CriticalError(f'Invalid format for attribute: {attribute}, should be in format: key=value') + self.attributes[key] = value + + def _parse_request(self, url, method='post', **kwargs): + if method not in ['post', 'get', 'patch']: + raise CriticalError(f'Invalid method: {method}') + request_method = getattr(requests, method) + try: + response = request_method(url, **kwargs) + except requests.exceptions.ConnectionError: + url_group = 'Profiles' if self.profiles_url in url else 'Transmission' + raise CriticalError( + f'Connection error raised when connecting to {url}. Ensure service {url_group} is running.') + + if not response.ok: + if response.status_code == 503: + logger.warning('Ensure Engine services are running.') + self.errors += response.json()['errors'] + raise NonCriticalError(f'Invalid response returned from {url}') + + return response.json() + + def _retrieve_updated_attributes(self): + logger.info('Updating attributes to remove/update random and sequential variables') + updated_attributes = deepcopy(self.attributes) + for key, value in updated_attributes.items(): + if "##RAND##" in value: + updated_attributes[key] = value.replace("##RAND##", str(uuid4())) + elif "##NUM##" in value: + updated_attributes[key] = value.replace("##NUM##", self.sequence_number) + if self.device: + try: + device_response = self._parse_request( + f"{self.profiles_url}/api/v1/device", + data={'device_type': "AXLE_GATEWAY"}, + headers={'Authorization': 'JWT {}'.format(self.shipper['token'])}) + except NonCriticalError: + raise CriticalError('Error generating device') + updated_attributes['device_id'] = device_response['data']['id'] + self.sequence_number += 1 + return updated_attributes + + def get_user_jwt(self, user): + if user['token_exp'] and datetime.now(timezone.utc) < user['token_exp']: + logger.debug(f'User {user["username"]} has non-expired token') + return user['token'] + + response = self._parse_request( + f"{self.profiles_url}/openid/token/", + data={ + "username": user['username'], + "password": user['password'], + "client_id": self.client_id, + "grant_type": "password", + "scope": "openid email" + }) + if not response: + raise CriticalError(f'Error generating token for user {user["username"]}') + + user['token'] = response['id_token'] + # Give a buffer of 60 seconds to the token time check + user['token_exp'] = datetime.now(timezone.utc) + timedelta(seconds=(response['expires_in'] - 60)) + return response['id_token'] + + def _reuse_wallets(self): + logger.info('Reusing wallets for shipment') + for wallet_owner in ('shipper', 'carrier', 'moderator'): + wallet_owner_dict = getattr(self, wallet_owner) + if not wallet_owner_dict: + continue + + if not wallet_owner_dict['wallets']: + logger.debug(f'Wallets not found for owner: {wallet_owner_dict["username"]}') + try: + response = self._parse_request( + f"{self.profiles_url}/api/v1/wallet?page_size=9999", method='get', + headers={'Authorization': f'JWT {self.get_user_jwt(self.shipper)}'}) + except NonCriticalError: + raise CriticalError(f'Error retrieving wallets for user {wallet_owner_dict["username"]}.') + + logger.debug(f"Wallet count returned: {response['meta']['pagination']['count']}") + if response['meta']['pagination']['count'] == 0 or response['meta']['pagination']['count'] \ + < self.total // self.partition: + raise CriticalError(f'Not enough wallets for user {wallet_owner_dict["username"]}.') + + wallet_owner_dict['wallets'] = response['data'] + + wallet = wallet_owner_dict['wallets'].pop() + + self.attributes[f'{wallet_owner}_wallet_id'] = wallet['id'] + + def _generate_wallets(self): + logger.info('Generating wallets for shipment') + try: + shipper_response = self._parse_request( + f"{self.profiles_url}/api/v1/wallet/generate", + headers={'Authorization': f'JWT {self.get_user_jwt(self.shipper)}'}) + except NonCriticalError: + raise CriticalError(f'Error generating shipper wallet for user {self.shipper["username"]}.') + try: + carrier_response = self._parse_request( + f"{self.profiles_url}/api/v1/wallet/generate", + headers={'Authorization': f'JWT {self.get_user_jwt(self.carrier)}'}) + except NonCriticalError: + raise CriticalError(f'Error generating carrier wallet for user {self.carrier["username"]}.') + + if self.moderator: + try: + moderator_response = self._parse_request( + f"{self.profiles_url}/api/v1/wallet/generate/", + headers={'Authorization': f'JWT {self.get_user_jwt(self.moderator)}'}) + except NonCriticalError: + raise CriticalError(f'Error generating storage credentials for user {self.shipper["username"]}.') + + self.attributes['moderator_wallet_id'] = moderator_response['data']['id'] + self.attributes['shipper_wallet_id'] = shipper_response['data']['id'] + self.attributes['carrier_wallet_id'] = carrier_response['data']['id'] + + def _reuse_storage_credentials(self): + logger.info('Reusing storage credentials.') + try: + response = self._parse_request( + f"{self.profiles_url}/api/v1/storage_credentials", + headers={'Authorization': f'JWT {self.get_user_jwt(self.shipper)}'}, method='get') + except NonCriticalError: + raise CriticalError('Error retrieving storage credentials') + + logger.debug(f'Storage credentials count returned: {response["meta"]["pagination"]["count"]}') + if response['meta']['pagination']['count'] == 0: + raise CriticalError(f'No storage credentials found associated with account {self.carrier["username"]}') + + self.attributes['storage_credentials_id'] = response['data'][0]['id'] + + def _generate_storage_credentials(self): + logger.info('Creating storage credentials.') + try: + response = self._parse_request( + f"{self.profiles_url}/api/v1/storage_credentials", data={ + 'driver_type': 'local', + 'base_path': '/shipments', + 'options': "{}", + 'title': f'Shipment creator SC: {str(uuid4())}' + }, headers={'Authorization': f'JWT {self.get_user_jwt(self.shipper)}'}) + except NonCriticalError: + raise CriticalError('Error generating storage credentials.') + + logger.debug(f'Created storage credentials: {response["data"]["id"]}') + self.attributes['storage_credentials_id'] = response['data']['id'] + + # pylint:disable=unused-variable + def _add_telemetry(self, device_id): + logger.info(f'Creating sensors for device: {device_id}') + telemetry_data = [] + self.telemetry_data['device_id'] = device_id + for i in range(3): + attributes = { + 'name': f'Sensor: {str(uuid4())}', + 'hardware_id': f'Hardware_id: {str(uuid4())}', + 'units': 'c', + } + try: + response = self._parse_request( + f'{self.profiles_url}/api/v1/device/{device_id}/sensor/', + data=attributes, + headers={'Authorization': f'JWT {self.get_user_jwt(self.shipper)}'} + ) + + except NonCriticalError: + logger.warning(f'Failed to create sensor for device: {device_id}') + continue + + logger.info(f'Created sensor {response["data"]["id"]} for device {device_id}') + + telemetry_copy = deepcopy(self.telemetry_data) + telemetry_copy['hardware_id'] = attributes['hardware_id'] + telemetry_copy['sensor_id'] = response['data']['id'] + for j in range(3): + telemetry_copy['value'] = randint(0, 100) + telemetry_data.append({"payload": telemetry_copy}) + + logger.info(f'Adding telemetry via device: {device_id}') + telemetry_response = requests.post( + f"{self.transmission_url}/api/v1/devices/{device_id}/telemetry", + json=telemetry_data, + headers={"Content-type": "application/json"} + ) + if not telemetry_response.ok: + self.errors += telemetry_response.json()['errors'] + logger.warning(f'Failed to add telemetry data for device: {device_id}') + else: + logger.info(f'Added telemetry data via device: {device_id}') + + def _add_tracking(self, device_id): + logger.info(f'Adding tracking via device: {device_id}') + self.tracking_data['device_id'] = device_id + faker = Faker() + tracking_data_collection = [] + start_location = faker.local_latlng() + end_location = faker.local_latlng() + for location in (start_location, end_location): + if 'Honolulu' in location[4]: + location = faker.local_latlng() + + longitude = float(start_location[0]) + latitude = float(start_location[1]) + long_delta = (float(end_location[0]) - longitude) / 10 + lat_delta = (float(end_location[1]) - latitude) / 10 + for i in range(10): + tracking_data_collection.append({"payload": { + 'longitude': longitude, + 'latitude': latitude, + 'speed': randint(45, 85), + 'altitude': randint(500, 800), + 'uncertainty': randint(75, 100), + **self.tracking_data + }}) + longitude += long_delta + latitude += lat_delta + + tracking_response = requests.post( + f"{self.transmission_url}/api/v1/devices/{device_id}/tracking", + json=tracking_data_collection, + headers={"Content-type": "application/json"} + ) + if not tracking_response.ok: + self.errors += tracking_response.json()['errors'] + logger.warning(f'Failed to add tracking data for device: {device_id}') + else: + logger.info(f'Added tracking data via device: {device_id}') + + def _create_shipment(self): + attributes = self._retrieve_updated_attributes() + logger.info('Creating shipment') + try: + response = self._parse_request( + f"{self.transmission_url}/api/v1/shipments/", + data=attributes, + headers={'Authorization': f'JWT {self.get_user_jwt(self.shipper)}'}) + except NonCriticalError: + logger.warning('Failed to create shipment.') + return + + logger.debug(f'Created shipment: {response["data"]["id"]}') + self.shipments.append(response['data']['id']) + + if self.add_tracking: + self._add_tracking(attributes['device_id']) + if self.add_telemetry: + self._add_telemetry(attributes['device_id']) + + def create_bulk_shipments(self): + try: + (self._reuse_storage_credentials() if self.reuse_storage_credentials + else self._generate_storage_credentials()) + except CriticalError: + logger.critical(f'Response errors: {self.errors}') + sys.exit(2) + + for i in range(self.total): + if i % self.partition == 0: + try: + self._reuse_wallets() if self.reuse_wallets else self._generate_wallets() + except CriticalError: + logger.critical(f'Response errors: {self.errors}') + break + try: + self._create_shipment() + except NonCriticalError: + logger.info(f'Errors when generating shipments: {self.errors}') + + logger.info(f'Shipments created: {len(self.shipments)}') + logger.info(f'Shipment ids: {self.shipments}') + logger.info(f'Response errors: {self.errors}') + + +if __name__ == '__main__': + args = parser.parse_args() + args_dict = vars(args) + shipment_creator = ShipmentCreator() + try: + shipment_creator.set_shipment_fields(**args_dict) + except CriticalError: + sys.exit(2) + shipment_creator.create_bulk_shipments() diff --git a/compose/dev.yml b/compose/dev.yml index 8d35ff3f..f2feb092 100644 --- a/compose/dev.yml +++ b/compose/dev.yml @@ -26,6 +26,13 @@ services: image: transmission-django-dev volumes: - ../:/app + networks: + default: + aliases: + - transmission-django-shell + portal: + aliases: + - transmission-django-shell links: - psql - redis_db diff --git a/poetry.lock b/poetry.lock index 1fd29b84..f2d12abd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,7 +16,7 @@ description = "Low-level AMQP client for Python (fork of amqplib)." name = "amqp" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.6.0" +version = "2.6.1" [package.dependencies] vine = ">=1.1.3,<5.0.0a1" @@ -863,6 +863,18 @@ optional = false python-versions = "*" version = "1.0.1" +[[package]] +category = "main" +description = "Faker is a Python package that generates fake data for you." +name = "faker" +optional = false +python-versions = ">=3.4" +version = "4.1.1" + +[package.dependencies] +python-dateutil = ">=2.4" +text-unidecode = "1.3" + [[package]] category = "dev" description = "the modular source code checker: pep8 pyflakes and co" @@ -1074,6 +1086,14 @@ six = ">=1.10.0" [package.extras] test = ["mock", "nose", "nose-cov", "requests-mock"] +[[package]] +category = "dev" +description = "iniconfig: brain-dead simple config-ini parsing" +name = "iniconfig" +optional = false +python-versions = "*" +version = "1.0.1" + [[package]] category = "main" description = "Self-contained ISO 3166-1 country definitions." @@ -1822,24 +1842,25 @@ description = "pytest: simple powerful testing with Python" name = "pytest" optional = false python-versions = ">=3.5" -version = "5.4.3" +version = "6.0.1" [package.dependencies] atomicwrites = ">=1.0" attrs = ">=17.4.0" colorama = "*" +iniconfig = "*" more-itertools = ">=4.0.0" packaging = "*" pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -wcwidth = "*" +py = ">=1.8.2" +toml = "*" [package.dependencies.importlib-metadata] python = "<3.8" version = ">=0.12" [package.extras] -checkqa-mypy = ["mypy (v0.761)"] +checkqa_mypy = ["mypy (0.780)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] @@ -2181,6 +2202,14 @@ pbr = ">=2.0.0,<2.1.0 || >2.1.0" python = "<3.8" version = ">=1.7.0" +[[package]] +category = "main" +description = "The most basic Text::Unidecode port" +name = "text-unidecode" +optional = false +python-versions = "*" +version = "1.3" + [[package]] category = "dev" description = "module for creating simple ASCII tables" @@ -2435,7 +2464,7 @@ test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] -content-hash = "19562a3c273db2db38c77f8aa6113c2168f779dd2e402aae9d1fb8be09d40fe0" +content-hash = "ebd7ee19e677945f6ecb3257949142e550d4c952f6a12aa30afc846d1053f84c" python-versions = "==3.6.9" [metadata.files] @@ -2444,8 +2473,8 @@ aioredis = [ {file = "aioredis-1.3.1.tar.gz", hash = "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a"}, ] amqp = [ - {file = "amqp-2.6.0-py2.py3-none-any.whl", hash = "sha256:bb68f8d2bced8f93ccfd07d96c689b716b3227720add971be980accfc2952139"}, - {file = "amqp-2.6.0.tar.gz", hash = "sha256:24dbaff8ce4f30566bb88976b398e8c4e77637171af3af6f1b9650f48890e60b"}, + {file = "amqp-2.6.1-py2.py3-none-any.whl", hash = "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"}, + {file = "amqp-2.6.1.tar.gz", hash = "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21"}, ] argh = [ {file = "argh-0.26.2-py2.py3-none-any.whl", hash = "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3"}, @@ -2783,6 +2812,10 @@ ecdsa = [ et-xmlfile = [ {file = "et_xmlfile-1.0.1.tar.gz", hash = "sha256:614d9722d572f6246302c4491846d2c393c199cfa4edc9af593437691683335b"}, ] +faker = [ + {file = "Faker-4.1.1-py3-none-any.whl", hash = "sha256:1290f589648bc470b8d98fff1fdff773fe3f46b4ca2cac73ac74668b12cf008e"}, + {file = "Faker-4.1.1.tar.gz", hash = "sha256:c006b3664c270a2cfd4785c5e41ff263d48101c4e920b5961cf9c237131d8418"}, +] flake8 = [ {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, @@ -2913,6 +2946,10 @@ influxdb = [ {file = "influxdb-5.0.0-py2.py3-none-any.whl", hash = "sha256:43b2fde195ee2302cfa87c8a0e1d29429f175ed584516d02e04f9b2c3c2ac2ad"}, {file = "influxdb-5.0.0.tar.gz", hash = "sha256:6adba2ddfd5781a06b5204339e679d66645bf6cc2b7f493eb9d7c8986d714e80"}, ] +iniconfig = [ + {file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"}, + {file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"}, +] iso3166 = [ {file = "iso3166-1.0.1-py2.py3-none-any.whl", hash = "sha256:b07208703bd881a4f974e39fa013c4498dddd64913ada15f24be75d02ae68a44"}, {file = "iso3166-1.0.1.tar.gz", hash = "sha256:b1e58dbcf50fbb2c9c418ec7a6057f0cdb30b8f822ac852f72e71ba769dae8c5"}, @@ -3276,8 +3313,8 @@ pyrsistent = [ {file = "pyrsistent-0.16.0.tar.gz", hash = "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3"}, ] pytest = [ - {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, - {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, + {file = "pytest-6.0.1-py3-none-any.whl", hash = "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"}, + {file = "pytest-6.0.1.tar.gz", hash = "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4"}, ] pytest-asyncio = [ {file = "pytest-asyncio-0.14.0.tar.gz", hash = "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700"}, @@ -3403,6 +3440,10 @@ stevedore = [ {file = "stevedore-3.2.0-py3-none-any.whl", hash = "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"}, {file = "stevedore-3.2.0.tar.gz", hash = "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5"}, ] +text-unidecode = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] texttable = [ {file = "texttable-1.6.2-py2.py3-none-any.whl", hash = "sha256:7dc282a5b22564fe0fdc1c771382d5dd9a54742047c61558e071c8cd595add86"}, {file = "texttable-1.6.2.tar.gz", hash = "sha256:eff3703781fbc7750125f50e10f001195174f13825a92a45e9403037d539b4f4"}, diff --git a/pyproject.toml b/pyproject.toml index b54ddc13..2e24a895 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ uwsgi = "~2.0.17" watchdog = "~0.9" whitenoise = "~4.1" watchtower = "==0.5.5" +faker = "^4.1.1" [tool.poetry.dev-dependencies]