diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..c5f2c14 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,30 @@ +kind: pipeline +type: docker +name: default +steps: + - name: container-build + image: 'docker:dind' + environment: + USERNAME: + from_secret: docker_username + PASSWORD: + from_secret: docker_password + volumes: + - name: dockersock + path: /var/run/docker.sock + commands: + - >- + if [ "${DRONE_BRANCH}" = "main" ]; then export + BUILD_TAG="latest"; else export BUILD_TAG="${DRONE_BRANCH}"; fi; + - docker login -u $USERNAME -p $PASSWORD git.argideli.com + - >- + docker build -t + git.argideli.com/${DRONE_REPO_NAMESPACE}/${DRONE_REPO_NAME}:$BUILD_TAG + . + - >- + docker push + git.argideli.com/${DRONE_REPO_NAMESPACE}/${DRONE_REPO_NAME}:$BUILD_TAG +volumes: + - name: dockersock + host: + path: /var/run/docker.sock diff --git a/.gitignore b/.gitignore index 5d381cc..f8edd82 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ __pycache__/ # Distribution / packaging .Python +env/ +venv/ build/ develop-eggs/ dist/ @@ -160,3 +162,8 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +.vscode +.vscode/ +.cache.sqlite +.env +requirements-old.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..379d5bc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Use the official Python base image +FROM python:3.11-slim-buster + +# Set the working directory inside the container +WORKDIR /code + +# Copy the requirements file to the working directory +COPY ./requirements.txt /code/requirements.txt + +# Install the Python dependencies +RUN pip config --user set global.progress_bar off +RUN pip install --no-cache-dir --prefer-binary -r /code/requirements.txt + +# Copy the application code to the working directory +COPY ./*.py /code/ +COPY ./table_data /code/table_data +COPY ./dot_env /code/.env + + +# Expose the port on which the application will run +EXPOSE 80 + +# Run the FastAPI application using uvicorn server +#CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] +CMD ["uvicorn", "main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] \ No newline at end of file diff --git a/db_connector.py b/db_connector.py new file mode 100644 index 0000000..874db2a --- /dev/null +++ b/db_connector.py @@ -0,0 +1,34 @@ +import os +from dotenv import load_dotenv +from sqlalchemy.orm import declarative_base +from sqlalchemy import create_engine +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy_utils import database_exists, create_database + +#NOTE: Load environment variables from .env file +load_dotenv() +DATABASE = os.getenv('DATABASE') +DB_HOST = os.getenv('DB_HOST') +DB_USER = os.getenv('DB_USER') +DB_PASS = os.getenv('DB_PASS') + +DB_URI="postgresql://{}:{}@{}/{}".format(DB_USER, DB_PASS, DB_HOST, DATABASE) #, echo=True) + + +engine=create_engine(DB_URI) + +database=declarative_base() + +db_session=scoped_session(sessionmaker(bind=engine)) + +if not database_exists(engine.url): + create_database(engine.url) + +try: + connection = engine.connect() + print("\n\t\tConnected successfully to database: {}@{}\n".format(DATABASE, DB_HOST)) +except SQLAlchemyError as err: + print("\n\t\terror", err.__cause__, "\n") + + diff --git a/dot_env b/dot_env new file mode 100644 index 0000000..55fdb08 --- /dev/null +++ b/dot_env @@ -0,0 +1,4 @@ +DATABASE='weatherapp' +DB_HOST='10.1.1.1' +DB_USER='weatherapp' +DB_PASS='YEj5zxBsM4pZlDG6NrX2' \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..1f2bcbe --- /dev/null +++ b/main.py @@ -0,0 +1,107 @@ +from fastapi import FastAPI, status, HTTPException +from models import Location +from db_connector import database,engine +import utils + + +db_rebuild = True + +database.metadata.drop_all(engine) +if db_rebuild: + utils.initialize_database() + +app = FastAPI() + + +def error_400_handler(return_data): + """ + A function that handles errors with status code 400. Takes return_data as input. + Checks if the key 'error' exists in the data dictionary. If it does, it raises an HTTPException + with status code 400 and the value of the 'error' key as the detail. + """ + + if 'error' in return_data.keys(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={'error': return_data['error']} + ) + + +@app.get("/") +async def index_response(): + return {"Status": "OK"} + +@app.get("/locations") +async def get_location_weather(places: str): + """ + A function to retrieve weather data for specified locations. + + Parameters: + places (str): A string containing location identifiers separated by commas. + + Returns: + dict: A dictionary containing weather data for the specified locations. + """ + + #NOTE: Add option for fetching weather data for all locations, (debugging purposes) + if places != 'all': + places = [int(x) for x in list(places.split(","))] + + result = utils.retrieve_weather_data(places) + error_400_handler(result) + return result + + +@app.get("/locations/{id}") +async def get_weather_by_id(id: int): + """ + A function that retrieves weather data for a location by its ID. + + Parameters: + - id: an integer representing the ID of the location + + Returns: + - The weather data for the specified location + """ + + + result = utils.retrieve_weather_data([id]) + error_400_handler(result) + return result + + +@app.post("/locations", status_code=status.HTTP_201_CREATED) +async def add_location( + name: str = None, + longitude: float = None, + latitude: float = None, +): + """ + Add a new location to the database. + + Parameters: + - name (str): The name of the location. + - longitude (float): The longitude coordinate of the location. + - latitude (float): The latitude coordinate of the location. + """ + if len(name.encode('utf-8')) > 200: + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail={'name': 'Name cannot be longer than 200 characters'} + ) + else: + utils.add_location(Location(name=name,longitude=longitude,latitude=latitude)) + +@app.delete("/locations/{id}") +async def delete_location(id: int): + """ + Delete a location by its ID. + + Parameters: + id (int): The ID of the location to be deleted. + + Returns: + None + """ + + utils.delete_location(id) \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..633249a --- /dev/null +++ b/models.py @@ -0,0 +1,18 @@ +from sqlalchemy.sql.expression import null +from db_connector import database +from sqlalchemy import Integer, Column, Float, VARCHAR + +class Location(database): + #NOTE: Taumata longest place name on earth, 85 characters, + # also accounting for the extended ASCII characters, + # varchar of 200 Bytes should be enough for every use + # case even with multiword names such as "The Big Apple" + __tablename__='locations' + id=Column(Integer,primary_key=True,autoincrement=True) + name=Column(VARCHAR(200),nullable=False) + latitude=Column(Float,nullable=False) + longitude=Column(Float,nullable=False) + + + def __repr__(self): + return "id={} name={} longitude={} latitude={}".format(self.id,self.name,self.longitude,self.latitude) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2725e76 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[tool.ruff] +select = [ + "E", # pycodestyle + "F", # pyflakes + "UP", # pyupgrade + "I", # isort + "COM", # flake8-commas + "W", # pycodestyle +] + +fixable = ["I", "UP", "COM", "W"] +show-fixes = true +force-exclude = true +line-length = 120 +target-version = "py310" +extend-exclude = ["./alembic", "./.vscode", "./.devcontainer", "Dockerfile"] +extend-ignore = [ + 'E402', + 'E722', + 'W191', + 'W293', + 'C901' +] +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.black] +line-length = 120 +target-version = ['py310'] +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | data + | notebooks + | dist + | alembic + | \.devcontainer + | \.vscode + # The following are specific to Black, you probably don't want those. + | blib2to3 + | tests/data + | profiling +)/ +''' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fd2406b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,41 @@ +annotated-types==0.6.0 +anyio==4.3.0 +attrs==23.2.0 +black==24.2.0 +cattrs==23.2.3 +certifi==2024.2.2 +charset-normalizer==3.3.2 +click==8.1.7 +fastapi==0.110.0 +flatbuffers==24.3.25 +greenlet==3.0.3 +h11==0.14.0 +idna==3.6 +mypy-extensions==1.0.0 +numpy==1.26.4 +openmeteo_requests==1.2.0 +openmeteo_sdk==1.11.3 +packaging==24.0 +pandas==2.2.1 +pathspec==0.12.1 +platformdirs==4.2.0 +psycopg2-binary==2.9.9 +pydantic==2.6.4 +pydantic_core==2.16.3 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +pytz==2024.1 +requests==2.31.0 +requests-cache==1.2.0 +retry-requests==2.0.0 +ruff==0.3.0 +six==1.16.0 +sniffio==1.3.1 +SQLAlchemy==2.0.29 +SQLAlchemy-Utils==0.41.2 +starlette==0.36.3 +typing_extensions==4.11.0 +tzdata==2024.1 +url-normalize==1.4.3 +urllib3==2.2.1 +uvicorn==0.27.1 diff --git a/table_data/locations.csv b/table_data/locations.csv new file mode 100644 index 0000000..8f183b7 --- /dev/null +++ b/table_data/locations.csv @@ -0,0 +1,235 @@ +Country,Capital City,Latitude,Longitude,Population,Capital Type +Afghanistan,Kabul,34.5289,69.1725,4011770,Capital +Albania,Tiranë (Tirana),41.3275,19.8189,475577,Capital +Algeria,El Djazaïr (Algiers),36.7525,3.0420,2693542,Capital +American Samoa,Pago Pago,-14.2781,-170.7025,48526,Capital +Andorra,Andorra la Vella,42.5078,1.5211,22614,Capital +Angola,Luanda,-8.8368,13.2343,7774200,Capital +Anguilla,The Valley,18.2170,-63.0578,1402,Capital +Antigua and Barbuda,St. John's,17.1172,-61.8457,20764,Capital +Argentina,Buenos Aires,-34.6051,-58.4004,14966530,Capital +Armenia,Yerevan,40.1820,44.5146,1080324,Capital +Aruba,Oranjestad,12.5240,-70.0270,29877,Capital +Australia,Canberra,-35.2835,149.1281,447692,Capital +Austria,Wien (Vienna),48.2064,16.3707,1900547,Capital +Azerbaijan,Baku,40.3777,49.8920,2285729,Capital +Bahamas,Nassau,25.0582,-77.3431,279668,Capital +Bahrain,Al-Manamah (Manama),26.2154,50.5832,564631,Capital +Bangladesh,Dhaka,23.7104,90.4074,19578421,Capital +Barbados,Bridgetown,13.1000,-59.6167,89201,Capital +Belarus,Minsk,53.9000,27.5667,2004672,Capital +Belgium,Bruxelles-Brussel,50.8467,4.3499,2049510,Capital +Belize,Belmopan,17.2500,-88.7667,22964,Capital +Benin,Cotonou,6.3654,2.4183,685458,Economic Capital +Bermuda,Hamilton,32.2915,-64.7780,10073,Capital +Bhutan,Thimphu,27.4661,89.6419,203297,Capital +Bolivia (Plurinational State of),La Paz,-16.5000,-68.1500,1814087,Seat of Government +Bosnia and Herzegovina,Sarajevo,43.8486,18.3564,342577,Capital +Botswana,Gaborone,-24.6545,25.9086,269338,Capital +Brazil,Brasília,-15.7797,-47.9297,4469585,Capital +British Virgin Islands,Road Town,18.4167,-64.6167,15137,Capital +Brunei Darussalam,Bandar Seri Begawan,4.9403,114.9481,40781,Capital +Bulgaria,Sofia,42.6975,23.3242,1272418,Capital +Burkina Faso,Ouagadougou,12.3642,-1.5383,2531381,Capital +Burundi,Bujumbura,-3.3822,29.3644,898968,Capital +Cabo Verde,Praia,14.9215,-23.5087,167504,Capital +Cambodia,Phnum Pénh (Phnom Penh),11.5625,104.9160,1952329,Capital +Cameroon,Yaoundé,3.8667,11.5167,3655656,Capital +Canada,Ottawa-Gatineau,45.4166,-75.6980,1363159,Capital +Caribbean Netherlands,Kralendijk,12.1500,-68.2667,11313,Capital +Cayman Islands,George Town,19.2866,-81.3744,34875,Capital +Central African Republic,Bangui,4.3612,18.5550,850946,Capital +Chad,N'Djaména,12.1067,15.0444,1322679,Capital +Channel Islands,St. Helier,49.1880,-2.1049,34386,Capital +Channel Islands,St. Peter Port,49.4598,-2.5353,16271,Capital +Chile,Santiago,-33.4569,-70.6483,6680371,Capital +China,Beijing,39.9075,116.3972,19617963,Capital +"China, Hong Kong SAR",Hong Kong,22.2796,114.1887,7428887,Capital +"China, Macao SAR",Macao,22.2006,113.5461,632418,Capital +"China, Taiwan Province of China",Taibei,25.0470,121.5457,2705791,Others +Colombia,Bogotá,4.6097,-74.0818,10574409,Capital +Comoros,Moroni,-11.7022,43.2551,62351,Capital +Congo,Brazzaville,-4.2658,15.2832,2229693,Capital +Cook Islands,Rarotonga,-21.2300,-159.7600,13067,Capital +Costa Rica,San José,9.9278,-84.0807,1357745,Capital +Côte d'Ivoire,Abidjan,5.3453,-4.0268,4920776,Administrative Capital +Croatia,Zagreb,45.8144,15.9780,685587,Capital +Cuba,La Habana (Havana),23.1195,-82.3785,2136468,Capital +Curaçao,Willemstad,12.1084,-68.9335,144037,Capital +Cyprus,Lefkosia (Nicosia),35.1595,33.3669,269469,Capital +Czechia,Praha (Prague),50.0880,14.4208,1291552,Capital +Dem. People's Republic of Korea,P'yongyang,39.0339,125.7543,3037862,Capital +Democratic Republic of the Congo,Kinshasa,-4.3276,15.3136,13171256,Capital +Denmark,København (Copenhagen),55.6759,12.5655,1320826,Capital +Djibouti,Djibouti,11.5877,43.1447,561564,Capital +Dominica,Roseau,15.3017,-61.3881,14942,Capital +Dominican Republic,Santo Domingo,18.4896,-69.9018,3172152,Capital +Ecuador,Quito,-0.2299,-78.5250,1822397,Capital +Egypt,Al-Qahirah (Cairo),30.0392,31.2394,20076002,Capital +El Salvador,San Salvador,13.6894,-89.1872,1106698,Capital +Equatorial Guinea,Malabo,3.7500,8.7833,296770,Capital +Eritrea,Asmara,15.3333,38.9333,895863,Capital +Estonia,Tallinn,59.4370,24.7535,437027,Capital +Ethiopia,Addis Ababa,9.0250,38.7469,4399674,Capital +Faeroe Islands,Tórshavn,62.0097,-6.7716,20817,Capital +Falkland Islands (Malvinas),Stanley,-51.7012,-57.8494,2269,Others +Fiji,Suva,-18.1416,178.4415,178339,Capital +Finland,Helsinki,60.1692,24.9402,1279096,Capital +France,Paris,48.8534,2.3488,10900952,Capital +French Guiana,Cayenne,4.9333,-52.3333,57506,Capital +French Polynesia,Papeete,-17.5333,-149.5667,136005,Capital +Gabon,Libreville,0.3925,9.4537,813489,Capital +Gambia,Banjul,13.4531,-16.6794,437161,Capital +Georgia,Tbilisi,41.6941,44.8337,1077333,Capital +Germany,Berlin,52.5244,13.4105,3552123,Capital +Ghana,Accra,5.5560,-0.1969,2439389,Capital +Gibraltar,Gibraltar,36.1447,-5.3526,34733,Capital +Greece,Athínai (Athens),37.9534,23.7490,3155600,Capital +Greenland,Nuuk (Godthåb),64.1835,-51.7216,18406,Capital +Grenada,St.George's,12.0564,-61.7485,39297,Capital +Guadeloupe,Basse-Terre,15.9985,-61.7255,58397,Capital +Guam,Hagåtña,13.4757,144.7489,146905,Capital +Guatemala,Ciudad de Guatemala (Guatemala City),14.6127,-90.5307,2851104,Capital +Guinea,Conakry,9.5716,-13.6476,1843121,Capital +Guinea-Bissau,Bissau,11.8636,-15.5977,558399,Capital +Guyana,Georgetown,6.8045,-58.1553,109934,Capital +Haiti,Port-au-Prince,18.5392,-72.3350,2636763,Capital +Holy See,Vatican City,41.9024,12.4533,801,Capital +Honduras,Tegucigalpa,14.0818,-87.2068,1363041,Capital +Hungary,Budapest,47.4980,19.0399,1759497,Capital +Iceland,Reykjavík,64.1355,-21.8954,216364,Capital +India,Delhi,28.6667,77.2167,28513682,Capital +Indonesia,Jakarta,-6.2118,106.8416,10516927,Capital +Iran (Islamic Republic of),Tehran,35.6944,51.4215,8895947,Capital +Iraq,Baghdad,33.3406,44.4009,6811955,Capital +Ireland,Dublin,53.3331,-6.2489,1201426,Capital +Isle of Man,Douglas,54.1500,-4.4833,27171,Capital +Israel,Jerusalem,31.7690,35.2163,907062,Capital +Italy,Roma (Rome),41.8947,12.4811,4209710,Capital +Jamaica,Kingston,17.9970,-76.7936,589083,Capital +Japan,Tokyo,35.6895,139.6917,37468302,Capital +Jordan,Amman,31.9552,35.9450,2064582,Capital +Kazakhstan,Astana,51.1801,71.4460,1068113,Capital +Kenya,Nairobi,-1.2833,36.8167,4385853,Capital +Kiribati,Tarawa,1.3272,172.9813,64011,Capital +Kuwait,Al Kuwayt (Kuwait City),29.3697,47.9783,2989270,Capital +Kyrgyzstan,Bishkek,42.8700,74.5900,996319,Capital +Lao People's Democratic Republic,Vientiane,17.9667,102.6000,664754,Capital +Latvia,Riga,56.9460,24.1059,637089,Capital +Lebanon,Bayrut (Beirut),33.9000,35.4833,2385271,Capital +Lesotho,Maseru,-29.3167,27.4833,201851,Capital +Liberia,Monrovia,6.3005,-10.7969,1418300,Capital +Libya,Tarabulus (Tripoli),32.8752,13.1875,1157746,Capital +Liechtenstein,Vaduz,47.1415,9.5215,5470,Capital +Lithuania,Vilnius,54.6892,25.2798,536055,Capital +Luxembourg,Luxembourg,49.6117,6.1300,119752,Capital +Madagascar,Antananarivo,-18.9137,47.5361,3058387,Capital +Malawi,Lilongwe,-13.9669,33.7873,1029639,Capital +Malaysia,Kuala Lumpur,3.1412,101.6865,7563912,Capital +Maldives,Male,4.1748,73.5089,176851,Capital +Mali,Bamako,12.6500,-8.0000,2446749,Capital +Malta,Valletta,35.8997,14.5147,212768,Capital +Marshall Islands,Majuro,7.0897,171.3803,30661,Capital +Martinique,Fort-de-France,14.6089,-61.0733,79361,Capital +Mauritania,Nouakchott,18.0858,-15.9785,1205414,Capital +Mauritius,Port Louis,-20.1619,57.4989,149365,Capital +Mayotte,Mamoudzou,-12.7794,45.2272,6180,Capital +Mexico,Ciudad de México (Mexico City),19.4273,-99.1419,21580827,Capital +Micronesia (Fed. States of),Palikir,6.9174,158.1588,6996,Capital +Monaco,Monaco,43.7333,7.4167,38897,Capital +Mongolia,Ulaanbaatar,47.9077,106.8832,1520381,Capital +Montenegro,Podgorica,42.4411,19.2636,177177,Capital +Montserrat,Brades Estate,16.7918,-62.2106,472,Capital +Morocco,Rabat,34.0133,-6.8326,1846661,Capital +Mozambique,Maputo,-25.9653,32.5892,1101771,Capital +Myanmar,Nay Pyi Taw,19.7450,96.1297,500218,Capital +Namibia,Windhoek,-22.5594,17.0832,404280,Capital +Nauru,Nauru,-0.5308,166.9112,11312,Capital +Nepal,Kathmandu,27.7017,85.3206,1329732,Capital +Netherlands,Amsterdam,52.3740,4.8897,1131690,Capital +New Caledonia,Nouméa,-22.2763,166.4572,197787,Capital +New Zealand,Wellington,-41.2866,174.7756,411346,Capital +Nicaragua,Managua,12.1328,-86.2504,1047923,Capital +Niger,Niamey,13.5137,2.1098,1213781,Capital +Nigeria,Abuja,9.0574,7.4898,2918518,Capital +Niue,Alofi,-19.0585,-169.9213,727,Capital +Northern Mariana Islands,Saipan,15.2123,145.7545,50568,Capital +Norway,Oslo,59.9127,10.7461,1012225,Capital +Oman,Masqat (Muscat),23.6139,58.5922,1446563,Capital +Pakistan,Islamabad,33.7035,73.0594,1061412,Capital +Palau,Koror,7.3426,134.4789,11410,Capital +Panama,Ciudad de Panamá (Panama City),8.9958,-79.5196,1783490,Capital +Papua New Guinea,Port Moresby,-9.4431,147.1797,366862,Capital +Paraguay,Asunción,-25.3007,-57.6359,3222199,Capital +Peru,Lima,-12.0432,-77.0282,10390607,Capital +Philippines,Manila,14.6042,120.9822,13482468,Capital +Poland,Warszawa (Warsaw),52.2298,21.0118,1767798,Capital +Portugal,Lisboa (Lisbon),38.7169,-9.1399,2927316,Capital +Puerto Rico,San Juan,18.4663,-66.1057,2454337,Capital +Qatar,Ad-Dawhah (Doha),25.2747,51.5245,633401,Capital +Republic of Korea,Seoul,37.5683,126.9778,9963497,Capital +Republic of Moldova,Chişinău,47.0056,28.8575,509707,Capital +Réunion,Saint-Denis,-20.8823,55.4504,147209,Capital +Romania,Bucuresti (Bucharest),44.4328,26.1043,1821380,Capital +Russian Federation,Moskva (Moscow),55.7550,37.6218,12409738,Capital +Rwanda,Kigali,-1.9474,30.0579,1057836,Capital +Saint Helena,Jamestown,-15.9387,-5.7168,603,Capital +Saint Kitts and Nevis,Basseterre,17.2948,-62.7261,14434,Capital +Saint Lucia,Castries,14.0060,-60.9910,22258,Capital +Saint Pierre and Miquelon,Saint-Pierre,46.7738,-56.1815,5723,Capital +Saint Vincent and the Grenadines,Kingstown,13.1587,-61.2248,26636,Capital +Samoa,Apia,-13.8333,-171.7667,36066,Capital +San Marino,San Marino,43.9333,12.4500,4465,Capital +Sao Tome and Principe,São Tomé,0.3365,6.7273,80099,Capital +Saudi Arabia,Ar-Riyadh (Riyadh),24.6905,46.7096,6906595,Capital +Senegal,Dakar,14.6937,-17.4441,2978419,Capital +Serbia,Beograd (Belgrade),44.8176,20.4633,1389351,Capital +Seychelles,Victoria,-4.6167,55.4500,28091,Capital +Sierra Leone,Freetown,8.4840,-13.2299,1135949,Capital +Singapore,Singapore,1.2897,103.8501,5791901,Capital +Sint Maarten (Dutch part),Philipsburg,18.0260,-63.0458,40552,Capital +Slovakia,Bratislava,48.1482,17.1067,429920,Capital +Slovenia,Ljubljana,46.0511,14.5051,286491,Capital +Solomon Islands,Honiara,-9.4333,159.9500,81801,Capital +Somalia,Muqdisho (Mogadishu),2.0416,45.3435,2081624,Capital +South Africa,Cape Town,-33.9258,18.4232,4430367,Legislative Capital +South Sudan,Juba,4.8517,31.5825,368914,Capital +Spain,Madrid,40.4165,-3.7026,6497124,Capital +Sri Lanka,Colombo,6.9319,79.8478,599821,Capital +State of Palestine,Al-Quds[East Jerusalem],31.7834,35.2339,275086,Capital +Sudan,Al-Khartum (Khartoum),15.5518,32.5324,5534079,Capital +Suriname,Paramaribo,5.8664,-55.1668,239457,Capital +Swaziland,Mbabane,-26.3167,31.1333,68010,Capital +Sweden,Stockholm,59.3326,18.0649,1582968,Capital +Switzerland,Bern,46.9481,7.4474,422153,Capital +Syrian Arab Republic,Dimashq (Damascus),33.5086,36.3084,2319545,Capital +Tajikistan,Dushanbe,38.5358,68.7791,872653,Capital +TFYR Macedonia,Skopje,42.0000,21.4333,584208,Capital +Thailand,Krung Thep (Bangkok),13.7220,100.5252,10156316,Capital +Timor-Leste,Dili,-8.5601,125.5668,281135,Capital +Togo,Lomé,6.1375,1.2123,1745744,Capital +Tokelau,Tokelau,-9.3800,-171.2500,0,Others +Tonga,Nuku'alofa,-21.1394,-175.2032,22904,Capital +Trinidad and Tobago,Port of Spain,10.6662,-61.5166,544417,Capital +Tunisia,Tunis,36.8190,10.1658,2290777,Capital +Turkey,Ankara,39.9199,32.8543,4919074,Capital +Turkmenistan,Ashgabat,37.9500,58.3833,810186,Capital +Turks and Caicos Islands,Cockburn Town,21.4612,-71.1419,5447,Capital +Tuvalu,Funafuti,-8.5189,179.1991,7042,Capital +Uganda,Kampala,0.3163,32.5822,2986352,Capital +Ukraine,Kyiv (Kiev),50.4454,30.5186,2956706,Capital +United Arab Emirates,Abu Zaby (Abu Dhabi),24.4648,54.3618,1419699,Capital +United Kingdom,London,51.5085,-0.1257,9046485,Capital +United Republic of Tanzania,Dodoma,-6.1722,35.7395,261645,Capital +United States of America,"Washington, D.C.",38.8951,-77.0364,5206593,Capital +United States Virgin Islands,Charlotte Amalie,18.3419,-64.9307,52322,Capital +Uruguay,Montevideo,-34.8335,-56.1674,1736989,Capital +Uzbekistan,Tashkent,41.2647,69.2163,2463969,Capital +Vanuatu,Port Vila,-17.7338,168.3219,52690,Capital +Venezuela (Bolivarian Republic of),Caracas,10.4880,-66.8792,2934560,Capital +Viet Nam,Hà Noi,21.0245,105.8412,4282738,Capital +Wallis and Futuna Islands,Matu-Utu,-13.2816,-176.1745,1025,Capital +Western Sahara,El Aaiún,27.1532,-13.2014,232388,Capital +Yemen,Sana'a',15.3531,44.2078,2779317,Capital +Zambia,Lusaka,-15.4134,28.2771,2523844,Capital +Zimbabwe,Harare,-17.8294,31.0539,1515016,Capital \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..4f7e590 --- /dev/null +++ b/utils.py @@ -0,0 +1,192 @@ +from models import Location +from db_connector import database,engine,db_session +import pandas as pd +import openmeteo_requests +import requests_cache +from retry_requests import retry + + +def initialize_database(): + """ + A function to initialize the database by checking table availability and creating it if it does not exist. + """ + + db_tables = ['locations'] + # check table availability and create it if it does not exist + for tb in db_tables: + if not engine.dialect.has_table(engine.connect(), tb): + print("Creating table: {}\n".format(tb)) + database.metadata.create_all(engine) + + table_data = pd.read_csv ('./table_data/locations.csv', index_col=None, header=0) + for i in range(len(table_data)): + add_location(Location(name=table_data.loc[i, "Capital City"], + latitude=float(table_data.loc[i, "Latitude"]), + longitude=float(table_data.loc[i, "Longitude"]), + ), + no_commit=True + ) + db_session.commit() + +def add_location(location:Location , no_commit=False): + """ + A function that adds a location to the database session. + + Parameters: + location (Location): The location object to be added to the database session. + no_commit (bool): Flag indicating whether to commit the transaction immediately. + + Returns: + None + """ + + db_session.add(location) + #print("Adding location: {}".format(location)) + if not no_commit: + db_session.commit() + + +def delete_location(id): + """ + Deletes a location from the database based on the provided ID. + + Parameters: + id (int): The ID of the location to be deleted. + + Returns: + dict: A dictionary with the key "id" indicating that the location was successfully deleted. + """ + + db_session.query(Location).filter(Location.id == id).delete() + db_session.commit() + return {"id": "Deleted"} + + +def chunkify_data(data, chunk_size): + """ + A function that chunks the input data into smaller pieces of the specified chunk size. + + Parameters: + - data: the input data to be chunked + - chunk_size: the size of each chunk + + Returns: + - A generator that yields chunks of the input data + """ + + for i in range(0, len(data), chunk_size): + yield data[i:i + chunk_size] + + +def retrieve_weather_data(location_id=None): + """ + A function to retrieve weather data based on location IDs. + + Parameters: + - location_id: an optional parameter to specify the location ID(s) to retrieve weather data for. If 'all' is provided, data for all locations is returned. + + Returns: + - If location_id is not 'all' and any specified location ID does not exist, a dictionary of invalid location IDs is returned. + - If location_id is None, None is returned. + - If location_id is 'all', weather data for all locations is returned in a dictionary. + - Otherwise, weather data for the specified location IDs is returned in a dictionary. + """ + print('xaz') + max_id = db_session.query(Location).count() + + #NOTE: Disable location check if location_id is 'all' (returns all locations), debugging purposes + if location_id != 'all': + loc_check = {} + for loc_id in location_id: + if loc_id > max_id: + loc_check[loc_id] = "The location does not exist" + print(len(loc_check)) + if len(loc_check) > 0: + return {'error': loc_check} + + print('az') + weather_data = {} + + #NOTE: Get weather data for all locations if location_id is 'all' (debugging purposes), otherwise get weather data for specified locations + if location_id == 'all': + locations = list(chunkify_data(db_session.query(Location).all(),100)) + else: + locations = list(chunkify_data(db_session.query(Location).filter(Location.id.in_(location_id)).all(),100)) + print(locations) + for chunk in locations: + coordinates = [[loc.id, loc.latitude, loc.longitude, loc.name] for loc in chunk] + weather_data.update(get_openmeteo_data(coordinates)) + #print(weather_data) + #exit(0) + return weather_data + +def get_openmeteo_data(coordinates): + """ + Retrieves weather data from the OpenMeteo API for the given coordinates. + + Parameters: + coordinates (list): List of tuples containing latitude, longitude, and location name. + + Returns: + dict: A dictionary containing the retrieved weather data for each location. + """ + + data_dict = {} + cache_session = requests_cache.CachedSession('.cache', expire_after = 3600) + retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2) + openmeteo_client = openmeteo_requests.Client(session = retry_session) + + latitude = [coords[1] for coords in coordinates] + longitude = [coords[2] for coords in coordinates] + + # NOTE: Uncomment to get all available weather data for each location + # data_names = [ + # "weather_code", "temperature_2m_max", "temperature_2m_min", + # "apparent_temperature_max", "apparent_temperature_min", + # "sunrise", "sunset", "daylight_duration", "sunshine_duration", + # "uv_index_max", "uv_index_clear_sky_max", "precipitation_sum", + # "rain_sum", "showers_sum", "snowfall_sum", "precipitation_hours", + # "precipitation_probability_max", "wind_speed_10m_max", + # "wind_gusts_10m_max", "wind_direction_10m_dominant", + # "shortwave_radiation_sum", "et0_fao_evapotranspiration" + # ] + + data_names = ["temperature_2m_max", "temperature_2m_min", "precipitation_sum"] + + #NOTE: OpenMeteo API has a limit of 10000 requests per day, + # so in a production environment it would be wise to change to + # an enterprise account OR utilize the clients for the requests + url = "https://api.open-meteo.com/v1/forecast" + params = { + "latitude": latitude, + "longitude": longitude, + "daily": data_names + } + + responses = openmeteo_client.weather_api(url, params=params) + + for idx, response in enumerate(responses, start=1): + location = coordinates[idx-1][3] + idx = coordinates[idx-1][0] + + daily = response.Daily() + + daily_data = { variable: daily.Variables(i).ValuesAsNumpy().tolist() + if variable not in ["sunrise", "sunset"] + else daily.Variables(i).ValuesAsNumpy() + for i, variable in enumerate(data_names) + } + + dates = pd.date_range(start = pd.to_datetime(daily.Time(), unit = "s", utc = True), + end = pd.to_datetime(daily.TimeEnd(), unit = "s", utc = True), + freq = pd.Timedelta(seconds = daily.Interval()), + inclusive = "left").tolist() + + daily_data["date"] = [date.strftime("%d/%m/%Y") for date in dates] + daily_data["location_name"] = location + daily_data["resp_coordinates"] = [response.Latitude(), response.Longitude()] + + data_dict[idx] = daily_data + + return data_dict +