From 5f1a422ac1f5348349d3aff09729521844ebf368 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sat, 21 Mar 2020 19:15:43 -0400 Subject: [PATCH 01/30] add FastAPI uvicorn --- Pipfile | 6 ++ Pipfile.lock | 160 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 111 insertions(+), 55 deletions(-) diff --git a/Pipfile b/Pipfile index 104c27c8..297fb5cb 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,7 @@ pytest = "*" pylint = "*" [packages] +fastapi = "*" flask = "*" python-dotenv = "*" requests = "*" @@ -16,6 +17,11 @@ gunicorn = "*" flask-cors = "*" cachetools = "*" python-dateutil = "*" +uvicorn = "*" [requires] python_version = "3.8" + +[scripts] +dev_app = "uvicorn app.main:APP --reload" +app = "uvicorn app.main:APP" \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock index bcc795ad..d505e349 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "3ca964b855d418f59464ea8c7de126e18ab3f8ff5c7142d774468f95d9a1156c" + "sha256": "846c10a9cdea8ecb7482b41acd826e486578f42a7443022155bd6484f104376b" }, "pipfile-spec": 6, "requires": { @@ -45,6 +45,14 @@ ], "version": "==7.1.1" }, + "fastapi": { + "hashes": [ + "sha256:532648b4e16dd33673d71dc0b35dff1b4d20c709d04078010e258b9f3a79771a", + "sha256:721b11d8ffde52c669f52741b6d9d761fe2e98778586f4cfd6f5e47254ba5016" + ], + "index": "pypi", + "version": "==0.52.0" + }, "flask": { "hashes": [ "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", @@ -69,6 +77,13 @@ "index": "pypi", "version": "==20.0.4" }, + "h11": { + "hashes": [ + "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1", + "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1" + ], + "version": "==0.9.0" + }, "idna": { "hashes": [ "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", @@ -128,6 +143,25 @@ ], "version": "==1.1.1" }, + "pydantic": { + "hashes": [ + "sha256:012c422859bac2e03ab3151ea6624fecf0e249486be7eb8c6ee69c91740c6752", + "sha256:07911aab70f3bc52bb845ce1748569c5e70478ac977e106a150dd9d0465ebf04", + "sha256:47b8db7024ba3d46c3d4768535e1cf87b6c8cf92ccd81e76f4e1cb8ee47688b3", + "sha256:50e4e948892a6815649ad5a9a9379ad1e5f090f17842ac206535dfaed75c6f2f", + "sha256:51f11c8bbf794a68086540da099aae4a9107447c7a9d63151edbb7d50110cf21", + "sha256:6100d7862371115c40be55cc4b8d766a74b1d0dbaf99dbfe72bb4bac0faf89ed", + "sha256:61d22d36808087d3184ed6ac0d91dd71c533b66addb02e4a9930e1e30833202f", + "sha256:72184c1421103cca128300120f8f1185fb42a9ea73a1c9845b1c53db8c026a7d", + "sha256:831a0265a9e3933b3d0f04d1a81bba543bafbe4119c183ff2771871db70524ab", + "sha256:8848b4eb458469739126e4c1a202d723dd092e087f8dbe3104371335f87ba5df", + "sha256:bbbed364376f4a0aebb9ea452ff7968b306499a9e74f4db69b28ff2cd4043a11", + "sha256:e27559cedbd7f59d2375bfd6eea29a330ea1a5b0589c34d6b4e0d7bec6027bbf", + "sha256:f17ec336e64d4583311249fb179528e9a2c27c8a2eaf590ec6ec2c6dece7cb3f", + "sha256:f863456d3d4bf817f2e5248553dee3974c5dc796f48e6ddb599383570f4215ac" + ], + "version": "==1.4" + }, "python-dateutil": { "hashes": [ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", @@ -159,6 +193,13 @@ ], "version": "==1.14.0" }, + "starlette": { + "hashes": [ + "sha256:6169ee78ded501095d1dda7b141a1dc9f9934d37ad23196e180150ace2c6449b", + "sha256:a9bb130fa7aa736eda8a814b6ceb85ccf7a209ed53843d0d61e246b380afa10f" + ], + "version": "==0.13.2" + }, "urllib3": { "hashes": [ "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", @@ -166,6 +207,41 @@ ], "version": "==1.25.8" }, + "uvicorn": { + "hashes": [ + "sha256:0f58170165c4495f563d8224b2f415a0829af0412baa034d6f777904613087fd", + "sha256:6fdaf8e53bf1b2ddf0fe9ed06079b5348d7d1d87b3365fe2549e6de0d49e631c" + ], + "index": "pypi", + "version": "==0.11.3" + }, + "websockets": { + "hashes": [ + "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", + "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", + "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", + "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", + "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", + "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", + "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", + "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", + "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", + "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", + "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", + "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", + "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", + "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", + "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", + "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", + "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", + "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", + "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", + "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", + "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", + "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" + ], + "version": "==8.1" + }, "werkzeug": { "hashes": [ "sha256:169ba8a33788476292d04186ab33b01d6add475033dfc07215e6d219cc077096", @@ -182,6 +258,14 @@ ], "version": "==2.3.3" }, + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "markers": "sys_platform == 'win32'", + "version": "==1.3.0" + }, "attrs": { "hashes": [ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", @@ -197,6 +281,14 @@ "index": "pypi", "version": "==1.6.2" }, + "colorama": { + "hashes": [ + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.4.3" + }, "gitdb": { "hashes": [ "sha256:284a6a4554f954d6e737cddcff946404393e030b76a282c6640df8efd6b3da5e", @@ -211,14 +303,6 @@ ], "version": "==3.1.0" }, - "importlib-metadata": { - "hashes": [ - "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302", - "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b" - ], - "markers": "python_version < '3.8'", - "version": "==1.5.0" - }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", @@ -319,19 +403,19 @@ }, "pyyaml": { "hashes": [ - "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", - "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", - "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", - "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", - "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", - "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", - "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", - "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", - "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", - "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", - "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" ], - "version": "==5.3" + "version": "==5.3.1" }, "six": { "hashes": [ @@ -354,33 +438,6 @@ ], "version": "==1.32.0" }, - "typed-ast": { - "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" - ], - "markers": "implementation_name == 'cpython' and python_version < '3.8'", - "version": "==1.4.1" - }, "wcwidth": { "hashes": [ "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", @@ -393,13 +450,6 @@ "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" ], "version": "==1.11.2" - }, - "zipp": { - "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" - ], - "version": "==3.1.0" } } } From 8166a4d07d0e79772340b9304ade1ee709e2b5fd Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sat, 21 Mar 2020 19:16:43 -0400 Subject: [PATCH 02/30] define app --- app/main.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 app/main.py diff --git a/app/main.py b/app/main.py new file mode 100644 index 00000000..44c45b89 --- /dev/null +++ b/app/main.py @@ -0,0 +1,25 @@ +""" +app.main.py +""" +import os + +import fastapi +import uvicorn + +APP = fastapi.FastAPI( + title="Coronavirus Tracker", + description="API for tracking the global coronavirus (COVID-19, SARS-CoV-2) outbreak.", + version="3.0.0", + prefix="/v3", + docs_url="/v3", + redoc_url="/docs", +) + + +if __name__ == "__main__": + uvicorn.run( + "app.main:APP", + host="127.0.0.1", + port=int(os.getenv("PORT", 5000)), + log_level="info", + ) From d79965ebf624966901b910d61324d7231b9fce00 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sat, 21 Mar 2020 20:15:02 -0400 Subject: [PATCH 03/30] create provisional models --- app/main.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/main.py b/app/main.py index 44c45b89..40247584 100644 --- a/app/main.py +++ b/app/main.py @@ -2,10 +2,35 @@ app.main.py """ import os +import datetime as dt +from typing import Dict import fastapi +import pydantic import uvicorn + +class Stats(pydantic.BaseModel): + confirmed: int + deaths: int + recovered: int + + +class Latest(pydantic.BaseModel): + latest: Stats + + +class Country(pydantic.BaseModel): + id: int + country: str + country_code: str + province: str = None + last_updated: dt.datetime = None + coordinates: Dict = None + latest: Stats = None + timelines: Dict = None + + APP = fastapi.FastAPI( title="Coronavirus Tracker", description="API for tracking the global coronavirus (COVID-19, SARS-CoV-2) outbreak.", From cd64e471864e07ccb6565201d2b273ac033371cd Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sat, 21 Mar 2020 20:15:41 -0400 Subject: [PATCH 04/30] define latest, location endpoints --- app/main.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/main.py b/app/main.py index 40247584..8b165b1a 100644 --- a/app/main.py +++ b/app/main.py @@ -41,6 +41,22 @@ class Country(pydantic.BaseModel): ) +@APP.get("/latest", response_model=Latest) +def get_latest(): + sample_data = {"latest": {"confirmed": 197146, "deaths": 7905, "recovered": 80840}} + return sample_data + + +@APP.get("/locations") +def get_all_locations(country_code: str = None, timelines: int = None): + return + + +@APP.get("/locations/{id}") +def get_location_by_id(id: int): + return + + if __name__ == "__main__": uvicorn.run( "app.main:APP", From 21f4472b526216a1956d26d8f11b2e79a4292345 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 09:19:54 -0400 Subject: [PATCH 05/30] update models --- app/main.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/app/main.py b/app/main.py index 8b165b1a..0c7348e6 100644 --- a/app/main.py +++ b/app/main.py @@ -3,32 +3,52 @@ """ import os import datetime as dt -from typing import Dict +from typing import Dict, List import fastapi import pydantic import uvicorn -class Stats(pydantic.BaseModel): +class Totals(pydantic.BaseModel): confirmed: int deaths: int recovered: int class Latest(pydantic.BaseModel): - latest: Stats + latest: Totals + + +class TimelineStats(pydantic.BaseModel): + latest: int + timeline: Dict[str, int] + + +class Timelines(pydantic.BaseModel): + confirmed: TimelineStats + deaths: TimelineStats + recovered: TimelineStats class Country(pydantic.BaseModel): - id: int + coordinates: Dict = None country: str country_code: str - province: str = None + id: int last_updated: dt.datetime = None - coordinates: Dict = None - latest: Stats = None - timelines: Dict = None + latest: Totals = None + province: str = None + timelines: Timelines = None + + +class AllLocations(pydantic.BaseModel): + latest: Totals + locations: List[Country] + + +class Location(pydantic.BaseModel): + location: Country APP = fastapi.FastAPI( From b36275052fa363c53294d89cba71065ffb4ecec6 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 09:36:33 -0400 Subject: [PATCH 06/30] change model name to match pre-existing model --- app/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index 0c7348e6..da3d4f14 100644 --- a/app/main.py +++ b/app/main.py @@ -25,7 +25,7 @@ class TimelineStats(pydantic.BaseModel): timeline: Dict[str, int] -class Timelines(pydantic.BaseModel): +class TimelinedLocation(pydantic.BaseModel): confirmed: TimelineStats deaths: TimelineStats recovered: TimelineStats @@ -39,7 +39,7 @@ class Country(pydantic.BaseModel): last_updated: dt.datetime = None latest: Totals = None province: str = None - timelines: Timelines = None + timelines: TimelinedLocation = None class AllLocations(pydantic.BaseModel): From 033d9247a0953c14b5758b75b7577fa9e8b29a95 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 09:48:28 -0400 Subject: [PATCH 07/30] defaults --- app/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/main.py b/app/main.py index da3d4f14..82eb6a97 100644 --- a/app/main.py +++ b/app/main.py @@ -32,14 +32,14 @@ class TimelinedLocation(pydantic.BaseModel): class Country(pydantic.BaseModel): - coordinates: Dict = None + coordinates: Dict country: str country_code: str id: int - last_updated: dt.datetime = None - latest: Totals = None - province: str = None - timelines: TimelinedLocation = None + last_updated: dt.datetime + latest: Totals + province: str = "" + timelines: TimelinedLocation class AllLocations(pydantic.BaseModel): From b1a4742d03ea8b57238ad8f0e4fd0b7355c380f7 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 09:48:42 -0400 Subject: [PATCH 08/30] update versions and response models --- app/main.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/main.py b/app/main.py index 82eb6a97..00ff8dcb 100644 --- a/app/main.py +++ b/app/main.py @@ -54,25 +54,26 @@ class Location(pydantic.BaseModel): APP = fastapi.FastAPI( title="Coronavirus Tracker", description="API for tracking the global coronavirus (COVID-19, SARS-CoV-2) outbreak.", - version="3.0.0", - prefix="/v3", - docs_url="/v3", + version="2.1.0", + prefix="/v2-1", + docs_url="/v2-1", redoc_url="/docs", ) @APP.get("/latest", response_model=Latest) def get_latest(): + """Getting latest amount of total confirmed cases, deaths, and recoveries.""" sample_data = {"latest": {"confirmed": 197146, "deaths": 7905, "recovered": 80840}} return sample_data -@APP.get("/locations") +@APP.get("/locations", response_model=AllLocations) def get_all_locations(country_code: str = None, timelines: int = None): return -@APP.get("/locations/{id}") +@APP.get("/locations/{id}", response_model=Location) def get_location_by_id(id: int): return From bdae7f098fa83631b947cd3537c7276dca26cc91 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 12:58:48 -0400 Subject: [PATCH 09/30] divide sections --- app/main.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/main.py b/app/main.py index 00ff8dcb..f284d159 100644 --- a/app/main.py +++ b/app/main.py @@ -10,6 +10,11 @@ import uvicorn +# ################################# +# Models +# ################################# + + class Totals(pydantic.BaseModel): confirmed: int deaths: int @@ -51,6 +56,16 @@ class Location(pydantic.BaseModel): location: Country +# ################ +# Dependencies +# ################ + + +# ############ +# FastAPI App +# ############ +LOGGER = logging.getLogger("api") + APP = fastapi.FastAPI( title="Coronavirus Tracker", description="API for tracking the global coronavirus (COVID-19, SARS-CoV-2) outbreak.", @@ -60,6 +75,15 @@ class Location(pydantic.BaseModel): redoc_url="/docs", ) +# ##################### +# Middleware +####################### + + +# ################ +# Routes +# ################ + @APP.get("/latest", response_model=Latest) def get_latest(): From 825de314b859c773f7e9bb37c956d41796dee42f Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 13:15:59 -0400 Subject: [PATCH 10/30] create middleware to attach the "data_source" https://www.starlette.io/requests/#other-state --- app/main.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index f284d159..409304e1 100644 --- a/app/main.py +++ b/app/main.py @@ -2,6 +2,7 @@ app.main.py """ import os +import logging import datetime as dt from typing import Dict, List @@ -9,6 +10,7 @@ import pydantic import uvicorn +from .data import data_source # ################################# # Models @@ -80,16 +82,33 @@ class Location(pydantic.BaseModel): ####################### +# TODO this could probably just be a FastAPI dependency +@APP.middleware("http") +async def add_datasource(request: fastapi.Request, call_next): + """Attach the data source to the request.state.""" + source = request.query_params.get("source", default="jhu") + request.state.source = data_source(source) + LOGGER.info(f"source: {request.state.source.__class__.__name__}") + response = await call_next(request) + return response + + # ################ # Routes # ################ @APP.get("/latest", response_model=Latest) -def get_latest(): +def get_latest(request: fastapi.Request): """Getting latest amount of total confirmed cases, deaths, and recoveries.""" - sample_data = {"latest": {"confirmed": 197146, "deaths": 7905, "recovered": 80840}} - return sample_data + locations = request.state.source.get_all() + return { + "latest": { + "confirmed": sum(map(lambda location: location.confirmed, locations)), + "deaths": sum(map(lambda location: location.deaths, locations)), + "recovered": sum(map(lambda location: location.recovered, locations)), + } + } @APP.get("/locations", response_model=AllLocations) From e31aad6295da5e7dab498ab75303257991822c40 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 14:41:01 -0400 Subject: [PATCH 11/30] validation exception handler --- app/main.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index 409304e1..cf0410a6 100644 --- a/app/main.py +++ b/app/main.py @@ -1,9 +1,9 @@ """ app.main.py """ -import os -import logging import datetime as dt +import logging +import os from typing import Dict, List import fastapi @@ -93,6 +93,18 @@ async def add_datasource(request: fastapi.Request, call_next): return response +# ################ +# Exception Handler +# ################ + + +@APP.exception_handler(pydantic.error_wrappers.ValidationError) +async def handle_validation_error( + request: fastapi.Request, exc: pydantic.error_wrappers.ValidationError +): + return fastapi.responses.JSONResponse({"message": exc.errors()}, status_code=422) + + # ################ # Routes # ################ @@ -112,7 +124,7 @@ def get_latest(request: fastapi.Request): @APP.get("/locations", response_model=AllLocations) -def get_all_locations(country_code: str = None, timelines: int = None): +def get_all_locations(country_code: str = None, timelines: int = 0): return From 20c49c28629e0ac6e29549f47b2e154040d43c0c Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 14:47:56 -0400 Subject: [PATCH 12/30] get location by id --- app/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index cf0410a6..d4a91449 100644 --- a/app/main.py +++ b/app/main.py @@ -129,8 +129,8 @@ def get_all_locations(country_code: str = None, timelines: int = 0): @APP.get("/locations/{id}", response_model=Location) -def get_location_by_id(id: int): - return +def get_location_by_id(request: fastapi.Request, id: int, timelines: int = 1): + return {"location": request.state.source.get(id).serialize(timelines)} if __name__ == "__main__": From 1cfdc2c2fa297c381e8402594554f076e80a7b41 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 15:47:28 -0400 Subject: [PATCH 13/30] WIP get_all_locations --- app/main.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index d4a91449..1096f842 100644 --- a/app/main.py +++ b/app/main.py @@ -4,6 +4,7 @@ import datetime as dt import logging import os +import reprlib from typing import Dict, List import fastapi @@ -46,11 +47,11 @@ class Country(pydantic.BaseModel): last_updated: dt.datetime latest: Totals province: str = "" - timelines: TimelinedLocation + timelines: TimelinedLocation = None # FIXME class AllLocations(pydantic.BaseModel): - latest: Totals + latest: Totals = None # FIXME locations: List[Country] @@ -124,8 +125,25 @@ def get_latest(request: fastapi.Request): @APP.get("/locations", response_model=AllLocations) -def get_all_locations(country_code: str = None, timelines: int = 0): - return +def get_all_locations( + request: fastapi.Request, country_code: str = None, timelines: int = 0 +): + # Retrieve all the locations. + locations = request.state.source.get_all() + + # Filtering my country code if provided. + if country_code: + locations = list( + filter( + lambda location: location.country_code == country_code.upper(), + locations, + ) + ) + response_dict = { + "locations": [location.serialize(timelines) for location in locations] + } + LOGGER.info(f"response: {reprlib.repr(response_dict)}") + return response_dict @APP.get("/locations/{id}", response_model=Location) From e8e50e802b3df8840f4494fd86c2da2dff5c15aa Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 16:21:47 -0400 Subject: [PATCH 14/30] add Totals --- app/main.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/main.py b/app/main.py index 1096f842..c166c79c 100644 --- a/app/main.py +++ b/app/main.py @@ -51,7 +51,7 @@ class Country(pydantic.BaseModel): class AllLocations(pydantic.BaseModel): - latest: Totals = None # FIXME + latest: Totals locations: List[Country] @@ -139,11 +139,14 @@ def get_all_locations( locations, ) ) - response_dict = { - "locations": [location.serialize(timelines) for location in locations] + return { + "latest": { + "confirmed": sum(map(lambda location: location.confirmed, locations)), + "deaths": sum(map(lambda location: location.deaths, locations)), + "recovered": sum(map(lambda location: location.recovered, locations)), + }, + "locations": [location.serialize(timelines) for location in locations], } - LOGGER.info(f"response: {reprlib.repr(response_dict)}") - return response_dict @APP.get("/locations/{id}", response_model=Location) From 94a2f4e8c0dee8ae7af34c2f8f52bfd923f5aefa Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 16:45:12 -0400 Subject: [PATCH 15/30] Mount v2 of the application --- app/__init__.py | 25 +------------------------ app/core.py | 24 ++++++++++++++++++++++++ app/main.py | 4 ++++ 3 files changed, 29 insertions(+), 24 deletions(-) create mode 100644 app/core.py diff --git a/app/__init__.py b/app/__init__.py index 9861d8b9..ed9f7bab 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,27 +1,4 @@ -from flask import Flask -from flask_cors import CORS - # See PEP396. __version__ = '2.0' -def create_app(): - """ - Construct the core application. - """ - # Create flask app with CORS enabled. - app = Flask(__name__) - CORS(app) - - # Set app config from settings. - app.config.from_pyfile('config/settings.py'); - - with app.app_context(): - # Import routes. - from . import routes - - # Register api endpoints. - app.register_blueprint(routes.api_v1) - app.register_blueprint(routes.api_v2) - - # Return created app. - return app +from .core import create_app \ No newline at end of file diff --git a/app/core.py b/app/core.py new file mode 100644 index 00000000..cb0cc9ba --- /dev/null +++ b/app/core.py @@ -0,0 +1,24 @@ +from flask import Flask +from flask_cors import CORS + +def create_app(): + """ + Construct the core application. + """ + # Create flask app with CORS enabled. + app = Flask(__name__) + CORS(app) + + # Set app config from settings. + app.config.from_pyfile('config/settings.py'); + + with app.app_context(): + # Import routes. + from . import routes + + # Register api endpoints. + app.register_blueprint(routes.api_v1) + app.register_blueprint(routes.api_v2) + + # Return created app. + return app \ No newline at end of file diff --git a/app/main.py b/app/main.py index c166c79c..9cf45720 100644 --- a/app/main.py +++ b/app/main.py @@ -10,8 +10,10 @@ import fastapi import pydantic import uvicorn +from fastapi.middleware.wsgi import WSGIMiddleware from .data import data_source +from .core import create_app # ################################# # Models @@ -153,6 +155,8 @@ def get_all_locations( def get_location_by_id(request: fastapi.Request, id: int, timelines: int = 1): return {"location": request.state.source.get(id).serialize(timelines)} +# mount the existing Flask app to /v2 +APP.mount("/v2", WSGIMiddleware(create_app())) if __name__ == "__main__": uvicorn.run( From 091b461d67e6197c2f8b2170bad826046b49ed10 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 16:47:12 -0400 Subject: [PATCH 16/30] add FIXME for timelines --- app/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/main.py b/app/main.py index 9cf45720..51b9b32d 100644 --- a/app/main.py +++ b/app/main.py @@ -141,6 +141,7 @@ def get_all_locations( locations, ) ) + # FIXME: timelines are not showing up return { "latest": { "confirmed": sum(map(lambda location: location.confirmed, locations)), From ce9374ce808540eb9b8d16af60b5069769ada6b8 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 17:05:47 -0400 Subject: [PATCH 17/30] end of file newlines --- Pipfile | 2 +- app/__init__.py | 2 +- app/core.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile b/Pipfile index 297fb5cb..d6ad6732 100644 --- a/Pipfile +++ b/Pipfile @@ -24,4 +24,4 @@ python_version = "3.8" [scripts] dev_app = "uvicorn app.main:APP --reload" -app = "uvicorn app.main:APP" \ No newline at end of file +app = "uvicorn app.main:APP" diff --git a/app/__init__.py b/app/__init__.py index ed9f7bab..b4b15379 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,4 @@ # See PEP396. __version__ = '2.0' -from .core import create_app \ No newline at end of file +from .core import create_app diff --git a/app/core.py b/app/core.py index cb0cc9ba..a77b37b3 100644 --- a/app/core.py +++ b/app/core.py @@ -21,4 +21,4 @@ def create_app(): app.register_blueprint(routes.api_v2) # Return created app. - return app \ No newline at end of file + return app From 0d2f5f328ce14fcaed450ee218d44aa0eb32fe4a Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 18:03:23 -0400 Subject: [PATCH 18/30] move models to models.py --- app/main.py | 56 ++++++--------------------------------------------- app/models.py | 50 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 50 deletions(-) create mode 100644 app/models.py diff --git a/app/main.py b/app/main.py index 51b9b32d..b1b6c8a9 100644 --- a/app/main.py +++ b/app/main.py @@ -12,54 +12,9 @@ import uvicorn from fastapi.middleware.wsgi import WSGIMiddleware -from .data import data_source +from . import models from .core import create_app - -# ################################# -# Models -# ################################# - - -class Totals(pydantic.BaseModel): - confirmed: int - deaths: int - recovered: int - - -class Latest(pydantic.BaseModel): - latest: Totals - - -class TimelineStats(pydantic.BaseModel): - latest: int - timeline: Dict[str, int] - - -class TimelinedLocation(pydantic.BaseModel): - confirmed: TimelineStats - deaths: TimelineStats - recovered: TimelineStats - - -class Country(pydantic.BaseModel): - coordinates: Dict - country: str - country_code: str - id: int - last_updated: dt.datetime - latest: Totals - province: str = "" - timelines: TimelinedLocation = None # FIXME - - -class AllLocations(pydantic.BaseModel): - latest: Totals - locations: List[Country] - - -class Location(pydantic.BaseModel): - location: Country - +from .data import data_source # ################ # Dependencies @@ -113,7 +68,7 @@ async def handle_validation_error( # ################ -@APP.get("/latest", response_model=Latest) +@APP.get("/latest", response_model=models.Latest) def get_latest(request: fastapi.Request): """Getting latest amount of total confirmed cases, deaths, and recoveries.""" locations = request.state.source.get_all() @@ -126,7 +81,7 @@ def get_latest(request: fastapi.Request): } -@APP.get("/locations", response_model=AllLocations) +@APP.get("/locations", response_model=models.AllLocations) def get_all_locations( request: fastapi.Request, country_code: str = None, timelines: int = 0 ): @@ -152,10 +107,11 @@ def get_all_locations( } -@APP.get("/locations/{id}", response_model=Location) +@APP.get("/locations/{id}", response_model=models.Location) def get_location_by_id(request: fastapi.Request, id: int, timelines: int = 1): return {"location": request.state.source.get(id).serialize(timelines)} + # mount the existing Flask app to /v2 APP.mount("/v2", WSGIMiddleware(create_app())) diff --git a/app/models.py b/app/models.py new file mode 100644 index 00000000..a67b7573 --- /dev/null +++ b/app/models.py @@ -0,0 +1,50 @@ +""" +app.models.py +~~~~~~~~~~~~~ +Reponse data models. +""" +import datetime as dt +from typing import Dict, List + +import pydantic + + +class Totals(pydantic.BaseModel): + confirmed: int + deaths: int + recovered: int + + +class Latest(pydantic.BaseModel): + latest: Totals + + +class TimelineStats(pydantic.BaseModel): + latest: int + timeline: Dict[str, int] + + +class TimelinedLocation(pydantic.BaseModel): + confirmed: TimelineStats + deaths: TimelineStats + recovered: TimelineStats + + +class Country(pydantic.BaseModel): + coordinates: Dict + country: str + country_code: str + id: int + last_updated: dt.datetime + latest: Totals + province: str = "" + timelines: TimelinedLocation = None # FIXME + + +class AllLocations(pydantic.BaseModel): + latest: Totals + locations: List[Country] + + +class Location(pydantic.BaseModel): + location: Country From 04a35d897af1ac22389cb823187b08dc992ebece Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 18:22:39 -0400 Subject: [PATCH 19/30] exclude unset --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index b1b6c8a9..514b8642 100644 --- a/app/main.py +++ b/app/main.py @@ -81,7 +81,7 @@ def get_latest(request: fastapi.Request): } -@APP.get("/locations", response_model=models.AllLocations) +@APP.get("/locations", response_model=models.AllLocations, response_model_exclude_unset=True) def get_all_locations( request: fastapi.Request, country_code: str = None, timelines: int = 0 ): From 6ecbc26730bc4ba325b335e16714bb448c402c4b Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 20:11:00 -0400 Subject: [PATCH 20/30] gunicorn with FastAPI --- Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Procfile b/Procfile index a46b58e3..dd838293 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: gunicorn app:create_app\(\) \ No newline at end of file +web: gunicorn app.main:APP -k uvicorn.workers.UvicornWorker \ No newline at end of file From 43bdeb0b2d72c9c768b1358e6b82a78a19c9eea1 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 20:13:32 -0400 Subject: [PATCH 21/30] change version name --- app/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index 514b8642..ae807e26 100644 --- a/app/main.py +++ b/app/main.py @@ -29,9 +29,9 @@ APP = fastapi.FastAPI( title="Coronavirus Tracker", description="API for tracking the global coronavirus (COVID-19, SARS-CoV-2) outbreak.", - version="2.1.0", - prefix="/v2-1", - docs_url="/v2-1", + version="2.0.1", + prefix="/v2-beta", + docs_url="/v2-beta", redoc_url="/docs", ) From 86617e6c2038fc07e8ca4a0a08158810c8ed085f Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 20:14:40 -0400 Subject: [PATCH 22/30] create Sources enum --- app/main.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index ae807e26..4683e79e 100644 --- a/app/main.py +++ b/app/main.py @@ -2,6 +2,7 @@ app.main.py """ import datetime as dt +import enum import logging import os import reprlib @@ -21,6 +22,11 @@ # ################ +class Sources(str, enum.Enum): + jhu = "jhu" + csbs = "csbs" + + # ############ # FastAPI App # ############ @@ -81,9 +87,14 @@ def get_latest(request: fastapi.Request): } -@APP.get("/locations", response_model=models.AllLocations, response_model_exclude_unset=True) +@APP.get( + "/locations", response_model=models.AllLocations, response_model_exclude_unset=True +) def get_all_locations( - request: fastapi.Request, country_code: str = None, timelines: int = 0 + request: fastapi.Request, + country_code: str = None, + timelines: int = 0, + source: Sources = "jhu", ): # Retrieve all the locations. locations = request.state.source.get_all() From 21e952cc43469ea43a2a89bc3849b5d22786abbb Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 22 Mar 2020 21:28:35 -0400 Subject: [PATCH 23/30] expose v1 and v2 apis via mounted WSGI app --- app/main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index 4683e79e..81d00b12 100644 --- a/app/main.py +++ b/app/main.py @@ -123,8 +123,10 @@ def get_location_by_id(request: fastapi.Request, id: int, timelines: int = 1): return {"location": request.state.source.get(id).serialize(timelines)} -# mount the existing Flask app to /v2 -APP.mount("/v2", WSGIMiddleware(create_app())) +# mount the existing Flask app +# v1 @ / +# v2 @ /v2 +APP.mount("/", WSGIMiddleware(create_app())) if __name__ == "__main__": uvicorn.run( From 0a2a262e109d0a8ab945f300fa521a2ccc3897a6 Mon Sep 17 00:00:00 2001 From: Gabriel Gore Date: Mon, 23 Mar 2020 07:12:01 -0400 Subject: [PATCH 24/30] lock dependencies for linux machines --- Pipfile.lock | 55 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index d505e349..b79d6b4b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -84,6 +84,24 @@ ], "version": "==0.9.0" }, + "httptools": { + "hashes": [ + "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be", + "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d", + "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce", + "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2", + "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6", + "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f", + "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009", + "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce", + "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a", + "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c", + "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4", + "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437" + ], + "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", + "version": "==0.1.1" + }, "idna": { "hashes": [ "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", @@ -215,6 +233,21 @@ "index": "pypi", "version": "==0.11.3" }, + "uvloop": { + "hashes": [ + "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", + "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", + "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", + "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", + "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", + "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", + "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", + "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", + "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" + ], + "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", + "version": "==0.14.0" + }, "websockets": { "hashes": [ "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", @@ -258,14 +291,6 @@ ], "version": "==2.3.3" }, - "atomicwrites": { - "hashes": [ - "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", - "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" - ], - "markers": "sys_platform == 'win32'", - "version": "==1.3.0" - }, "attrs": { "hashes": [ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", @@ -281,14 +306,6 @@ "index": "pypi", "version": "==1.6.2" }, - "colorama": { - "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" - ], - "markers": "sys_platform == 'win32'", - "version": "==0.4.3" - }, "gitdb": { "hashes": [ "sha256:284a6a4554f954d6e737cddcff946404393e030b76a282c6640df8efd6b3da5e", @@ -440,10 +457,10 @@ }, "wcwidth": { "hashes": [ - "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", - "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" + "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", + "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" ], - "version": "==0.1.8" + "version": "==0.1.9" }, "wrapt": { "hashes": [ From 8a199592cde40259729f2142d2ba655163478ea3 Mon Sep 17 00:00:00 2001 From: Gabriel Gore Date: Mon, 23 Mar 2020 07:37:03 -0400 Subject: [PATCH 25/30] specify python 3.8 runtime for Heroku --- runtime.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 runtime.txt diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 00000000..724c203e --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.8.2 From ab817bd9f91ef0c1e53413bd2d4b17d0c6733e9e Mon Sep 17 00:00:00 2001 From: Gabriel Date: Mon, 23 Mar 2020 21:11:46 -0400 Subject: [PATCH 26/30] add sources --- app/main.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 81d00b12..3683de48 100644 --- a/app/main.py +++ b/app/main.py @@ -15,7 +15,7 @@ from . import models from .core import create_app -from .data import data_source +from .data import data_source, data_sources # ################ # Dependencies @@ -123,6 +123,16 @@ def get_location_by_id(request: fastapi.Request, id: int, timelines: int = 1): return {"location": request.state.source.get(id).serialize(timelines)} +@APP.get('/sources') +async def sources(): + """ + Retrieves a list of data-sources that are availble to use. + """ + return { + 'sources': list(data_sources.keys()) + } + + # mount the existing Flask app # v1 @ / # v2 @ /v2 From ece30f396863d1b0debafceef7f96829b865f4ed Mon Sep 17 00:00:00 2001 From: Gabriel Date: Mon, 23 Mar 2020 21:13:21 -0400 Subject: [PATCH 27/30] update prefix --- app/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index 3683de48..696832f9 100644 --- a/app/main.py +++ b/app/main.py @@ -36,8 +36,7 @@ class Sources(str, enum.Enum): title="Coronavirus Tracker", description="API for tracking the global coronavirus (COVID-19, SARS-CoV-2) outbreak.", version="2.0.1", - prefix="/v2-beta", - docs_url="/v2-beta", + docs_url="/", redoc_url="/docs", ) From aec80ac0ab7f87b22f21824c2c6da5158ae0dbc7 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Mon, 23 Mar 2020 21:25:39 -0400 Subject: [PATCH 28/30] use separate v2 router --- app/main.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index 696832f9..e67766c8 100644 --- a/app/main.py +++ b/app/main.py @@ -72,8 +72,10 @@ async def handle_validation_error( # Routes # ################ +V2 = fastapi.APIRouter() -@APP.get("/latest", response_model=models.Latest) + +@V2.get("/latest", response_model=models.Latest) def get_latest(request: fastapi.Request): """Getting latest amount of total confirmed cases, deaths, and recoveries.""" locations = request.state.source.get_all() @@ -86,7 +88,7 @@ def get_latest(request: fastapi.Request): } -@APP.get( +@V2.get( "/locations", response_model=models.AllLocations, response_model_exclude_unset=True ) def get_all_locations( @@ -117,12 +119,12 @@ def get_all_locations( } -@APP.get("/locations/{id}", response_model=models.Location) +@V2.get("/locations/{id}", response_model=models.Location) def get_location_by_id(request: fastapi.Request, id: int, timelines: int = 1): return {"location": request.state.source.get(id).serialize(timelines)} -@APP.get('/sources') +@V2.get('/sources') async def sources(): """ Retrieves a list of data-sources that are availble to use. @@ -132,6 +134,7 @@ async def sources(): } +APP.include_router(V2, prefix="/v2-beta", tags=["v2"]) # mount the existing Flask app # v1 @ / # v2 @ /v2 From 1949beaf81ce215b33baf673799fdb3eba0d12ca Mon Sep 17 00:00:00 2001 From: Gabriel Date: Mon, 23 Mar 2020 18:22:43 -0400 Subject: [PATCH 29/30] add sources and make Timelines a boolean --- app/main.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/main.py b/app/main.py index e67766c8..5b62ac9c 100644 --- a/app/main.py +++ b/app/main.py @@ -76,7 +76,7 @@ async def handle_validation_error( @V2.get("/latest", response_model=models.Latest) -def get_latest(request: fastapi.Request): +def get_latest(request: fastapi.Request, source: Sources = "jhu"): """Getting latest amount of total confirmed cases, deaths, and recoveries.""" locations = request.state.source.get_all() return { @@ -94,7 +94,7 @@ def get_latest(request: fastapi.Request): def get_all_locations( request: fastapi.Request, country_code: str = None, - timelines: int = 0, + timelines: bool = False, source: Sources = "jhu", ): # Retrieve all the locations. @@ -120,18 +120,16 @@ def get_all_locations( @V2.get("/locations/{id}", response_model=models.Location) -def get_location_by_id(request: fastapi.Request, id: int, timelines: int = 1): +def get_location_by_id(request: fastapi.Request, id: int, timelines: bool = True): return {"location": request.state.source.get(id).serialize(timelines)} -@V2.get('/sources') +@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(data_sources.keys())} APP.include_router(V2, prefix="/v2-beta", tags=["v2"]) From f7b7354cef0830e291f22ae6b776ddaf629f7e66 Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 02:54:18 +0100 Subject: [PATCH 30/30] fixed wrong date format and timelines appearing null --- app/main.py | 5 +- app/models.py | 33 +++--- coronavirus-tracker-api-swagger.yaml | 152 --------------------------- 3 files changed, 18 insertions(+), 172 deletions(-) delete mode 100644 coronavirus-tracker-api-swagger.yaml diff --git a/app/main.py b/app/main.py index 5b62ac9c..8e0b367c 100644 --- a/app/main.py +++ b/app/main.py @@ -89,7 +89,7 @@ def get_latest(request: fastapi.Request, source: Sources = "jhu"): @V2.get( - "/locations", response_model=models.AllLocations, response_model_exclude_unset=True + "/locations", response_model=models.Locations, response_model_exclude_unset=True ) def get_all_locations( request: fastapi.Request, @@ -131,8 +131,9 @@ async def sources(): """ return {"sources": list(data_sources.keys())} - +# Include routers. APP.include_router(V2, prefix="/v2-beta", tags=["v2"]) + # mount the existing Flask app # v1 @ / # v2 @ /v2 diff --git a/app/models.py b/app/models.py index a67b7573..36b1396f 100644 --- a/app/models.py +++ b/app/models.py @@ -3,48 +3,45 @@ ~~~~~~~~~~~~~ Reponse data models. """ -import datetime as dt +from pydantic import BaseModel from typing import Dict, List -import pydantic - - -class Totals(pydantic.BaseModel): +class Totals(BaseModel): confirmed: int deaths: int recovered: int -class Latest(pydantic.BaseModel): +class Latest(BaseModel): latest: Totals -class TimelineStats(pydantic.BaseModel): +class TimelineStats(BaseModel): latest: int - timeline: Dict[str, int] + timeline: Dict[str, int] = {} -class TimelinedLocation(pydantic.BaseModel): +class TimelinedLocation(BaseModel): confirmed: TimelineStats deaths: TimelineStats recovered: TimelineStats -class Country(pydantic.BaseModel): - coordinates: Dict +class Country(BaseModel): + id: int country: str country_code: str - id: int - last_updated: dt.datetime - latest: Totals province: str = "" - timelines: TimelinedLocation = None # FIXME + last_updated: str # TODO use datetime.datetime type. + coordinates: Dict + latest: Totals + timelines: TimelinedLocation = {} -class AllLocations(pydantic.BaseModel): +class Locations(BaseModel): latest: Totals - locations: List[Country] + locations: List[Country] = [] -class Location(pydantic.BaseModel): +class Location(BaseModel): location: Country diff --git a/coronavirus-tracker-api-swagger.yaml b/coronavirus-tracker-api-swagger.yaml deleted file mode 100644 index d17b9d42..00000000 --- a/coronavirus-tracker-api-swagger.yaml +++ /dev/null @@ -1,152 +0,0 @@ -swagger: '2.0' -info: - description: 'Corona Virus Tracker API based on JHU data sets. https://github.com/ExpDev07/coronavirus-tracker-api' - version: 1.0.0 - title: Corona Virus Tracker API - contact: - url: https://github.com/ExpDev07/coronavirus-tracker-api -host: coronavirus-tracker-api.herokuapp.com -basePath: /v2 -schemes: - - https -paths: - /latest: - get: - summary: Latest global Corona stats - operationId: latest - produces: - - application/json - responses: - '200': - description: successful operation - schema: - type: object - properties: - latest: - type: object - properties: - confirmed: - type: number - example: 214910 - deaths: - type: number - example: 8733 - recovered: - type: number - example: 83207 - /locations: - get: - summary: All Locations - operationId: allLocations - produces: - - application/json - parameters: - - name: country_code - in: query - description: "ISO alpha 2 country code. https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2" - required: false - type: string - x-example: US - - name: timelines - in: query - description: "Include timeline objects in response? true or false" - required: false - type: string - x-example: true - responses: - '200': - description: successful operation - schema: - type: object - properties: - locations: - type: array - items: - $ref: '#/definitions/Location' - '/locations/{id}': - get: - summary: location - operationId: location - produces: - - application/json - parameters: - - name: id - in: path - description: "Internal ID" - required: true - type: string - x-example: 39 - responses: - '200': - description: successful operation - schema: - type: object - properties: - location: - $ref: '#/definitions/Location' - '404': - description: Location not found -definitions: - Location: - type: object - properties: - coordinates: - type: object - properties: - latitude: - type: string - example: "47.4009" - longitude: - type: string - example: "-121.4905" - country: - type: string - example: "US" - country_code: - type: string - example: US - id: - type: number - example: 98 - latest: - type: object - properties: - confirmed: - type: number - example: 1014 - deaths: - type: number - example: 55 - recovered: - type: number - example: 10 - province: - type: string - example: "Washington" - timelines: - type: object - properties: - confirmed: - type: object - properties: - latest: - type: number - example: 1014 - timeline: - type: object - deaths: - type: object - properties: - latest: - type: number - example: 55 - timeline: - type: object - recovered: - type: object - properties: - latest: - type: number - example: 10 - timeline: - type: object