diff --git a/.all-contributorsrc b/.all-contributorsrc index 0f568cbd..bbf06353 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -153,6 +153,15 @@ "contributions": [ "code" ] + }, + { + "login": "ibhuiyan17", + "name": "Ibtida Bhuiyan", + "avatar_url": "https://avatars1.githubusercontent.com/u/33792969?v=4", + "profile": "http://ibtida.me", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/Makefile b/Makefile index 78f8f6b4..7059a9be 100644 --- a/Makefile +++ b/Makefile @@ -21,9 +21,7 @@ lint: pylint $(APP) || true fmt: - isort --apply --atomic - black . -l 120 + invoke fmt check-fmt: - isort -rc --check - black . --check --diff + invoke check --fmt --sort diff --git a/Pipfile b/Pipfile index 6b30ef90..21965c37 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ verify_ssl = true [dev-packages] bandit = "*" black = "==19.10b0" +invoke = "*" isort = "*" pytest = "*" pylint = "*" @@ -27,5 +28,7 @@ python_version = "3.8" [scripts] dev = "uvicorn app.main:APP --reload" start = "uvicorn app.main:APP" -fmt = "black . -l 120" -sort = "isort --apply --atomic" +fmt = "invoke fmt" +sort = "invoke sort" +lint = "invoke lint" +test = "invoke test" diff --git a/Pipfile.lock b/Pipfile.lock index c949f6cb..cc93e092 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ecd83aad2c3783fdaa5581f562d022a6b500b3f3b4beb7c3f63d3d5baff85813" + "sha256": "9574394caac3b437d4357507a93178b38289861241165e4736369a7b4c2a2cbc" }, "pipfile-spec": 6, "requires": { @@ -342,6 +342,15 @@ ], "version": "==3.1.0" }, + "invoke": { + "hashes": [ + "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132", + "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134", + "sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d" + ], + "index": "pypi", + "version": "==1.4.1" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", diff --git a/README.md b/README.md index 72a30c6a..18ad6645 100644 --- a/README.md +++ b/README.md @@ -391,15 +391,22 @@ These are the available API wrappers created by the community. They are not nece You will need the following things properly installed on your computer. * [Python 3](https://www.python.org/downloads/) (with pip) -* [Flask](https://pypi.org/project/Flask/) * [pipenv](https://pypi.org/project/pipenv/) ## Installation * `git clone https://github.com/ExpDev07/coronavirus-tracker-api.git` * `cd coronavirus-tracker-api` -* `pipenv shell` -* `pipenv install` + +1. Make sure you have [`python3.8` installed and on your `PATH`](https://docs.python-guide.org/starting/installation/). +2. [Install the `pipenv` dependency manager](https://pipenv.readthedocs.io/en/latest/install/#installing-pipenv) + * with [pipx](https://pipxproject.github.io/pipx/) `$ pipx install pipenv` + * with [Homebrew/Linuxbrew](https://pipenv.readthedocs.io/en/latest/install/#homebrew-installation-of-pipenv) `$ brew install pipenv` + * with [pip/pip3 directly](https://pipenv.readthedocs.io/en/latest/install/#pragmatic-installation-of-pipenv) `$ pip install --user pipenv` +3. Create virtual environment and install all dependencies `$ pipenv sync --dev` +4. Activate/enter the virtual environment `$ pipenv shell` + +And don't despair if don't get the python setup working on the first try. No one did. Guido got pretty close... once. But that's another story. Good luck. ## Running / Development @@ -407,12 +414,26 @@ You will need the following things properly installed on your computer. * Visit your app at [http://localhost:5000](http://localhost:5000). ### Running Tests +> pytest + +```bash +pipenv run test +``` -* `make test` ### Linting +> pylint -* `make lint` +```bash +pipenv run lint +``` + +### Formatting +> black + +```bash +pipenv run fmt +``` ### Building @@ -446,6 +467,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Turreted

đź’» +
Ibtida Bhuiyan

💻 diff --git a/app/location/__init__.py b/app/location/__init__.py index cafcf547..4782fddb 100644 --- a/app/location/__init__.py +++ b/app/location/__init__.py @@ -1,5 +1,5 @@ from ..coordinates import Coordinates -from ..utils import countrycodes +from ..utils import countries from ..utils.populations import country_population @@ -31,7 +31,7 @@ def country_code(self): :returns: The country code. :rtype: str """ - return (countrycodes.country_code(self.country) or countrycodes.default_code).upper() + return (countries.country_code(self.country) or countries.default_country_code).upper() @property def country_population(self): diff --git a/app/main.py b/app/main.py index 44876182..f977622d 100644 --- a/app/main.py +++ b/app/main.py @@ -10,7 +10,6 @@ import uvicorn from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware -from fastapi.middleware.wsgi import WSGIMiddleware from fastapi.responses import JSONResponse from .core import create_app @@ -18,7 +17,8 @@ from .models.latest import LatestResponse as Latest from .models.location import LocationResponse as Location from .models.location import LocationsResponse as Locations -from .router import router +from .router.v1 import router as v1router +from .router.v2 import router as v2router # ############ # FastAPI App @@ -83,11 +83,9 @@ async def handle_validation_error(request: Request, exc: pydantic.error_wrappers # Include routers. -APP.include_router(router, prefix="/v2", tags=["v2"]) +APP.include_router(v1router, prefix="", tags=["v1"]) +APP.include_router(v2router, prefix="/v2", tags=["v2"]) -# mount the existing Flask app -# v1 @ / -APP.mount("/", WSGIMiddleware(create_app())) # Running of app. if __name__ == "__main__": diff --git a/app/router/__init__.py b/app/router/__init__.py index eefb5f0a..e37fdd9f 100644 --- a/app/router/__init__.py +++ b/app/router/__init__.py @@ -1,7 +1,6 @@ from fastapi import APIRouter -# Create the router. -router = APIRouter() +from .v1 import all, confirmed, deaths, recovered # The routes. -from . import latest, sources, locations # isort:skip +from .v2 import latest, sources, locations # isort:skip diff --git a/app/router/v1/__init__.py b/app/router/v1/__init__.py new file mode 100644 index 00000000..af9233c5 --- /dev/null +++ b/app/router/v1/__init__.py @@ -0,0 +1,3 @@ +from fastapi import APIRouter + +router = APIRouter() diff --git a/app/router/v1/all.py b/app/router/v1/all.py new file mode 100644 index 00000000..e528ed5a --- /dev/null +++ b/app/router/v1/all.py @@ -0,0 +1,19 @@ +from ...services.location.jhu import get_category +from . import router + + +@router.get("/all") +def all(): + # Get all the categories. + confirmed = get_category("confirmed") + deaths = get_category("deaths") + recovered = get_category("recovered") + + return { + # Data. + "confirmed": confirmed, + "deaths": deaths, + "recovered": recovered, + # Latest. + "latest": {"confirmed": confirmed["latest"], "deaths": deaths["latest"], "recovered": recovered["latest"],}, + } diff --git a/app/router/v1/confirmed.py b/app/router/v1/confirmed.py new file mode 100644 index 00000000..0a8ab1c3 --- /dev/null +++ b/app/router/v1/confirmed.py @@ -0,0 +1,9 @@ +from ...services.location.jhu import get_category +from . import router + + +@router.get("/confirmed") +def confirmed(): + confirmed = get_category("confirmed") + + return confirmed diff --git a/app/router/v1/deaths.py b/app/router/v1/deaths.py new file mode 100644 index 00000000..b3d90413 --- /dev/null +++ b/app/router/v1/deaths.py @@ -0,0 +1,9 @@ +from ...services.location.jhu import get_category +from . import router + + +@router.get("/deaths") +def deaths(): + deaths = get_category("deaths") + + return deaths diff --git a/app/router/v1/recovered.py b/app/router/v1/recovered.py new file mode 100644 index 00000000..e9ae8f72 --- /dev/null +++ b/app/router/v1/recovered.py @@ -0,0 +1,9 @@ +from ...services.location.jhu import get_category +from . import router + + +@router.get("/recovered") +def recovered(): + recovered = get_category("recovered") + + return recovered diff --git a/app/router/v2/__init__.py b/app/router/v2/__init__.py new file mode 100644 index 00000000..af9233c5 --- /dev/null +++ b/app/router/v2/__init__.py @@ -0,0 +1,3 @@ +from fastapi import APIRouter + +router = APIRouter() diff --git a/app/router/latest.py b/app/router/v2/latest.py similarity index 86% rename from app/router/latest.py rename to app/router/v2/latest.py index 81b254cf..8e2e561b 100644 --- a/app/router/latest.py +++ b/app/router/v2/latest.py @@ -1,7 +1,7 @@ from fastapi import Request -from ..enums.sources import Sources -from ..models.latest import LatestResponse as Latest +from ...enums.sources import Sources +from ...models.latest import LatestResponse as Latest from . import router diff --git a/app/router/locations.py b/app/router/v2/locations.py similarity index 92% rename from app/router/locations.py rename to app/router/v2/locations.py index af4b1cfd..2fde5c9e 100644 --- a/app/router/locations.py +++ b/app/router/v2/locations.py @@ -1,8 +1,8 @@ from fastapi import HTTPException, Request -from ..enums.sources import Sources -from ..models.location import LocationResponse as Location -from ..models.location import LocationsResponse as Locations +from ...enums.sources import Sources +from ...models.location import LocationResponse as Location +from ...models.location import LocationsResponse as Locations from . import router diff --git a/app/router/sources.py b/app/router/v2/sources.py similarity index 85% rename from app/router/sources.py rename to app/router/v2/sources.py index 538921f4..4ade2fef 100644 --- a/app/router/sources.py +++ b/app/router/v2/sources.py @@ -1,4 +1,4 @@ -from ..data import data_sources +from ...data import data_sources from . import router diff --git a/app/routes/__init__.py b/app/routes/__init__.py deleted file mode 100644 index 2a584490..00000000 --- a/app/routes/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -app.routes - -isort:skip_file -""" -from flask import Blueprint, redirect, request, abort, current_app as app -from ..data import data_source - -# Follow the import order to avoid circular dependency -api_v1 = Blueprint("api_v1", __name__, url_prefix="") - -# API version 1. -from .v1 import confirmed, deaths, recovered, all diff --git a/app/routes/v1/__init__.py b/app/routes/v1/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/app/routes/v1/all.py b/app/routes/v1/all.py deleted file mode 100644 index 9638c4bd..00000000 --- a/app/routes/v1/all.py +++ /dev/null @@ -1,23 +0,0 @@ -from flask import jsonify - -from ...routes import api_v1 as api -from ...services.location.jhu import get_category - - -@api.route("/all") -def all(): - # Get all the categories. - confirmed = get_category("confirmed") - deaths = get_category("deaths") - recovered = get_category("recovered") - - return jsonify( - { - # Data. - "confirmed": confirmed, - "deaths": deaths, - "recovered": recovered, - # Latest. - "latest": {"confirmed": confirmed["latest"], "deaths": deaths["latest"], "recovered": recovered["latest"],}, - } - ) diff --git a/app/routes/v1/confirmed.py b/app/routes/v1/confirmed.py deleted file mode 100644 index 85cfe039..00000000 --- a/app/routes/v1/confirmed.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import jsonify - -from ...routes import api_v1 as api -from ...services.location.jhu import get_category - - -@api.route("/confirmed") -def confirmed(): - return jsonify(get_category("confirmed")) diff --git a/app/routes/v1/deaths.py b/app/routes/v1/deaths.py deleted file mode 100644 index cb65874b..00000000 --- a/app/routes/v1/deaths.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import jsonify - -from ...routes import api_v1 as api -from ...services.location.jhu import get_category - - -@api.route("/deaths") -def deaths(): - return jsonify(get_category("deaths")) diff --git a/app/routes/v1/recovered.py b/app/routes/v1/recovered.py deleted file mode 100644 index be5fe646..00000000 --- a/app/routes/v1/recovered.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import jsonify - -from ...routes import api_v1 as api -from ...services.location.jhu import get_category - - -@api.route("/recovered") -def recovered(): - return jsonify(get_category("recovered")) diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index 9fde386f..ef99dddc 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -7,7 +7,7 @@ from ...coordinates import Coordinates from ...location import TimelinedLocation from ...timeline import Timeline -from ...utils import countrycodes +from ...utils import countries from ...utils import date as date_util from . import LocationService @@ -80,7 +80,7 @@ def get_category(category): { # General info. "country": country, - "country_code": countrycodes.country_code(country), + "country_code": countries.country_code(country), "province": item["Province/State"], # Coordinates. "coordinates": {"lat": item["Lat"], "long": item["Long"],}, diff --git a/app/utils/countrycodes.py b/app/utils/countries.py similarity index 73% rename from app/utils/countrycodes.py rename to app/utils/countries.py index f3e90e8f..6647e679 100644 --- a/app/utils/countrycodes.py +++ b/app/utils/countries.py @@ -4,13 +4,13 @@ LOGGER = logging.getLogger(__name__) # Default country code. -default_code = "XX" +default_country_code = "XX" # Mapping of country names to alpha-2 codes according to # https://en.wikipedia.org/wiki/ISO_3166-1. # As a reference see also https://github.com/TakahikoKawasaki/nv-i18n (in Java) # fmt: off -is_3166_1 = { +country_name__country_code = { "Afghanistan" : "AF", "Åland Islands" : "AX", "Albania" : "AL", @@ -27,7 +27,10 @@ "Australia" : "AU", "Austria" : "AT", "Azerbaijan" : "AZ", + " Azerbaijan" : "AZ", "Bahamas" : "BS", + "The Bahamas" : "BS", + "Bahamas, The" : "BS", "Bahrain" : "BH", "Bangladesh" : "BD", "Barbados" : "BB", @@ -38,13 +41,18 @@ "Bermuda" : "BM", "Bhutan" : "BT", "Bolivia, Plurinational State of" : "BO", + "Bolivia" : "BO", "Bonaire, Sint Eustatius and Saba" : "BQ", + "Caribbean Netherlands" : "BQ", "Bosnia and Herzegovina" : "BA", + # "Bosnia–Herzegovina" : "BA", + "Bosnia" : "BA", "Botswana" : "BW", "Bouvet Island" : "BV", "Brazil" : "BR", "British Indian Ocean Territory" : "IO", "Brunei Darussalam" : "BN", + "Brunei" : "BN", "Bulgaria" : "BG", "Burkina Faso" : "BF", "Burundi" : "BI", @@ -52,29 +60,40 @@ "Cameroon" : "CM", "Canada" : "CA", "Cape Verde" : "CV", + "Cabo Verde" : "CV", "Cayman Islands" : "KY", "Central African Republic" : "CF", "Chad" : "TD", "Chile" : "CL", "China" : "CN", + "Mainland China" : "CN", "Christmas Island" : "CX", "Cocos (Keeling) Islands" : "CC", "Colombia" : "CO", "Comoros" : "KM", "Congo" : "CG", + "Congo (Brazzaville)" : "CG", + "Republic of the Congo" : "CG", "Congo, the Democratic Republic of the" : "CD", + "Congo (Kinshasa)" : "CD", + "DR Congo" : "CD", "Cook Islands" : "CK", "Costa Rica" : "CR", "Côte d'Ivoire" : "CI", + "Cote d'Ivoire" : "CI", + "Ivory Coast" : "CI", "Croatia" : "HR", "Cuba" : "CU", "Curaçao" : "CW", + "Curacao" : "CW", "Cyprus" : "CY", "Czech Republic" : "CZ", + "Czechia" : "CZ", "Denmark" : "DK", "Djibouti" : "DJ", "Dominica" : "DM", "Dominican Republic" : "DO", + "Dominican Rep" : "DO", "Ecuador" : "EC", "Egypt" : "EG", "El Salvador" : "SV", @@ -83,7 +102,9 @@ "Estonia" : "EE", "Ethiopia" : "ET", "Falkland Islands (Malvinas)" : "FK", + "Falkland Islands" : "FK", "Faroe Islands" : "FO", + "Faeroe Islands" : "FO", "Fiji" : "FJ", "Finland" : "FI", "France" : "FR", @@ -92,8 +113,11 @@ "French Southern Territories" : "TF", "Gabon" : "GA", "Gambia" : "GM", + "The Gambia" : "GM", + "Gambia, The" : "GM", "Georgia" : "GE", "Germany" : "DE", + "Deutschland" : "DE", "Ghana" : "GH", "Gibraltar" : "GI", "Greece" : "GR", @@ -109,31 +133,49 @@ "Haiti" : "HT", "Heard Island and McDonald Islands" : "HM", "Holy See (Vatican City State)" : "VA", + "Holy See" : "VA", + "Vatican City" : "VA", "Honduras" : "HN", "Hong Kong" : "HK", + "Hong Kong SAR" : "HK", "Hungary" : "HU", "Iceland" : "IS", "India" : "IN", "Indonesia" : "ID", "Iran, Islamic Republic of" : "IR", + "Iran" : "IR", + "Iran (Islamic Republic of)" : "IR", "Iraq" : "IQ", "Ireland" : "IE", + "Republic of Ireland" : "IE", "Isle of Man" : "IM", "Israel" : "IL", "Italy" : "IT", "Jamaica" : "JM", "Japan" : "JP", "Jersey" : "JE", + # Guernsey and Jersey form Channel Islands. Conjoin Guernsey on Jersey. + # Jersey has higher population. + # https://en.wikipedia.org/wiki/Channel_Islands + "Guernsey and Jersey" : "JE", + "Channel Islands" : "JE", + # "Channel Islands" : "GB", "Jordan" : "JO", "Kazakhstan" : "KZ", "Kenya" : "KE", "Kiribati" : "KI", "Korea, Democratic People's Republic of" : "KP", + "North Korea" : "KP", "Korea, Republic of" : "KR", + "Korea, South" : "KR", + "South Korea" : "KR", + "Republic of Korea" : "KR", "Kosovo, Republic of" : "XK", + "Kosovo" : "XK", "Kuwait" : "KW", "Kyrgyzstan" : "KG", "Lao People's Democratic Republic" : "LA", + "Laos" : "LA", "Latvia" : "LV", "Lebanon" : "LB", "Lesotho" : "LS", @@ -143,7 +185,11 @@ "Lithuania" : "LT", "Luxembourg" : "LU", "Macao" : "MO", + # TODO Macau is probably a typo. Report it to CSSEGISandData/COVID-19 + "Macau" : "MO", + "Macao SAR" : "MO", "North Macedonia" : "MK", + "Macedonia" : "MK", "Madagascar" : "MG", "Malawi" : "MW", "Malaysia" : "MY", @@ -157,7 +203,11 @@ "Mayotte" : "YT", "Mexico" : "MX", "Micronesia, Federated States of" : "FM", + "F.S. Micronesia" : "FM", + "Micronesia" : "FM", "Moldova, Republic of" : "MD", + "Republic of Moldova" : "MD", + "Moldova" : "MD", "Monaco" : "MC", "Mongolia" : "MN", "Montenegro" : "ME", @@ -182,6 +232,11 @@ "Pakistan" : "PK", "Palau" : "PW", "Palestine, State of" : "PS", + "Palestine" : "PS", + "occupied Palestinian territory" : "PS", + "State of Palestine" : "PS", + "The West Bank and Gaza" : "PS", + "West Bank and Gaza" : "PS", "Panama" : "PA", "Papua New Guinea" : "PG", "Paraguay" : "PY", @@ -193,19 +248,30 @@ "Puerto Rico" : "PR", "Qatar" : "QA", "Réunion" : "RE", + "Reunion" : "RE", "Romania" : "RO", "Russian Federation" : "RU", + "Russia" : "RU", "Rwanda" : "RW", "Saint Barthélemy" : "BL", + "Saint Barthelemy" : "BL", "Saint Helena, Ascension and Tristan da Cunha" : "SH", + "Saint Helena" : "SH", "Saint Kitts and Nevis" : "KN", + "Saint Kitts & Nevis" : "KN", "Saint Lucia" : "LC", "Saint Martin (French part)" : "MF", + "Saint Martin" : "MF", + "St. Martin" : "MF", "Saint Pierre and Miquelon" : "PM", + "Saint Pierre & Miquelon" : "PM", "Saint Vincent and the Grenadines" : "VC", + "St. Vincent & Grenadines" : "VC", "Samoa" : "WS", "San Marino" : "SM", "Sao Tome and Principe" : "ST", + "São Tomé and Príncipe" : "ST", + "Sao Tome & Principe" : "ST", "Saudi Arabia" : "SA", "Senegal" : "SN", "Serbia" : "RS", @@ -213,6 +279,7 @@ "Sierra Leone" : "SL", "Singapore" : "SG", "Sint Maarten (Dutch part)" : "SX", + "Sint Maarten" : "SX", "Slovakia" : "SK", "Slovenia" : "SI", "Solomon Islands" : "SB", @@ -226,14 +293,21 @@ "Suriname" : "SR", "Svalbard and Jan Mayen" : "SJ", "Eswatini" : "SZ", # previous name "Swaziland" + "Swaziland" : "SZ", "Sweden" : "SE", "Switzerland" : "CH", "Syrian Arab Republic" : "SY", + "Syria" : "SY", "Taiwan, Province of China" : "TW", + "Taiwan*" : "TW", + "Taipei and environs" : "TW", + "Taiwan" : "TW", "Tajikistan" : "TJ", "Tanzania, United Republic of" : "TZ", + "Tanzania" : "TZ", "Thailand" : "TH", "Timor-Leste" : "TL", + "East Timor" : "TL", "Togo" : "TG", "Tokelau" : "TK", "Tonga" : "TO", @@ -242,21 +316,32 @@ "Turkey" : "TR", "Turkmenistan" : "TM", "Turks and Caicos Islands" : "TC", + "Turks and Caicos" : "TC", "Tuvalu" : "TV", "Uganda" : "UG", "Ukraine" : "UA", "United Arab Emirates" : "AE", + "Emirates" : "AE", "United Kingdom" : "GB", + "UK" : "GB", + # Conjoin North Ireland on United Kingdom + "North Ireland" : "GB", "United States" : "US", + "US" : "US", "United States Minor Outlying Islands" : "UM", "Uruguay" : "UY", "Uzbekistan" : "UZ", "Vanuatu" : "VU", "Venezuela, Bolivarian Republic of" : "VE", + "Venezuela" : "VE", "Viet Nam" : "VN", + "Vietnam" : "VN", "Virgin Islands, British" : "VG", + "British Virgin Islands" : "VG", "Virgin Islands, U.S." : "VI", + "U.S. Virgin Islands" : "VI", "Wallis and Futuna" : "WF", + "Wallis & Futuna" : "WF", "Western Sahara" : "EH", "Yemen" : "YE", "Zambia" : "ZM", @@ -265,123 +350,26 @@ # see also # https://en.wikipedia.org/wiki/List_of_sovereign_states_and_dependent_territories_by_continent_(data_file)#Data_file # https://en.wikipedia.org/wiki/List_of_sovereign_states_and_dependent_territories_by_continent - "United Nations Neutral Zone" : "XD", - "Iraq-Saudi Arabia Neutral Zone" : "XE", - "Spratly Islands" : "XS", + "United Nations Neutral Zone" : "XD", + "Iraq-Saudi Arabia Neutral Zone" : "XE", + "Spratly Islands" : "XS", - # TODO "Disputed Territory" conflicts with `default_code` - # "Disputed Territory" : "XX", -} + # "Diamond Princess" : default_country_code, + # TODO "Disputed Territory" conflicts with `default_country_code` + # "Disputed Territory" : "XX", -# Mapping of alternative names, spelling, typos to the names of countries used -# by the ISO 3166-1 norm -synonyms = { - "Mainland China" : "China", - "Czechia" : "Czech Republic", - "Channel Islands" : "United Kingdom", - "Republic of Korea" : "Korea, Republic of", - "Republic of Moldova" : "Moldova, Republic of", - "Taiwan" : "Taiwan, Province of China", - "US" : "United States", - # TODO Macau is probably a typo. Report it to CSSEGISandData/COVID-19 - "Macau" : "Macao", - "Macao SAR" : "Macao", - "Vietnam" : "Viet Nam", - "UK" : "United Kingdom", - "Russia" : "Russian Federation", - "Iran (Islamic Republic of)" : "Iran, Islamic Republic of", - "Saint Barthelemy" : "Saint Barthélemy", - "Saint Martin" : "Saint Martin (French part)", - "Palestine" : "Palestine, State of", - "occupied Palestinian territory" : "Palestine, State of", - "State of Palestine" : "Palestine, State of", - "The West Bank and Gaza" : "Palestine, State of", - "Holy See" : "Holy See (Vatican City State)", - "Brunei" : "Brunei Darussalam", - "Hong Kong SAR" : "Hong Kong", - "Taipei and environs" : "Taiwan, Province of China", - "South Korea" : "Korea, Republic of", - "Iran" : "Iran, Islamic Republic of", - "Vatican City" : "Holy See (Vatican City State)", - "DR Congo" : "Congo, the Democratic Republic of the", - "Republic of the Congo" : "Congo", - "Tanzania" : "Tanzania, United Republic of", - "Venezuela" : "Venezuela, Bolivarian Republic of", - "North Korea" : "Korea, Democratic People's Republic of", - "Syria" : "Syrian Arab Republic", - "Bolivia" : "Bolivia, Plurinational State of", - "Laos" : "Lao People's Democratic Republic", - "Moldova" : "Moldova, Republic of", - "Eswatini" : "Swaziland", - "Cabo Verde" : "Cape Verde", - "Sao Tome & Principe" : "Sao Tome and Principe", - "Micronesia" : "Micronesia, Federated States of", - "St. Vincent & Grenadines" : "Saint Vincent and the Grenadines", - "U.S. Virgin Islands" : "Virgin Islands, U.S.", - "Saint Kitts & Nevis" : "Saint Kitts and Nevis", - "Faeroe Islands" : "Faroe Islands", - "Sint Maarten" : "Sint Maarten (Dutch part)", - "Turks and Caicos" : "Turks and Caicos Islands", - "Saint Martin" : "Saint Martin (French part)", - "British Virgin Islands" : "Virgin Islands, British", - "Wallis & Futuna" : "Wallis and Futuna", - "Saint Helena" : "Saint Helena, Ascension and Tristan da Cunha", - "Saint Pierre & Miquelon" : "Saint Pierre and Miquelon", - "Falkland Islands" : "Falkland Islands (Malvinas)", - "Republic of Ireland" : "Ireland", - "Ivory Coast" : "Côte d'Ivoire", - " Azerbaijan" : "Azerbaijan", - # Conjoin North Ireland on United Kingdom - "North Ireland" : "United Kingdom", - "East Timor" : "Timor-Leste", - "São Tomé and Príncipe" : "Sao Tome and Principe", - # Guernsey and Jersey form Channel Islands. Conjoin Guernsey on Jersey. - # Jersey has higher population. - # https://en.wikipedia.org/wiki/Channel_Islands - "Guernsey and Jersey" : "Jersey", - "Channel Islands" : "Jersey", - "Caribbean Netherlands" : "Bonaire, Sint Eustatius and Saba", - "F.S. Micronesia" : "Micronesia, Federated States of", - "Emirates" : "United Arab Emirates", - # "Bosnia–Herzegovina" : "Bosnia and Herzegovina", - "Bosnia" : "Bosnia and Herzegovina", - "Dominican Rep" : "Dominican Republic", - "Macedonia" : "North Macedonia", - "Korea, South" : "Korea, Republic of", - "Cote d'Ivoire" : "Côte d'Ivoire", - "St. Martin" : "Saint Martin (French part)", - "Congo (Kinshasa)" : "Congo, the Democratic Republic of the", - "Taiwan*" : "Taiwan, Province of China", - "Reunion" : "Réunion", - "Curacao" : "Curaçao", - "Congo (Brazzaville)" : "Congo", - "Deutschland" : "Germany", - "The Bahamas" : "Bahamas", - "The Gambia" : "Gambia", - "Kosovo" : "Kosovo, Republic of", - "Swaziland" : "Eswatini", - "Gambia, The" : "Gambia", - "Bahamas, The" : "Bahamas", # "Others" has no mapping, i.e. the default val is used # "Cruise Ship" has no mapping, i.e. the default val is used } # fmt: on -def country_code(country): +def country_code(s): """ Return two letter country code (Alpha-2) according to https://en.wikipedia.org/wiki/ISO_3166-1 Defaults to "XX". """ - # Look in synonyms if not found. - if not country in is_3166_1 and country in synonyms: - country = synonyms[country] - - # Get country or fallback to default_code. - country_code = is_3166_1.get(country, default_code) - - # Default picked? - if country_code == default_code: - LOGGER.warning(f"No country_code found for '{country}'. Using '{country_code}'!") + country_code = country_name__country_code.get(s, default_country_code) + if country_code == default_country_code: + LOGGER.warning(f"No country code found for '{s}'. Using '{country_code}'!") - # Return. return country_code diff --git a/app/utils/populations.py b/app/utils/populations.py index 8a78ec50..ea72c334 100644 --- a/app/utils/populations.py +++ b/app/utils/populations.py @@ -5,7 +5,7 @@ import requests from cachetools import TTLCache, cached -from .countrycodes import country_code +from .countries import country_code LOGGER = logging.getLogger(__name__) diff --git a/tasks.py b/tasks.py new file mode 100644 index 00000000..3ff5f24c --- /dev/null +++ b/tasks.py @@ -0,0 +1,66 @@ +""" +tasks.py +-------- +Project invoke tasks + +Available commands + invoke --list + invoke fmt + invoke sort + invoke check +""" +import invoke + +TARGETS_DESCRIPTION = "Paths/directories to format. [default: . ]" + + +@invoke.task(help={"targets": TARGETS_DESCRIPTION}) +def sort(ctx, targets="."): + """Sort module imports.""" + print("sorting imports ...") + args = ["isort", "-rc", "--atomic", targets] + ctx.run(" ".join(args)) + + +@invoke.task(pre=[sort], help={"targets": TARGETS_DESCRIPTION}) +def fmt(ctx, targets="."): + """Format python source code & sort imports.""" + print("formatting ...") + args = ["black", targets] + ctx.run(" ".join(args)) + + +@invoke.task +def check(ctx, fmt=False, sort=False, diff=False): # pylint: disable=redefined-outer-name + """Check code format and import order.""" + if not any([fmt, sort]): + fmt = True + sort = True + + fmt_args = ["black", "--check", "."] + sort_args = ["isort", "-rc", "--check", "."] + + if diff: + fmt_args.append("--diff") + sort_args.append("--diff") + + cmd_args = [] + if fmt: + cmd_args.extend(fmt_args) + if sort: + if cmd_args: + cmd_args.append("&") + cmd_args.extend(sort_args) + ctx.run(" ".join(cmd_args)) + + +@invoke.task +def lint(ctx): + """Run linter.""" + ctx.run(" ".join(["pylint", "app"])) + + +@invoke.task +def test(ctx): + """Run pytest tests.""" + ctx.run(" ".join(["pytest", "-v"])) diff --git a/tests/test_countries.py b/tests/test_countries.py new file mode 100644 index 00000000..2c9ba65e --- /dev/null +++ b/tests/test_countries.py @@ -0,0 +1,24 @@ +import pytest + +from app.utils import countries + + +""" +Todo: + * Test cases for capturing of stdout/stderr +""" + + +@pytest.mark.parametrize( + "country_name,expected_country_code", + [ + ("Germany", "DE"), + ("Bolivia, Plurinational State of", "BO"), + ("Korea, Democratic People's Republic of", "KP"), + ("US", "US"), + ("BlaBla", countries.default_country_code), + ("Others", countries.default_country_code), + ], +) +def test_countries_country_name__country_code(country_name, expected_country_code): + assert countries.country_code(country_name) == expected_country_code diff --git a/tests/test_countrycodes.py b/tests/test_countrycodes.py deleted file mode 100644 index 1b132266..00000000 --- a/tests/test_countrycodes.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest - -from app.utils import countrycodes - - -""" -Todo: - * Test cases for capturing of stdout/stderr -""" - - -@pytest.mark.parametrize( - "country_name,expected_country_code", - [ - ("Germany", "DE"), - ("Bolivia, Plurinational State of", "BO"), - ("Korea, Democratic People's Republic of", "KP"), - ("BlaBla", "XX"), - ], -) -def test_countrycodes_is_3166_1(country_name, expected_country_code): - assert countrycodes.country_code(country_name) == expected_country_code - - -@pytest.mark.parametrize( - "country_name_synonym, expected_country_code", - [("Deutschland", "DE"), ("Iran (Islamic Republic of)", "IR"), ("British Virgin Islands", "VG")], -) -def test_countrycodes_synonym(country_name_synonym, expected_country_code): - assert countrycodes.country_code(country_name_synonym) == expected_country_code diff --git a/tests/test_routes.py b/tests/test_routes.py index 9e1c03ef..48d804e5 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -6,8 +6,8 @@ import pytest from fastapi.testclient import TestClient -import app -from app import services +# import app +# from app import services from app.main import APP from .test_jhu import DATETIME_STRING, mocked_requests_get, mocked_strptime_isoformat @@ -22,11 +22,7 @@ class FlaskRoutesTest(unittest.TestCase): Store all integration testcases in one class to ensure app context """ - # load app context only once. - app = app.create_app() - def setUp(self): - self.client = FlaskRoutesTest.app.test_client() self.asgi_client = TestClient(APP) self.date = DATETIME_STRING @@ -48,36 +44,36 @@ def test_v1_confirmed(self, mock_request_get, mock_datetime): mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "confirmed" expected_json_output = self.read_file_v1(state=state) - return_data = self.client.get("/{}".format(state)).data.decode() + return_data = self.asgi_client.get("/{}".format(state)).json() - assert return_data == expected_json_output + assert return_data == json.loads(expected_json_output) def test_v1_deaths(self, mock_request_get, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "deaths" expected_json_output = self.read_file_v1(state=state) - return_data = self.client.get("/{}".format(state)).data.decode() + return_data = self.asgi_client.get("/{}".format(state)).json() - assert return_data == expected_json_output + assert return_data == json.loads(expected_json_output) def test_v1_recovered(self, mock_request_get, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "recovered" expected_json_output = self.read_file_v1(state=state) - return_data = self.client.get("/{}".format(state)).data.decode() + return_data = self.asgi_client.get("/{}".format(state)).json() - assert return_data == expected_json_output + assert return_data == json.loads(expected_json_output) def test_v1_all(self, mock_request_get, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "all" expected_json_output = self.read_file_v1(state=state) - return_data = self.client.get("/{}".format(state)).data.decode() - # print(return_data) - assert return_data == expected_json_output + return_data = self.asgi_client.get("/{}".format(state)).json() + + assert return_data == json.loads(expected_json_output) def test_v2_latest(self, mock_request_get, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING