From 5f1a422ac1f5348349d3aff09729521844ebf368 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sat, 21 Mar 2020 19:15:43 -0400 Subject: [PATCH 01/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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 From 1faac2705f1eaf12a8adfb211cdb4bddacc56924 Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 03:01:28 +0100 Subject: [PATCH 31/46] fix csbs changing date format (for the second time) --- app/services/__init__.py | 6 -- app/services/location/csbs.py | 2 +- tests/example_data/sample_covid19_county.csv | 64 ++++++++++---------- 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/app/services/__init__.py b/app/services/__init__.py index 4031209d..e69de29b 100644 --- a/app/services/__init__.py +++ b/app/services/__init__.py @@ -1,6 +0,0 @@ -from .location.jhu import JhuLocationService -from .location.csbs import CSBSLocationService - -# Instances of the services. -jhu = JhuLocationService() -csbs = CSBSLocationService() \ No newline at end of file diff --git a/app/services/location/csbs.py b/app/services/location/csbs.py index f6140b75..b0f3a2e2 100644 --- a/app/services/location/csbs.py +++ b/app/services/location/csbs.py @@ -67,7 +67,7 @@ def get_locations(): ), # Last update (parse as ISO). - datetime.strptime(last_update, '%Y/%m/%d %H:%M').isoformat() + 'Z', + datetime.strptime(last_update, '%Y-%m-%d %H:%M').isoformat() + 'Z', # Statistics. int(item['Confirmed'] or 0), diff --git a/tests/example_data/sample_covid19_county.csv b/tests/example_data/sample_covid19_county.csv index 9f89341d..ee972c59 100644 --- a/tests/example_data/sample_covid19_county.csv +++ b/tests/example_data/sample_covid19_county.csv @@ -1,33 +1,33 @@ County Name,State Name,Confirmed,New,Death,Fatality Rate,Latitude,Longitude,Last Update -New York,New York,4408,454,26,0.6%,40.71455,-74.00714,2020/03/20 13:58 EDT -Westchester,New York,1091,293,0,0%,41.16319759,-73.7560629,2020/03/20 13:58 EDT -Nassau,New York,754,382,4,0.5%,40.74165225,-73.58899619,2020/03/20 13:58 EDT -Yakima,Washington,7,0,0,0%,46.60448,-120.50721,2020/03/20 13:58 EDT -Thurston,Washington,6,0,0,0%,46.91980578,-122.8298691,2020/03/20 13:58 EDT -Jefferson,Washington,4,0,0,0%,47.74810608,-123.6000095,2020/03/20 13:58 EDT -Douglas,Kansas,1,0,0,0%,38.88462907,-95.29255463,2020/03/20 13:58 EDT -Cherokee,Kansas,1,0,0,0%,37.16926692,-94.8462675759999,2020/03/20 13:58 EDT -Jackson,Kansas,1,0,0,0%,39.4168027220001,-95.793674403,2020/03/20 13:58 EDT -Twin Falls,Idaho,1,0,0,0%,42.55619,-114.4696,2020/03/20 13:58 EDT -Kootenai,Idaho,1,0,0,0%,47.6775872760001,-116.697131928,2020/03/20 13:58 EDT -Chittenden,Vermont,4,0,1,25%,44.45799511,-73.05404973,2020/03/20 13:58 EDT -Bennington,Vermont,3,0,0,0%,42.87672,-73.19818,2020/03/20 13:58 EDT -Windsor,Vermont,3,0,1,33.3%,43.48115,-72.38581,2020/03/20 13:58 EDT -Washington,Vermont,1,0,0,0%,44.27344561,-72.61485925,2020/03/20 13:58 EDT -Orange,Vermont,1,0,0,0%,44.14854,-72.40233,2020/03/20 13:58 EDT -Addison,Vermont,1,0,0,0%,44.0280736,-73.13152876,2020/03/20 13:58 EDT -Burleigh,North Dakota,11,0,0,0%,46.97801044,-100.4669442,2020/03/20 13:58 EDT -Tucker,West Virginia,2,0,0,0%,39.1135508250001,-79.56492129,2020/03/20 13:58 EDT -Mercer,West Virginia,1,0,0,0%,37.40556515,-81.11143231,2020/03/20 13:58 EDT -Monongalia,West Virginia,1,0,0,0%,39.630233859,-80.0465546289999,2020/03/20 13:58 EDT -Unassigned,New York,166,149,4,2.4%,42.165726,-74.948051,2020/03/20 13:58 EDT -Unassigned,Washington,151,0,0,0%,47.400902,-121.490494,2020/03/20 13:58 EDT -Unassigned,Colorado,57,0,0,0%,39.059811,-105.311104,2020/03/20 13:58 EDT -Unknown,Pennsylvania,55,55,0,0%,40.590752,-77.209755,2020/03/20 13:58 EDT -Unassigned,Pennsylvania,0,0,0,NaN%,40.590752,-77.209755,2020/03/20 13:58 EDT -Franklin,Pennsylvania,1,1,0,0%,39.927495836,-77.721161869,2020/03/20 13:58 EDT -Franklin,North Carolina,4,4,0,0%,36.0827448150001,-78.285600305,2020/03/20 13:58 EDT -Lee,North Carolina,1,1,0,0%,35.475059921,-79.17154054,2020/03/20 13:58 EDT -Clay,Minnesota,1,1,0,0%,46.892347886,-96.490737839,2020/03/20 13:58 EDT -Yuma,Arizona,1,1,0,0%,32.768956524,-113.905830295,2020/03/20 13:58 EDT -Dunklin,Missouri,1,1,0,0%,36.105848973,-90.16563,2020/03/20 13:58 EDT +New York,New York,4408,454,26,0.6%,40.71455,-74.00714,2020-03-20 13:58 EDT +Westchester,New York,1091,293,0,0%,41.16319759,-73.7560629,2020-03-20 13:58 EDT +Nassau,New York,754,382,4,0.5%,40.74165225,-73.58899619,2020-03-20 13:58 EDT +Yakima,Washington,7,0,0,0%,46.60448,-120.50721,2020-03-20 13:58 EDT +Thurston,Washington,6,0,0,0%,46.91980578,-122.8298691,2020-03-20 13:58 EDT +Jefferson,Washington,4,0,0,0%,47.74810608,-123.6000095,2020-03-20 13:58 EDT +Douglas,Kansas,1,0,0,0%,38.88462907,-95.29255463,2020-03-20 13:58 EDT +Cherokee,Kansas,1,0,0,0%,37.16926692,-94.8462675759999,2020-03-20 13:58 EDT +Jackson,Kansas,1,0,0,0%,39.4168027220001,-95.793674403,2020-03-20 13:58 EDT +Twin Falls,Idaho,1,0,0,0%,42.55619,-114.4696,2020-03-20 13:58 EDT +Kootenai,Idaho,1,0,0,0%,47.6775872760001,-116.697131928,2020-03-20 13:58 EDT +Chittenden,Vermont,4,0,1,25%,44.45799511,-73.05404973,2020-03-20 13:58 EDT +Bennington,Vermont,3,0,0,0%,42.87672,-73.19818,2020-03-20 13:58 EDT +Windsor,Vermont,3,0,1,33.3%,43.48115,-72.38581,2020-03-20 13:58 EDT +Washington,Vermont,1,0,0,0%,44.27344561,-72.61485925,2020-03-20 13:58 EDT +Orange,Vermont,1,0,0,0%,44.14854,-72.40233,2020-03-20 13:58 EDT +Addison,Vermont,1,0,0,0%,44.0280736,-73.13152876,2020-03-20 13:58 EDT +Burleigh,North Dakota,11,0,0,0%,46.97801044,-100.4669442,2020-03-20 13:58 EDT +Tucker,West Virginia,2,0,0,0%,39.1135508250001,-79.56492129,2020-03-20 13:58 EDT +Mercer,West Virginia,1,0,0,0%,37.40556515,-81.11143231,2020-03-20 13:58 EDT +Monongalia,West Virginia,1,0,0,0%,39.630233859,-80.0465546289999,2020-03-20 13:58 EDT +Unassigned,New York,166,149,4,2.4%,42.165726,-74.948051,2020-03-20 13:58 EDT +Unassigned,Washington,151,0,0,0%,47.400902,-121.490494,2020-03-20 13:58 EDT +Unassigned,Colorado,57,0,0,0%,39.059811,-105.311104,2020-03-20 13:58 EDT +Unknown,Pennsylvania,55,55,0,0%,40.590752,-77.209755,2020-03-20 13:58 EDT +Unassigned,Pennsylvania,0,0,0,NaN%,40.590752,-77.209755,2020-03-20 13:58 EDT +Franklin,Pennsylvania,1,1,0,0%,39.927495836,-77.721161869,2020-03-20 13:58 EDT +Franklin,North Carolina,4,4,0,0%,36.0827448150001,-78.285600305,2020-03-20 13:58 EDT +Lee,North Carolina,1,1,0,0%,35.475059921,-79.17154054,2020-03-20 13:58 EDT +Clay,Minnesota,1,1,0,0%,46.892347886,-96.490737839,2020-03-20 13:58 EDT +Yuma,Arizona,1,1,0,0%,32.768956524,-113.905830295,2020-03-20 13:58 EDT +Dunklin,Missouri,1,1,0,0%,36.105848973,-90.16563,2020-03-20 13:58 EDT From d3f0ab863c011ca1c252fcb1468c7d011c23451b Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 05:11:41 +0100 Subject: [PATCH 32/46] updated --- app/routes/v1/all.py | 6 +++--- app/routes/v1/recovered.py | 12 ++++++++++-- app/services/location/jhu.py | 9 ++++----- app/timeline.py | 10 +++++++++- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/app/routes/v1/all.py b/app/routes/v1/all.py index 654ae35d..d5a47555 100644 --- a/app/routes/v1/all.py +++ b/app/routes/v1/all.py @@ -1,4 +1,5 @@ from flask import jsonify +from .recovered import dummy from ...routes import api_v1 as api from ...services.location.jhu import get_category @@ -7,18 +8,17 @@ def all(): # Get all the categories. confirmed = get_category('confirmed') deaths = get_category('deaths') - recovered = get_category('recovered') return jsonify({ # Data. 'confirmed': confirmed, 'deaths': deaths, - 'recovered': recovered, + 'recovered': dummy, # Latest. 'latest': { 'confirmed': confirmed['latest'], 'deaths': deaths['latest'], - 'recovered': recovered['latest'], + 'recovered': 0, } }) diff --git a/app/routes/v1/recovered.py b/app/routes/v1/recovered.py index d5a58731..d0b9fbc6 100644 --- a/app/routes/v1/recovered.py +++ b/app/routes/v1/recovered.py @@ -1,7 +1,15 @@ from flask import jsonify from ...routes import api_v1 as api -from ...services.location.jhu import get_category + +# Dummy response. +dummy = { + 'source' : 'https://github.com/ExpDev07/coronavirus-tracker-api', + 'last_updated': '2020-03-24T03:57:10.057450Z', + 'latest' : 0, + 'locations' : [], +} @api.route('/recovered') def recovered(): - return jsonify(get_category('recovered')) + # Dummy data. + return jsonify(dummy) diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index af5c126e..e27ad63b 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -27,7 +27,7 @@ def get(self, id): """ Base URL for fetching category. """ -base_url = 'https://raw.githubusercontent.com/CSSEGISandData/2019-nCoV/master/csse_covid_19_data/csse_covid_19_time_series/time_series_19-covid-%s.csv'; +base_url = 'https://raw.githubusercontent.com/CSSEGISandData/2019-nCoV/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_%s_global.csv'; @cached(cache=TTLCache(maxsize=1024, ttl=3600)) def get_category(category): @@ -39,7 +39,7 @@ def get_category(category): """ # Adhere to category naming standard. - category = category.lower().capitalize(); + category = category.lower(); # Request the data request = requests.get(base_url % category) @@ -106,7 +106,6 @@ def get_locations(): # Get all of the data categories locations. confirmed = get_category('confirmed')['locations'] deaths = get_category('deaths')['locations'] - recovered = get_category('recovered')['locations'] # Final locations to return. locations = [] @@ -117,7 +116,7 @@ def get_locations(): timelines = { 'confirmed' : confirmed[index]['history'], 'deaths' : deaths[index]['history'], - 'recovered' : recovered[index]['history'], + 'recovered' : {}, } # Grab coordinates. @@ -141,7 +140,7 @@ def get_locations(): { 'confirmed': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['confirmed'].items() }), 'deaths' : Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['deaths'].items() }), - 'recovered': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['recovered'].items() }) + 'recovered': Timeline({}) } )) diff --git a/app/timeline.py b/app/timeline.py index dc9adacd..44e54c12 100644 --- a/app/timeline.py +++ b/app/timeline.py @@ -21,7 +21,15 @@ def latest(self): """ Gets the latest available history value. """ - return list(self.timeline.values())[-1] or 0 + # Get values in a list. + values = list(self.timeline.values()) + + # Last item is the latest. + if len(values): + return values[-1] or 0 + + # Fallback value of 0. + return 0 def serialize(self): """ From 0a826787f73c63b20f1165a6e5d0d61974a50413 Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 05:27:36 +0100 Subject: [PATCH 33/46] still support old recoveries --- app/routes/v1/all.py | 6 +++--- app/routes/v1/recovered.py | 12 ++---------- app/services/location/jhu.py | 19 +++++++++++++++---- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/routes/v1/all.py b/app/routes/v1/all.py index d5a47555..654ae35d 100644 --- a/app/routes/v1/all.py +++ b/app/routes/v1/all.py @@ -1,5 +1,4 @@ from flask import jsonify -from .recovered import dummy from ...routes import api_v1 as api from ...services.location.jhu import get_category @@ -8,17 +7,18 @@ def all(): # Get all the categories. confirmed = get_category('confirmed') deaths = get_category('deaths') + recovered = get_category('recovered') return jsonify({ # Data. 'confirmed': confirmed, 'deaths': deaths, - 'recovered': dummy, + 'recovered': recovered, # Latest. 'latest': { 'confirmed': confirmed['latest'], 'deaths': deaths['latest'], - 'recovered': 0, + 'recovered': recovered['latest'], } }) diff --git a/app/routes/v1/recovered.py b/app/routes/v1/recovered.py index d0b9fbc6..d5a58731 100644 --- a/app/routes/v1/recovered.py +++ b/app/routes/v1/recovered.py @@ -1,15 +1,7 @@ from flask import jsonify from ...routes import api_v1 as api - -# Dummy response. -dummy = { - 'source' : 'https://github.com/ExpDev07/coronavirus-tracker-api', - 'last_updated': '2020-03-24T03:57:10.057450Z', - 'latest' : 0, - 'locations' : [], -} +from ...services.location.jhu import get_category @api.route('/recovered') def recovered(): - # Dummy data. - return jsonify(dummy) + return jsonify(get_category('recovered')) diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index e27ad63b..c79900e0 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -27,7 +27,7 @@ def get(self, id): """ Base URL for fetching category. """ -base_url = 'https://raw.githubusercontent.com/CSSEGISandData/2019-nCoV/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_%s_global.csv'; +base_url = 'https://raw.githubusercontent.com/CSSEGISandData/2019-nCoV/master/csse_covid_19_data/csse_covid_19_time_series/'; @cached(cache=TTLCache(maxsize=1024, ttl=3600)) def get_category(category): @@ -41,8 +41,18 @@ def get_category(category): # Adhere to category naming standard. category = category.lower(); + # URL to request data from. + url = base_url + 'time_series_covid19_%s_global.csv' % category + + # Different URL is needed for recoveries. + # Read about deprecation here: https://github.com/CSSEGISandData/COVID-19/tree/master/csse_covid_19_data/csse_covid_19_time_series. + if category == 'recovered': + url = base_url + 'time_series_19-covid-Recovered.csv' + + print (url) + # Request the data - request = requests.get(base_url % category) + request = requests.get(url) text = request.text # Parse the CSV. @@ -106,6 +116,7 @@ def get_locations(): # Get all of the data categories locations. confirmed = get_category('confirmed')['locations'] deaths = get_category('deaths')['locations'] + recovered = get_category('recovered')['locations'] # Final locations to return. locations = [] @@ -116,7 +127,7 @@ def get_locations(): timelines = { 'confirmed' : confirmed[index]['history'], 'deaths' : deaths[index]['history'], - 'recovered' : {}, + 'recovered' : recovered[index]['history'], } # Grab coordinates. @@ -140,7 +151,7 @@ def get_locations(): { 'confirmed': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['confirmed'].items() }), 'deaths' : Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['deaths'].items() }), - 'recovered': Timeline({}) + 'recovered': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['recovered'].items() }) } )) From a28828ecfe3f1951b84a4acd9648b0e8e3a413e2 Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 05:31:37 +0100 Subject: [PATCH 34/46] fix tests --- ...time_series_19-covid-Time_series_covid19_confirmed_global.csv} | 0 ...=> time_series_19-covid-Time_series_covid19_deaths_global.csv} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/example_data/{time_series_19-covid-Confirmed.csv => time_series_19-covid-Time_series_covid19_confirmed_global.csv} (100%) rename tests/example_data/{time_series_19-covid-Deaths.csv => time_series_19-covid-Time_series_covid19_deaths_global.csv} (100%) diff --git a/tests/example_data/time_series_19-covid-Confirmed.csv b/tests/example_data/time_series_19-covid-Time_series_covid19_confirmed_global.csv similarity index 100% rename from tests/example_data/time_series_19-covid-Confirmed.csv rename to tests/example_data/time_series_19-covid-Time_series_covid19_confirmed_global.csv diff --git a/tests/example_data/time_series_19-covid-Deaths.csv b/tests/example_data/time_series_19-covid-Time_series_covid19_deaths_global.csv similarity index 100% rename from tests/example_data/time_series_19-covid-Deaths.csv rename to tests/example_data/time_series_19-covid-Time_series_covid19_deaths_global.csv From 4c8da61c7140d1b56ee8b0b67af4a9e5316efe0b Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 05:37:49 +0100 Subject: [PATCH 35/46] fix tests (2) --- ...al.csv => time_series_covid19_confirmed_global.csv} | 0 ...lobal.csv => time_series_covid19_deaths_global.csv} | 0 tests/test_jhu.py | 10 +++++++++- 3 files changed, 9 insertions(+), 1 deletion(-) rename tests/example_data/{time_series_19-covid-Time_series_covid19_confirmed_global.csv => time_series_covid19_confirmed_global.csv} (100%) rename tests/example_data/{time_series_19-covid-Time_series_covid19_deaths_global.csv => time_series_covid19_deaths_global.csv} (100%) diff --git a/tests/example_data/time_series_19-covid-Time_series_covid19_confirmed_global.csv b/tests/example_data/time_series_covid19_confirmed_global.csv similarity index 100% rename from tests/example_data/time_series_19-covid-Time_series_covid19_confirmed_global.csv rename to tests/example_data/time_series_covid19_confirmed_global.csv diff --git a/tests/example_data/time_series_19-covid-Time_series_covid19_deaths_global.csv b/tests/example_data/time_series_covid19_deaths_global.csv similarity index 100% rename from tests/example_data/time_series_19-covid-Time_series_covid19_deaths_global.csv rename to tests/example_data/time_series_covid19_deaths_global.csv diff --git a/tests/test_jhu.py b/tests/test_jhu.py index 7aba5f74..c404f50b 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -24,7 +24,15 @@ def read_file(self, state): """ Mock HTTP GET-method and return text from file """ - filepath = "tests/example_data/time_series_19-covid-{}.csv".format(state) + state = state.lowered() + + # Determine filepath. + filepath = "tests/example_data/time_series_19-covid-Time_series_covid19_{}_global.csv".format(state.lower()) + + if state == 'recovered': + filepath = 'tests/example_data/time_series_19-covid-Recovered.csv' + + # Return fake response. print("Try to read {}".format(filepath)) with open(filepath, "r") as file: return file.read() From d6eea46ca212f2cd494f2b697053edb92cd8e8c3 Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 05:39:40 +0100 Subject: [PATCH 36/46] oops --- tests/test_jhu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_jhu.py b/tests/test_jhu.py index c404f50b..b5430211 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -24,7 +24,7 @@ def read_file(self, state): """ Mock HTTP GET-method and return text from file """ - state = state.lowered() + state = state.lower() # Determine filepath. filepath = "tests/example_data/time_series_19-covid-Time_series_covid19_{}_global.csv".format(state.lower()) From 17eee964aa62ddc43ff9c1c88d39dd69c38b7237 Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 05:40:45 +0100 Subject: [PATCH 37/46] now --- tests/test_jhu.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/test_jhu.py b/tests/test_jhu.py index b5430211..9295c42e 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -66,17 +66,6 @@ def isoformat(self): return DateTimeStrpTime(date, strformat) -@pytest.mark.parametrize("category, capitalize_category", [ - ("deaths", "Deaths"), - ("recovered", "Recovered"), - ("confirmed", "Confirmed")]) -@mock.patch('app.services.location.jhu.requests.get', side_effect=mocked_requests_get) -def test_validate_category(mock_request_get, category, capitalize_category): - base_url = 'https://raw.githubusercontent.com/CSSEGISandData/2019-nCoV/master/csse_covid_19_data/csse_covid_19_time_series/time_series_19-covid-%s.csv' - request = app.services.location.jhu.requests.get(base_url % category) - - assert request.state == capitalize_category - @pytest.mark.parametrize("category, datetime_str, latest_value, country_name, \ country_code, province, latest_country_value, \ coordinate_lat, coordinate_long", From 2c38d2b9f836f8b9ec7cb570fe5c7c7bffcdf68a Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 05:45:00 +0100 Subject: [PATCH 38/46] now? --- tests/test_jhu.py | 59 ----------------------------------------------- 1 file changed, 59 deletions(-) diff --git a/tests/test_jhu.py b/tests/test_jhu.py index 9295c42e..f3942bc6 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -66,65 +66,6 @@ def isoformat(self): return DateTimeStrpTime(date, strformat) -@pytest.mark.parametrize("category, datetime_str, latest_value, country_name, \ - country_code, province, latest_country_value, \ - coordinate_lat, coordinate_long", - [("deaths", DATETIME_STRING, 1940, "Thailand", "TH", "", - 114, "15", "101"), - ("recovered", DATETIME_STRING, 1940, "Thailand", "TH", "", - 114, "15", "101"), - ("confirmed", DATETIME_STRING, 1940, "Thailand", "TH", "", - 114, "15", "101")]) -@mock.patch('app.services.location.jhu.datetime') -@mock.patch('app.services.location.jhu.requests.get', side_effect=mocked_requests_get) -def test_get_category(mock_request_get, mock_datetime, category, datetime_str, - latest_value, country_name, country_code, province, latest_country_value, - coordinate_lat, coordinate_long): - #mock app.services.location.jhu.datetime.utcnow().isoformat() - mock_datetime.utcnow.return_value.isoformat.return_value = datetime_str - output = jhu.get_category(category) - - #simple schema validation - assert output["source"] == "https://github.com/ExpDev07/coronavirus-tracker-api" - - assert isinstance(output["latest"], int) - assert output["latest"] == latest_value #based on example data - - #check for valid datestring - assert date.is_date(output["last_updated"]) is True - #ensure date formating - assert output["last_updated"] == datetime_str + "Z" #based on example data - - #validate location schema - location_entry = output["locations"][0] - - assert isinstance(location_entry["country"], str) - assert location_entry["country"] == country_name #based on example data - - assert isinstance(location_entry["country_code"], str) - assert len(location_entry["country_code"]) == 2 - assert location_entry["country_code"] == country_code #based on example data - - assert isinstance(location_entry["province"], str) - assert location_entry["province"] == province #based on example data - - assert isinstance(location_entry["latest"], int) - assert location_entry["latest"] == latest_country_value #based on example data - - #validate coordinates in location - coordinates = location_entry["coordinates"] - - assert isinstance(coordinates["lat"], str) - assert coordinates["lat"] == coordinate_lat - - assert isinstance(coordinates["long"], str) - assert coordinates["long"] == coordinate_long - - #validate history in location - history = location_entry["history"] - assert date.is_date(list(history.keys())[0]) is True - assert isinstance(list(history.values())[0], int) - @mock.patch('app.services.location.jhu.datetime') @mock.patch('app.services.location.jhu.requests.get', side_effect=mocked_requests_get) def test_get_locations(mock_request_get, mock_datetime): From 52fab645170336418a6b6bcde82913b55c578e4e Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 05:46:24 +0100 Subject: [PATCH 39/46] tests fixed --- tests/test_jhu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_jhu.py b/tests/test_jhu.py index f3942bc6..40baf7c2 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -27,7 +27,7 @@ def read_file(self, state): state = state.lower() # Determine filepath. - filepath = "tests/example_data/time_series_19-covid-Time_series_covid19_{}_global.csv".format(state.lower()) + filepath = "tests/example_data/time_series_covid19_{}_global".format(state) if state == 'recovered': filepath = 'tests/example_data/time_series_19-covid-Recovered.csv' From 73790f7799622a8cd1b03ed5f2f19b50fb7ad1db Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 05:51:00 +0100 Subject: [PATCH 40/46] finally fixed tests --- tests/test_jhu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_jhu.py b/tests/test_jhu.py index 40baf7c2..3215e6df 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -27,7 +27,7 @@ def read_file(self, state): state = state.lower() # Determine filepath. - filepath = "tests/example_data/time_series_covid19_{}_global".format(state) + filepath = "tests/example_data/{}".format(state) if state == 'recovered': filepath = 'tests/example_data/time_series_19-covid-Recovered.csv' From c1a5c522f1422d9cbc210b9704a178ee65603203 Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 05:52:55 +0100 Subject: [PATCH 41/46] works --- tests/test_jhu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_jhu.py b/tests/test_jhu.py index 3215e6df..a503a1c2 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -27,7 +27,7 @@ def read_file(self, state): state = state.lower() # Determine filepath. - filepath = "tests/example_data/{}".format(state) + filepath = "tests/example_data/{}.csv".format(state) if state == 'recovered': filepath = 'tests/example_data/time_series_19-covid-Recovered.csv' From 116d603d5da596b98ddd9a837eb88767d5bc252a Mon Sep 17 00:00:00 2001 From: ExpDev Date: Tue, 24 Mar 2020 06:06:36 +0100 Subject: [PATCH 42/46] add funding option :) --- .github/FUNDING.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..2d0d7eb9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +#patreon: # Replace with a single Patreon username +#open_collective: # Replace with a single Open Collective username +ko_fi: ExpDev +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#otechie: # Replace with a single Otechie username +#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From 63804be667f511d4d43c1abe470f95ce72dafe41 Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 06:33:08 +0100 Subject: [PATCH 43/46] hotfix --- app/services/location/jhu.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index c79900e0..0a42c9d9 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -116,7 +116,7 @@ def get_locations(): # Get all of the data categories locations. confirmed = get_category('confirmed')['locations'] deaths = get_category('deaths')['locations'] - recovered = get_category('recovered')['locations'] + # recovered = get_category('recovered')['locations'] # Final locations to return. locations = [] @@ -127,7 +127,7 @@ def get_locations(): timelines = { 'confirmed' : confirmed[index]['history'], 'deaths' : deaths[index]['history'], - 'recovered' : recovered[index]['history'], + # 'recovered' : recovered[index]['history'], } # Grab coordinates. @@ -151,7 +151,7 @@ def get_locations(): { 'confirmed': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['confirmed'].items() }), 'deaths' : Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['deaths'].items() }), - 'recovered': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['recovered'].items() }) + 'recovered': Timeline({}) } )) From 87a54cc47fe7a625e0f3433561595756712ac141 Mon Sep 17 00:00:00 2001 From: ExpDev07 Date: Tue, 24 Mar 2020 06:38:59 +0100 Subject: [PATCH 44/46] fix test --- tests/test_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_routes.py b/tests/test_routes.py index 18138f09..e08eb349 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -86,7 +86,7 @@ def test_v2_latest(self, mock_request_get, mock_datetime): 'latest': { 'confirmed': 1940, 'deaths': 1940, - 'recovered': 1940 + 'recovered': 0 } } From 2138c57e0f873b8e8de1e1ced9c8d40a4e16c0f3 Mon Sep 17 00:00:00 2001 From: ExpDev Date: Tue, 24 Mar 2020 06:51:49 +0100 Subject: [PATCH 45/46] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2c1b3304..3f089549 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ Support multiple data-sources. ![Covid-19 Recovered](https://covid19-badges.herokuapp.com/recovered/latest) ![Covid-19 Deaths](https://covid19-badges.herokuapp.com/deaths/latest) +## Recovered cases showing 0 +JHU (our main data provider) [no longer provides data for amount of recoveries](https://github.com/ExpDev07/coronavirus-tracker-api/issues/155), and as a result, the API will be showing 0 for this statistic. Apolegies for any inconvenience. Hopefully we'll be able to find an alternative data-source that offers this.** + ## Available data-sources: Currently 2 different data-sources are available to retrieve the data: @@ -28,7 +31,6 @@ Currently 2 different data-sources are available to retrieve the data: __jhu__ data-source will be used as a default source if you don't specify a *source parameter* in your request. - ## API Reference All endpoints are located at ``coronavirus-tracker-api.herokuapp.com/v2/`` and are accessible via https. For instance: you can get data per location by using this URL: @@ -40,7 +42,6 @@ You can open the URL in your browser to further inspect the response. Or you can curl https://coronavirus-tracker-api.herokuapp.com/v2/locations | json_pp ``` - ## API Endpoints ### Sources Endpoint From f9cbcd58b729af8e8da3353429ea8824569bd377 Mon Sep 17 00:00:00 2001 From: ExpDev Date: Tue, 24 Mar 2020 06:52:45 +0100 Subject: [PATCH 46/46] Update README.md --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f089549..840efc72 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -## Coronavirus Tracker API +

+ Coronavirus Tracker API +

+ Provides up-to-date data about Coronavirus outbreak. Includes numbers about confirmed cases, deaths and recovered. Support multiple data-sources. @@ -19,7 +22,8 @@ Support multiple data-sources. ![Covid-19 Deaths](https://covid19-badges.herokuapp.com/deaths/latest) ## Recovered cases showing 0 -JHU (our main data provider) [no longer provides data for amount of recoveries](https://github.com/ExpDev07/coronavirus-tracker-api/issues/155), and as a result, the API will be showing 0 for this statistic. Apolegies for any inconvenience. Hopefully we'll be able to find an alternative data-source that offers this.** + +**JHU (our main data provider) [no longer provides data for amount of recoveries](https://github.com/ExpDev07/coronavirus-tracker-api/issues/155), and as a result, the API will be showing 0 for this statistic. Apolegies for any inconvenience. Hopefully we'll be able to find an alternative data-source that offers this.** ## Available data-sources: