diff --git a/app/data/__init__.py b/app/data/__init__.py index 60a75dac..76bcf2d7 100644 --- a/app/data/__init__.py +++ b/app/data/__init__.py @@ -3,19 +3,26 @@ from ..services.location.jhu import JhuLocationService from ..services.location.nyt import NYTLocationService -# Mapping of services to data-sources. -DATA_SOURCES = { - "jhu": JhuLocationService(), - "csbs": CSBSLocationService(), - "nyt": NYTLocationService(), -} -def data_source(source): +class Source: + + def __init__(self, source): + # Mapping of services to data-sources. + self.__DATA_SOURCES_LIST = { + "jhu": JhuLocationService(), + "csbs": CSBSLocationService(), + "nyt": NYTLocationService(), + } + + def all_data_source(self): + return self.__DATA_SOURCES_LIST + + def single_data_source(self, source): """ Retrieves the provided data-source service. :returns: The service. :rtype: LocationService """ - return DATA_SOURCES.get(source.lower()) + return self.__DATA_SOURCES_LIST.get(source.lower()) diff --git a/app/location/__init__.py b/app/location/__init__.py index 1da5e9e5..032f9a61 100644 --- a/app/location/__init__.py +++ b/app/location/__init__.py @@ -1,7 +1,7 @@ """app.location""" from ..coordinates import Coordinates -from ..utils import countries -from ..utils.populations import country_population +from ..utils import countries_population +from ..utils.populations import POPULATION # pylint: disable=redefined-builtin,invalid-name @@ -11,6 +11,7 @@ class Location: # pylint: disable=too-many-instance-attributes """ def __init__( + self, id, country, province, coordinates, last_updated, confirmed, deaths, recovered, ): # pylint: disable=too-many-arguments # General info. diff --git a/app/location/csbs.py b/app/location/csbs.py index 649e8b22..8b4d2937 100644 --- a/app/location/csbs.py +++ b/app/location/csbs.py @@ -1,6 +1,6 @@ """app.locations.csbs.py""" from . import Location - +from . import serialize class CSBSLocation(Location): """ @@ -25,19 +25,7 @@ def __init__(self, id, state, county, coordinates, last_updated, confirmed, deat self.state = state self.county = county - def serialize(self, timelines=False): # pylint: disable=arguments-differ,unused-argument - """ - Serializes the location into a dict. - - :returns: The serialized location. - :rtype: dict - """ - serialized = super().serialize() + + - # Update with new fields. - serialized.update( - {"state": self.state, "county": self.county,} - ) - # Return the serialized location. - return serialized diff --git a/app/location/nyt.py b/app/location/nyt.py index ad92212e..5c4bfef7 100644 --- a/app/location/nyt.py +++ b/app/location/nyt.py @@ -1,5 +1,6 @@ """app.locations.nyt.py""" from . import TimelinedLocation +from . import serialize class NYTLocation(TimelinedLocation): @@ -14,19 +15,4 @@ def __init__(self, id, state, county, coordinates, last_updated, timelines): self.state = state self.county = county - def serialize(self, timelines=False): # pylint: disable=arguments-differ,unused-argument - """ - Serializes the location into a dict. - - :returns: The serialized location. - :rtype: dict - """ - serialized = super().serialize(timelines) - - # Update with new fields. - serialized.update( - {"state": self.state, "county": self.county,} - ) - - # Return the serialized location. - return serialized + diff --git a/app/location/serialize.py b/app/location/serialize.py new file mode 100644 index 00000000..b8d1d9aa --- /dev/null +++ b/app/location/serialize.py @@ -0,0 +1,19 @@ + +class serialize: + def serialize(self, timelines=False): # pylint: disable=arguments-differ,unused-argument + """ + Serializes the location into a dict. + + :returns: The serialized location. + :rtype: dict + """ + serialized = super().serialize(timelines) + + # Update with new fields. + serialized.update( + {"state": self.state, "county": self.county,} + ) + + # Return the serialized location. + return serialized + diff --git a/app/main.py b/app/main.py index b9aff949..5f76dfed 100644 --- a/app/main.py +++ b/app/main.py @@ -14,7 +14,7 @@ from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from .config import get_settings -from .data import data_source +from .data import Source from .routers import V1, V2 from .utils.httputils import setup_client_session, teardown_client_session @@ -46,6 +46,7 @@ ####################### # Scout APM + if SETTINGS.scout_name: # pragma: no cover LOGGER.info(f"Adding Scout APM middleware for `{SETTINGS.scout_name}`") APP.add_middleware(ScoutMiddleware) @@ -67,14 +68,14 @@ ) APP.add_middleware(GZipMiddleware, minimum_size=1000) - +SOURCES = Source() @APP.middleware("http") async def add_datasource(request: Request, call_next): """ Attach the data source to the request.state. """ # Retrieve the datas ource from query param. - source = data_source(request.query_params.get("source", default="jhu")) + source = SOURCE.get_data_source(request.query_params.get("source", default="jhu")) # Abort with 404 if source cannot be found. if not source: diff --git a/app/routers/v2.py b/app/routers/v2.py index 31eb408c..a1405c3a 100644 --- a/app/routers/v2.py +++ b/app/routers/v2.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, HTTPException, Request -from ..data import DATA_SOURCES +from ..data import Source from ..models import LatestResponse, LocationResponse, LocationsResponse V2 = APIRouter() @@ -101,10 +101,10 @@ async def get_location_by_id( location = await request.state.source.get(id) return {"location": location.serialize(timelines)} - +SOURCE = Source() @V2.get("/sources") async def sources(): """ Retrieves a list of data-sources that are availble to use. """ - return {"sources": list(DATA_SOURCES.keys())} + return {"sources": list(SOURCE.all_data_source)} diff --git a/app/utils/countries_population.py b/app/utils/countries_population.py new file mode 100644 index 00000000..90154664 --- /dev/null +++ b/app/utils/countries_population.py @@ -0,0 +1,461 @@ +"""app.utils.countries.py""" +import logging +import json +import logging +import requests +import app.io + +class countries_population: + + LOGGER = logging.getLogger(__name__) + GEONAMES_URL = "http://api.geonames.org/countryInfoJSON" + GEONAMES_BACKUP_PATH = "geonames_population_mappings.json" + + + # Default country code. + 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 + COUNTRY_NAME__COUNTRY_CODE = { + "Afghanistan" : "AF", + "Åland Islands" : "AX", + "Albania" : "AL", + "Algeria" : "DZ", + "American Samoa" : "AS", + "Andorra" : "AD", + "Angola" : "AO", + "Anguilla" : "AI", + "Antarctica" : "AQ", + "Antigua and Barbuda" : "AG", + "Argentina" : "AR", + "Armenia" : "AM", + "Aruba" : "AW", + "Australia" : "AU", + "Austria" : "AT", + "Azerbaijan" : "AZ", + " Azerbaijan" : "AZ", + "Bahamas" : "BS", + "The Bahamas" : "BS", + "Bahamas, The" : "BS", + "Bahrain" : "BH", + "Bangladesh" : "BD", + "Barbados" : "BB", + "Belarus" : "BY", + "Belgium" : "BE", + "Belize" : "BZ", + "Benin" : "BJ", + "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", + "Cambodia" : "KH", + "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", + "Equatorial Guinea" : "GQ", + "Eritrea" : "ER", + "Estonia" : "EE", + "Ethiopia" : "ET", + "Falkland Islands (Malvinas)" : "FK", + "Falkland Islands" : "FK", + "Faroe Islands" : "FO", + "Faeroe Islands" : "FO", + "Fiji" : "FJ", + "Finland" : "FI", + "France" : "FR", + "French Guiana" : "GF", + "French Polynesia" : "PF", + "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", + "Greenland" : "GL", + "Grenada" : "GD", + "Guadeloupe" : "GP", + "Guam" : "GU", + "Guatemala" : "GT", + "Guernsey" : "GG", + "Guinea" : "GN", + "Guinea-Bissau" : "GW", + "Guyana" : "GY", + "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", + "Liberia" : "LR", + "Libya" : "LY", + "Liechtenstein" : "LI", + "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", + "Maldives" : "MV", + "Mali" : "ML", + "Malta" : "MT", + "Marshall Islands" : "MH", + "Martinique" : "MQ", + "Mauritania" : "MR", + "Mauritius" : "MU", + "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", + "Montserrat" : "MS", + "Morocco" : "MA", + "Mozambique" : "MZ", + "Myanmar" : "MM", + "Burma" : "MM", + "Namibia" : "NA", + "Nauru" : "NR", + "Nepal" : "NP", + "Netherlands" : "NL", + "New Caledonia" : "NC", + "New Zealand" : "NZ", + "Nicaragua" : "NI", + "Niger" : "NE", + "Nigeria" : "NG", + "Niue" : "NU", + "Norfolk Island" : "NF", + "Northern Mariana Islands" : "MP", + "Norway" : "NO", + "Oman" : "OM", + "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", + "Peru" : "PE", + "Philippines" : "PH", + "Pitcairn" : "PN", + "Poland" : "PL", + "Portugal" : "PT", + "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", + "Seychelles" : "SC", + "Sierra Leone" : "SL", + "Singapore" : "SG", + "Sint Maarten (Dutch part)" : "SX", + "Sint Maarten" : "SX", + "Slovakia" : "SK", + "Slovenia" : "SI", + "Solomon Islands" : "SB", + "Somalia" : "SO", + "South Africa" : "ZA", + "South Georgia and the South Sandwich Islands" : "GS", + "South Sudan" : "SS", + "Spain" : "ES", + "Sri Lanka" : "LK", + "Sudan" : "SD", + "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", + "Trinidad and Tobago" : "TT", + "Tunisia" : "TN", + "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", + "Zimbabwe" : "ZW", + + # 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", + + # "Diamond Princess" : default_country_code, + # TODO "Disputed Territory" conflicts with `default_country_code` + # "Disputed Territory" : "XX", + + # "Others" has no mapping, i.e. the default val is used + + # ships: + # "Cruise Ship" + # "MS Zaandam" + } + + # fmt: on + def country_code(value): + """ + Return two letter country code (Alpha-2) according to https://en.wikipedia.org/wiki/ISO_3166-1 + Defaults to "XX". + """ + code = COUNTRY_NAME__COUNTRY_CODE.get(value, DEFAULT_COUNTRY_CODE) + if code == DEFAULT_COUNTRY_CODE: + # log at sub DEBUG level + LOGGER.log(5, f"No country code found for '{value}'. Using '{code}'!") + + return code + + # Fetching of the populations. + def fetch_populations(save=False): + """ + Returns a dictionary containing the population of each country fetched from the GeoNames. + https://www.geonames.org/ + + TODO: only skip writing to the filesystem when deployed with gunicorn, or handle concurent access, or use DB. + + :returns: The mapping of populations. + :rtype: dict + """ + LOGGER.info("Fetching populations...") + + # Mapping of populations + mappings = {} + + # Fetch the countries. + try: + countries = requests.get(GEONAMES_URL, params={"username": "dperic"}, timeout=1.25).json()[ + "geonames" + ] + # Go through all the countries and perform the mapping. + for country in countries: + mappings.update({country["countryCode"]: int(country["population"]) or None}) + + if mappings and save: + LOGGER.info(f"Saving population data to {app.io.save(GEONAMES_BACKUP_PATH, mappings)}") + except (json.JSONDecodeError, KeyError, requests.exceptions.Timeout) as err: + LOGGER.warning(f"Error pulling population data. {err.__class__.__name__}: {err}") + mappings = app.io.load(GEONAMES_BACKUP_PATH) + LOGGER.info(f"Using backup data from {GEONAMES_BACKUP_PATH}") + # Finally, return the mappings. + LOGGER.info("Fetched populations") + return mappings + + + # Mapping of alpha-2 codes country codes to population. + POPULATIONS = fetch_populations() + + # Retrieving. + def country_population(country_code, default=None): + """ + Fetches the population of the country with the provided country code. + + :returns: The population. + :rtype: int + """ + return POPULATIONS.get(country_code, default) + + + __instance = None + def getInstance(): + """ Static access method. """ + if countries_population.__instance == None: + countries_population() + return countries_population.__instance + + def __init__(self): + """ Virtually private constructor. """ + if countries_population.__instance != None: + raise Exception("This class is a singleton!") + else: + countries_population.__instance = self + + if __name__ == '__countries__': + objects = countries_population(); + print(objects) + + objects = countries_population.getInstance() + print(objects) + diff --git a/app/utils/populations.py b/app/utils/populations.py index c02f15a9..d944df25 100644 --- a/app/utils/populations.py +++ b/app/utils/populations.py @@ -10,51 +10,60 @@ GEONAMES_URL = "http://api.geonames.org/countryInfoJSON" GEONAMES_BACKUP_PATH = "geonames_population_mappings.json" -# Fetching of the populations. -def fetch_populations(save=False): - """ - Returns a dictionary containing the population of each country fetched from the GeoNames. - https://www.geonames.org/ - - TODO: only skip writing to the filesystem when deployed with gunicorn, or handle concurent access, or use DB. - - :returns: The mapping of populations. - :rtype: dict - """ - LOGGER.info("Fetching populations...") - - # Mapping of populations - mappings = {} - - # Fetch the countries. - try: - countries = requests.get(GEONAMES_URL, params={"username": "dperic"}, timeout=1.25).json()[ - "geonames" - ] - # Go through all the countries and perform the mapping. - for country in countries: - mappings.update({country["countryCode"]: int(country["population"]) or None}) - - if mappings and save: - LOGGER.info(f"Saving population data to {app.io.save(GEONAMES_BACKUP_PATH, mappings)}") - except (json.JSONDecodeError, KeyError, requests.exceptions.Timeout) as err: - LOGGER.warning(f"Error pulling population data. {err.__class__.__name__}: {err}") - mappings = app.io.load(GEONAMES_BACKUP_PATH) - LOGGER.info(f"Using backup data from {GEONAMES_BACKUP_PATH}") - # Finally, return the mappings. - LOGGER.info("Fetched populations") - return mappings - - -# Mapping of alpha-2 codes country codes to population. -POPULATIONS = fetch_populations() - -# Retrieving. -def country_population(country_code, default=None): - """ - Fetches the population of the country with the provided country code. - - :returns: The population. - :rtype: int - """ - return POPULATIONS.get(country_code, default) + +class POPULATION: + # Fetching of the populations. + POPULATION = fetch_population() + + def __init__(self): + self.population = POPULATION + + def fetch_populations(save=False): + """ + Returns a dictionary containing the population of each country fetched from the GeoNames. + https://www.geonames.org/ + + TODO: only skip writing to the filesystem when deployed with gunicorn, or handle concurent access, or use DB. + + :returns: The mapping of populations. + :rtype: dict + """ + LOGGER.info("Fetching populations...") + + # Mapping of populations + mappings = {} + + # Fetch the countries. + try: + countries = requests.get(GEONAMES_URL, params={"username": "dperic"}, timeout=1.25).json()[ + "geonames" + ] + # Go through all the countries and perform the mapping. + for country in countries: + mappings.update({country["countryCode"]: int(country["population"]) or None}) + + if mappings and save: + LOGGER.info(f"Saving population data to {app.io.save(GEONAMES_BACKUP_PATH, mappings)}") + except (json.JSONDecodeError, KeyError, requests.exceptions.Timeout) as err: + LOGGER.warning(f"Error pulling population data. {err.__class__.__name__}: {err}") + mappings = app.io.load(GEONAMES_BACKUP_PATH) + LOGGER.info(f"Using backup data from {GEONAMES_BACKUP_PATH}") + # Finally, return the mappings. + LOGGER.info("Fetched populations") + return mappings + + + # Mapping of alpha-2 codes country codes to population. + POPULATIONS = fetch_populations() + + # Retrieving. + def get_country_population(country_code, default=None): + """ + Fetches the population of the country with the provided country code. + + :returns: The population. + :rtype: int + """ + return self.population.get(country_code, default) + + diff --git a/tests/test_countries.py b/tests/test_countries.py index a9ce9c19..a1e6e307 100644 --- a/tests/test_countries.py +++ b/tests/test_countries.py @@ -1,6 +1,6 @@ import pytest -from app.utils import countries +from app.utils import countries_population """ Todo: @@ -19,5 +19,20 @@ ("Others", countries.DEFAULT_COUNTRY_CODE), ], ) + +__instance = None +def getInstance(): + """ Static access method. """ + if countries_population.__instance == None: + countries_populaiton() + return countries_population.__instance + +def __init__(self): + """ Virtually private constructor. """ + if countries_population.__instance != None: + raise Exception("This class is a singleton!") + else: + countries_population.__instance = self + def test_countries_country_name__country_code(country_name, expected_country_code): assert countries.country_code(country_name) == expected_country_code