├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── DataBot.sln ├── DataBot ├── Command.fs ├── Countries.fs ├── CovidProvider.fs ├── Data.fs ├── DataBot.fsproj ├── Graph.fs ├── Graph.fsx ├── MentionsRepository.fs ├── Program.fs ├── Twitter.fs └── covidsample.json ├── Dockerfile ├── README.md ├── Tests ├── DataTests.fs ├── GraphTests.fs ├── Program.fs ├── Tests.fsproj └── covidsample.json ├── azure-pipelines.yml └── doc └── img ├── FSharpDataBotBanner.jpg └── Screenshot 2020-05-27 at 16.32.32.png /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 3.1.x 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --no-restore 24 | - name: Test 25 | run: dotnet test --no-build --verbosity normal 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | .idea 7 | .DS_Store 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Aa][Rr][Mm]/ 30 | [Aa][Rr][Mm]64/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Ll]og/ 35 | [Ll]ogs/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # Visual Studio Code 46 | .vscode 47 | 48 | # MSTest test Results 49 | [Tt]est[Rr]esult*/ 50 | [Bb]uild[Ll]og.* 51 | 52 | # NUnit 53 | *.VisualState.xml 54 | TestResult.xml 55 | nunit-*.xml 56 | 57 | # Build Results of an ATL Project 58 | [Dd]ebugPS/ 59 | [Rr]eleasePS/ 60 | dlldata.c 61 | 62 | # Benchmark Results 63 | BenchmarkDotNet.Artifacts/ 64 | 65 | # .NET Core 66 | project.lock.json 67 | project.fragment.lock.json 68 | artifacts/ 69 | 70 | # StyleCop 71 | StyleCopReport.xml 72 | 73 | # Files built by Visual Studio 74 | *_i.c 75 | *_p.c 76 | *_h.h 77 | *.ilk 78 | *.meta 79 | *.obj 80 | *.iobj 81 | *.pch 82 | *.pdb 83 | *.ipdb 84 | *.pgc 85 | *.pgd 86 | *.rsp 87 | *.sbr 88 | *.tlb 89 | *.tli 90 | *.tlh 91 | *.tmp 92 | *.tmp_proj 93 | *_wpftmp.csproj 94 | *.log 95 | *.vspscc 96 | *.vssscc 97 | .builds 98 | *.pidb 99 | *.svclog 100 | *.scc 101 | 102 | # Chutzpah Test files 103 | _Chutzpah* 104 | 105 | # Visual C++ cache files 106 | ipch/ 107 | *.aps 108 | *.ncb 109 | *.opendb 110 | *.opensdf 111 | *.sdf 112 | *.cachefile 113 | *.VC.db 114 | *.VC.VC.opendb 115 | 116 | # Visual Studio profiler 117 | *.psess 118 | *.vsp 119 | *.vspx 120 | *.sap 121 | 122 | # Visual Studio Trace Files 123 | *.e2e 124 | 125 | # TFS 2012 Local Workspace 126 | $tf/ 127 | 128 | # Guidance Automation Toolkit 129 | *.gpState 130 | 131 | # ReSharper is a .NET coding add-in 132 | _ReSharper*/ 133 | *.[Rr]e[Ss]harper 134 | *.DotSettings.user 135 | 136 | # TeamCity is a build add-in 137 | _TeamCity* 138 | 139 | # DotCover is a Code Coverage Tool 140 | *.dotCover 141 | 142 | # AxoCover is a Code Coverage Tool 143 | .axoCover/* 144 | !.axoCover/settings.json 145 | 146 | # Visual Studio code coverage results 147 | *.coverage 148 | *.coveragexml 149 | 150 | # NCrunch 151 | _NCrunch_* 152 | .*crunch*.local.xml 153 | nCrunchTemp_* 154 | 155 | # MightyMoose 156 | *.mm.* 157 | AutoTest.Net/ 158 | 159 | # Web workbench (sass) 160 | .sass-cache/ 161 | 162 | # Installshield output folder 163 | [Ee]xpress/ 164 | 165 | # DocProject is a documentation generator add-in 166 | DocProject/buildhelp/ 167 | DocProject/Help/*.HxT 168 | DocProject/Help/*.HxC 169 | DocProject/Help/*.hhc 170 | DocProject/Help/*.hhk 171 | DocProject/Help/*.hhp 172 | DocProject/Help/Html2 173 | DocProject/Help/html 174 | 175 | # Click-Once directory 176 | publish/ 177 | 178 | # Publish Web Output 179 | *.[Pp]ublish.xml 180 | *.azurePubxml 181 | # Note: Comment the next line if you want to checkin your web deploy settings, 182 | # but database connection strings (with potential passwords) will be unencrypted 183 | *.pubxml 184 | *.publishproj 185 | 186 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 187 | # checkin your Azure Web App publish settings, but sensitive information contained 188 | # in these scripts will be unencrypted 189 | PublishScripts/ 190 | 191 | # NuGet Packages 192 | *.nupkg 193 | # NuGet Symbol Packages 194 | *.snupkg 195 | # The packages folder can be ignored because of Package Restore 196 | **/[Pp]ackages/* 197 | # except build/, which is used as an MSBuild target. 198 | !**/[Pp]ackages/build/ 199 | # Uncomment if necessary however generally it will be regenerated when needed 200 | #!**/[Pp]ackages/repositories.config 201 | # NuGet v3's project.json files produces more ignorable files 202 | *.nuget.props 203 | *.nuget.targets 204 | 205 | # Microsoft Azure Build Output 206 | csx/ 207 | *.build.csdef 208 | 209 | # Microsoft Azure Emulator 210 | ecf/ 211 | rcf/ 212 | 213 | # Windows Store app package directories and files 214 | AppPackages/ 215 | BundleArtifacts/ 216 | Package.StoreAssociation.xml 217 | _pkginfo.txt 218 | *.appx 219 | *.appxbundle 220 | *.appxupload 221 | 222 | # Visual Studio cache files 223 | # files ending in .cache can be ignored 224 | *.[Cc]ache 225 | # but keep track of directories ending in .cache 226 | !?*.[Cc]ache/ 227 | 228 | # Others 229 | ClientBin/ 230 | ~$* 231 | *~ 232 | *.dbmdl 233 | *.dbproj.schemaview 234 | *.jfm 235 | *.pfx 236 | *.publishsettings 237 | orleans.codegen.cs 238 | 239 | # Including strong name files can present a security risk 240 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 241 | #*.snk 242 | 243 | # Since there are multiple workflows, uncomment next line to ignore bower_components 244 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 245 | #bower_components/ 246 | 247 | # RIA/Silverlight projects 248 | Generated_Code/ 249 | 250 | # Backup & report files from converting an old project file 251 | # to a newer Visual Studio version. Backup files are not needed, 252 | # because we have git ;-) 253 | _UpgradeReport_Files/ 254 | Backup*/ 255 | UpgradeLog*.XML 256 | UpgradeLog*.htm 257 | ServiceFabricBackup/ 258 | *.rptproj.bak 259 | 260 | # SQL Server files 261 | *.mdf 262 | *.ldf 263 | *.ndf 264 | 265 | # Business Intelligence projects 266 | *.rdl.data 267 | *.bim.layout 268 | *.bim_*.settings 269 | *.rptproj.rsuser 270 | *- [Bb]ackup.rdl 271 | *- [Bb]ackup ([0-9]).rdl 272 | *- [Bb]ackup ([0-9][0-9]).rdl 273 | 274 | # Microsoft Fakes 275 | FakesAssemblies/ 276 | 277 | # GhostDoc plugin setting file 278 | *.GhostDoc.xml 279 | 280 | # Node.js Tools for Visual Studio 281 | .ntvs_analysis.dat 282 | node_modules/ 283 | 284 | # Visual Studio 6 build log 285 | *.plg 286 | 287 | # Visual Studio 6 workspace options file 288 | *.opt 289 | 290 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 291 | *.vbw 292 | 293 | # Visual Studio LightSwitch build output 294 | **/*.HTMLClient/GeneratedArtifacts 295 | **/*.DesktopClient/GeneratedArtifacts 296 | **/*.DesktopClient/ModelManifest.xml 297 | **/*.Server/GeneratedArtifacts 298 | **/*.Server/ModelManifest.xml 299 | _Pvt_Extensions 300 | 301 | # Paket dependency manager 302 | .paket/paket.exe 303 | paket-files/ 304 | 305 | # FAKE - F# Make 306 | .fake/ 307 | 308 | # CodeRush personal settings 309 | .cr/personal 310 | 311 | # Python Tools for Visual Studio (PTVS) 312 | __pycache__/ 313 | *.pyc 314 | 315 | # Cake - Uncomment if you are using it 316 | # tools/** 317 | # !tools/packages.config 318 | 319 | # Tabs Studio 320 | *.tss 321 | 322 | # Telerik's JustMock configuration file 323 | *.jmconfig 324 | 325 | # BizTalk build output 326 | *.btp.cs 327 | *.btm.cs 328 | *.odx.cs 329 | *.xsd.cs 330 | 331 | # OpenCover UI analysis results 332 | OpenCover/ 333 | 334 | # Azure Stream Analytics local run output 335 | ASALocalRun/ 336 | 337 | # MSBuild Binary and Structured Log 338 | *.binlog 339 | 340 | # NVidia Nsight GPU debugger configuration file 341 | *.nvuser 342 | 343 | # MFractors (Xamarin productivity tool) working folder 344 | .mfractor/ 345 | 346 | # Local History for Visual Studio 347 | .localhistory/ 348 | 349 | # BeatPulse healthcheck temp database 350 | healthchecksdb 351 | 352 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 353 | MigrationBackup/ 354 | 355 | # Ionide (cross platform F# VS Code tools) working folder 356 | .ionide/ 357 | 358 | # Launch Settings 359 | DataBot/Properties/launchSettings.json 360 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | mono: none 3 | dist: xenial 4 | dotnet: 3.1 5 | install: 6 | - dotnet restore 7 | script: 8 | - dotnet build 9 | - dotnet test Tests/Tests.fsproj -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /DataBot.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30011.22 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "DataBot", "DataBot\DataBot.fsproj", "{8ADDD216-0BC7-407F-A9D9-3E36E05A74A7}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Tests", "Tests\Tests.fsproj", "{9677EA27-717A-4A58-9233-00EC84A5D0A0}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {8ADDD216-0BC7-407F-A9D9-3E36E05A74A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {8ADDD216-0BC7-407F-A9D9-3E36E05A74A7}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {8ADDD216-0BC7-407F-A9D9-3E36E05A74A7}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {8ADDD216-0BC7-407F-A9D9-3E36E05A74A7}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {9677EA27-717A-4A58-9233-00EC84A5D0A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {9677EA27-717A-4A58-9233-00EC84A5D0A0}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {9677EA27-717A-4A58-9233-00EC84A5D0A0}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {9677EA27-717A-4A58-9233-00EC84A5D0A0}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {C2581309-8148-484D-867C-C7B8694E58F6} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /DataBot/Command.fs: -------------------------------------------------------------------------------- 1 | module Command 2 | 3 | open Data 4 | open System.Text.RegularExpressions 5 | 6 | type IndicatorFun = 7 | | WB of (WBCountry -> WBIndicator) 8 | | Covid of (Countries.Country->Async) 9 | 10 | type Token = 11 | | Country of Countries.Country 12 | | Indicator of IndicatorFun 13 | | Year of int 14 | | Unknown 15 | 16 | type FoldState = { 17 | Country : Countries.Country list 18 | Indicator : IndicatorFun list 19 | Year : int list 20 | } 21 | 22 | type Command = { 23 | Countries : Countries.Country list 24 | Indicator : IndicatorFun 25 | StartYear : int option 26 | EndYear : int option 27 | } 28 | 29 | let parse (token:string) = 30 | match token with 31 | | IndicatorMatcher indicator -> 32 | Token.Indicator (WB indicator) 33 | | CountryMatcher country -> 34 | Token.Country country 35 | | YearMatcher year -> 36 | Token.Year year 37 | | CovidMatcher covid -> 38 | Token.Indicator (Covid covid) 39 | | _ -> 40 | Token.Unknown 41 | 42 | let Parse (commandLine:string) = 43 | let tokens = 44 | Regex.Split(commandLine, "\s") 45 | |> Set.ofArray 46 | |> Set.toList 47 | |> List.map parse 48 | 49 | let folder (state: FoldState) (token:Token) = 50 | match token with 51 | | Token.Country c -> { state with Country = state.Country @ [c] } 52 | | Token.Indicator i -> { state with Indicator = state.Indicator @ [i] } 53 | | Token.Year y -> { state with Year = state.Year @ [y] } 54 | | _ -> state 55 | 56 | let state = 57 | tokens 58 | |> List.fold folder { Country = []; Indicator = []; Year = [] } 59 | 60 | let startYear = 61 | match state.Year with 62 | | [] -> None 63 | | years -> years |> List.min |> Some 64 | let endYear = 65 | match state.Year with 66 | | [] -> None 67 | | years -> years |> List.max |> Some 68 | 69 | seq { 70 | for indicator in state.Indicator do 71 | yield { Countries = state.Country; Indicator = indicator; StartYear = startYear; EndYear = endYear } 72 | } |> Seq.toList 73 | -------------------------------------------------------------------------------- /DataBot/Countries.fs: -------------------------------------------------------------------------------- 1 | module Countries 2 | 3 | type Country = 4 | { Name: string 5 | TwoLetterCode: string 6 | ThreeLetterCode: string } 7 | 8 | let country name two three = 9 | { Name = name 10 | TwoLetterCode = two 11 | ThreeLetterCode = three } 12 | 13 | let List = 14 | [ country "afghanistan" "af" "afg" 15 | country "albania" "al" "alb" 16 | country "algeria" "dz" "dza" 17 | country "american samoa" "as" "asm" 18 | country "andorra" "ad" "and" 19 | country "angola" "ao" "ago" 20 | country "anguilla" "ai" "aia" 21 | country "antarctica" "aq" "ata" 22 | country "antigua and barbuda" "ag" "atg" 23 | country "argentina" "ar" "arg" 24 | country "armenia" "am" "arm" 25 | country "aruba" "aw" "abw" 26 | country "australia" "au" "aus" 27 | country "austria" "at" "aut" 28 | country "azerbaijan" "az" "aze" 29 | country "bahamas" "bs" "bhs" 30 | country "bahrain" "bh" "bhr" 31 | country "bangladesh" "bd" "bgd" 32 | country "barbados" "bb" "brb" 33 | country "belarus" "by" "blr" 34 | country "belgium" "be" "bel" 35 | country "belize" "bz" "blz" 36 | country "benin" "bj" "ben" 37 | country "bermuda" "bm" "bmu" 38 | country "bhutan" "bt" "btn" 39 | country "bolivia" "bo" "bol" 40 | country "bonaire" "bq" "bes" 41 | country "bosnia and herzegovina" "ba" "bih" 42 | country "botswana" "bw" "bwa" 43 | country "bouvet island" "bv" "bvt" 44 | country "brazil" "br" "bra" 45 | country "brasil" "br" "bra" 46 | country "british indian ocean territory" "io" "iot" 47 | country "brunei darussalam" "bn" "brn" 48 | country "bulgaria" "bg" "bgr" 49 | country "burkina faso" "bf" "bfa" 50 | country "burundi" "bi" "bdi" 51 | country "cambodia" "kh" "khm" 52 | country "cameroon" "cm" "cmr" 53 | country "canada" "ca" "can" 54 | country "cape verde" "cv" "cpv" 55 | country "cayman islands" "ky" "cym" 56 | country "central african republic" "cf" "caf" 57 | country "chad" "td" "tcd" 58 | country "chile" "cl" "chl" 59 | country "china" "cn" "chn" 60 | country "christmas island" "cx" "cxr" 61 | country "cocos islands" "cc" "cck" 62 | country "keeling islands" "cc" "cck" 63 | country "colombia" "co" "col" 64 | country "comoros" "km" "com" 65 | country "congo" "cg" "cog" 66 | country "democratic republic of the congo" "cd" "cod" 67 | country "cook islands" "ck" "cok" 68 | country "costa rica" "cr" "cri" 69 | country "croatia" "hr" "hrv" 70 | country "cuba" "cu" "cub" 71 | country "curacao" "cw" "cuw" 72 | country "cyprus" "cy" "cyp" 73 | country "czech republic" "cz" "cze" 74 | country "cote d'ivoire" "ci" "civ" 75 | country "denmark" "dk" "dnk" 76 | country "djibouti" "dj" "dji" 77 | country "dominica" "dm" "dma" 78 | country "dominican republic" "do" "dom" 79 | country "ecuador" "ec" "ecu" 80 | country "egypt" "eg" "egy" 81 | country "el salvador" "sv" "slv" 82 | country "equatorial guinea" "gq" "gnq" 83 | country "eritrea" "er" "eri" 84 | country "estonia" "ee" "est" 85 | country "ethiopia" "et" "eth" 86 | country "falkland" "fk" "flk" 87 | country "maldivas" "fk" "flk" 88 | country "faroe islands" "fo" "fro" 89 | country "fiji" "fj" "fji" 90 | country "finland" "fi" "fin" 91 | country "france" "fr" "fra" 92 | country "french guiana" "gf" "guf" 93 | country "french polynesia" "pf" "pyf" 94 | country "french southern territories" "tf" "atf" 95 | country "gabon" "ga" "gab" 96 | country "gambia" "gm" "gmb" 97 | country "georgia" "ge" "geo" 98 | country "germany" "de" "deu" 99 | country "ghana" "gh" "gha" 100 | country "gibraltar" "gi" "gib" 101 | country "greece" "gr" "grc" 102 | country "greenland" "gl" "grl" 103 | country "grenada" "gd" "grd" 104 | country "guadeloupe" "gp" "glp" 105 | country "guam" "gu" "gum" 106 | country "guatemala" "gt" "gtm" 107 | country "guernsey" "gg" "ggy" 108 | country "guinea" "gn" "gin" 109 | country "guinea-bissau" "gw" "gnb" 110 | country "guyana" "gy" "guy" 111 | country "haiti" "ht" "hti" 112 | country "heard island and mcdonald islands" "hm" "hmd" 113 | country "vatican" "va" "vat" 114 | country "honduras" "hn" "hnd" 115 | country "hong kong" "hk" "hkg" 116 | country "hungary" "hu" "hun" 117 | country "iceland" "is" "isl" 118 | country "india" "in" "ind" 119 | country "indonesia" "id" "idn" 120 | country "iran" "ir" "irn" 121 | country "iraq" "iq" "irq" 122 | country "ireland" "ie" "irl" 123 | country "isle of man" "im" "imn" 124 | country "israel" "il" "isr" 125 | country "italy" "it" "ita" 126 | country "jamaica" "jm" "jam" 127 | country "japan" "jp" "jpn" 128 | country "jersey" "je" "jey" 129 | country "jordan" "jo" "jor" 130 | country "kazakhstan" "kz" "kaz" 131 | country "kenya" "ke" "ken" 132 | country "kiribati" "ki" "kir" 133 | country "north korea" "kp" "prk" 134 | country "south korea" "kr" "kor" 135 | country "kuwait" "kw" "kwt" 136 | country "kyrgyzstan" "kg" "kgz" 137 | country "lao" "la" "lao" 138 | country "latvia" "lv" "lva" 139 | country "lebanon" "lb" "lbn" 140 | country "lesotho" "ls" "lso" 141 | country "liberia" "lr" "lbr" 142 | country "libya" "ly" "lby" 143 | country "liechtenstein" "li" "lie" 144 | country "lithuania" "lt" "ltu" 145 | country "luxembourg" "lu" "lux" 146 | country "macao" "mo" "mac" 147 | country "macedonia" "mk" "mkd" 148 | country "madagascar" "mg" "mdg" 149 | country "malawi" "mw" "mwi" 150 | country "malaysia" "my" "mys" 151 | country "maldives" "mv" "mdv" 152 | country "mali" "ml" "mli" 153 | country "malta" "mt" "mlt" 154 | country "marshall islands" "mh" "mhl" 155 | country "martinique" "mq" "mtq" 156 | country "mauritania" "mr" "mrt" 157 | country "mauritius" "mu" "mus" 158 | country "mayotte" "yt" "myt" 159 | country "mexico" "mx" "mex" 160 | country "micronesia" "fm" "fsm" 161 | country "moldova" "md" "mda" 162 | country "monaco" "mc" "mco" 163 | country "mongolia" "mn" "mng" 164 | country "montenegro" "me" "mne" 165 | country "montserrat" "ms" "msr" 166 | country "morocco" "ma" "mar" 167 | country "mozambique" "mz" "moz" 168 | country "myanmar" "mm" "mmr" 169 | country "namibia" "na" "nam" 170 | country "nauru" "nr" "nru" 171 | country "nepal" "np" "npl" 172 | country "netherlands" "nl" "nld" 173 | country "new caledonia" "nc" "ncl" 174 | country "new zealand" "nz" "nzl" 175 | country "nicaragua" "ni" "nic" 176 | country "niger" "ne" "ner" 177 | country "nigeria" "ng" "nga" 178 | country "niue" "nu" "niu" 179 | country "norfolk island" "nf" "nfk" 180 | country "mariana islands" "mp" "mnp" 181 | country "norway" "no" "nor" 182 | country "oman" "om" "omn" 183 | country "pakistan" "pk" "pak" 184 | country "palau" "pw" "plw" 185 | country "palestine" "ps" "pse" 186 | country "panama" "pa" "pan" 187 | country "papua new guinea" "pg" "png" 188 | country "paraguay" "py" "pry" 189 | country "peru" "pe" "per" 190 | country "philippines" "ph" "phl" 191 | country "pitcairn" "pn" "pcn" 192 | country "poland" "pl" "pol" 193 | country "portugal" "pt" "prt" 194 | country "puerto rico" "pr" "pri" 195 | country "qatar" "qa" "qat" 196 | country "romania" "ro" "rou" 197 | country "russia" "ru" "rus" 198 | country "rwanda" "rw" "rwa" 199 | country "reunion" "re" "reu" 200 | country "saint barthelemy" "bl" "blm" 201 | country "saint helena" "sh" "shn" 202 | country "saint kitts and nevis" "kn" "kna" 203 | country "saint lucia" "lc" "lca" 204 | country "saint martin" "mf" "maf" 205 | country "saint pierre and miquelon" "pm" "spm" 206 | country "saint vincent and the grenadines" "vc" "vct" 207 | country "samoa" "ws" "wsm" 208 | country "san marino" "sm" "smr" 209 | country "sao tome and principe" "st" "stp" 210 | country "saudi arabia" "sa" "sau" 211 | country "senegal" "sn" "sen" 212 | country "serbia" "rs" "srb" 213 | country "seychelles" "sc" "syc" 214 | country "sierra leone" "sl" "sle" 215 | country "singapore" "sg" "sgp" 216 | country "saint maarten" "sx" "sxm" 217 | country "slovakia" "sk" "svk" 218 | country "slovenia" "si" "svn" 219 | country "solomon islands" "sb" "slb" 220 | country "somalia" "so" "som" 221 | country "south africa" "za" "zaf" 222 | country "south georgia and the south sandwich islands" "gs" "sgs" 223 | country "south sudan" "ss" "ssd" 224 | country "spain" "es" "esp" 225 | country "sri lanka" "lk" "lka" 226 | country "sudan" "sd" "sdn" 227 | country "suriname" "sr" "sur" 228 | country "svalbard and jan mayen" "sj" "sjm" 229 | country "swaziland" "sz" "swz" 230 | country "sweden" "se" "swe" 231 | country "switzerland" "ch" "che" 232 | country "syrian arab republic" "sy" "syr" 233 | country "taiwan" "tw" "twn" 234 | country "tajikistan" "tj" "tjk" 235 | country "united republic of tanzania" "tz" "tza" 236 | country "thailand" "th" "tha" 237 | country "timor-leste" "tl" "tls" 238 | country "togo" "tg" "tgo" 239 | country "tokelau" "tk" "tkl" 240 | country "tonga" "to" "ton" 241 | country "trinidad and tobago" "tt" "tto" 242 | country "tunisia" "tn" "tun" 243 | country "turkey" "tr" "tur" 244 | country "turkmenistan" "tm" "tkm" 245 | country "turks and caicos islands" "tc" "tca" 246 | country "tuvalu" "tv" "tuv" 247 | country "uganda" "ug" "uga" 248 | country "ukraine" "ua" "ukr" 249 | country "united arab emirates" "ae" "are" 250 | country "united kingdom" "gb" "gbr" 251 | country "uk" "gb" "gbr" 252 | country "united states" "us" "usa" 253 | country "united states minor outlying islands" "um" "umi" 254 | country "uruguay" "uy" "ury" 255 | country "uzbekistan" "uz" "uzb" 256 | country "vanuatu" "vu" "vut" 257 | country "venezuela" "ve" "ven" 258 | country "viet nam" "vn" "vnm" 259 | country "british virgin islands" "vg" "vgb" 260 | country "us virgin islands" "vi" "vir" 261 | country "wallis and futuna" "wf" "wlf" 262 | country "western sahara" "eh" "esh" 263 | country "yemen" "ye" "yem" 264 | country "zambia" "zm" "zmb" 265 | country "zimbabwe" "zw" "zwe" ] 266 | -------------------------------------------------------------------------------- /DataBot/CovidProvider.fs: -------------------------------------------------------------------------------- 1 | module CovidProvider 2 | open FSharp.Data 3 | open ZedGraph 4 | 5 | type Covid = JsonProvider<"covidsample.json", SampleIsList=true> 6 | 7 | let totalVsNew (total: (double*double) seq) (newCases: (double*double) list) = 8 | let totalValues = 9 | total 10 | |> Seq.sortBy( fun (x,_) -> x ) 11 | |> Seq.map( fun (_,v) -> v ) 12 | 13 | let newValues = 14 | newCases 15 | |> Seq.sortBy( fun (x,_) -> x ) 16 | |> Seq.map( fun (_,v) -> v ) 17 | Seq.zip totalValues newValues 18 | 19 | let folder (state: (double*double) list) (current:double*double): (double*double) list = 20 | let (_, prev) = state |> List.last 21 | let (dt, curr) = current 22 | state @ [(dt, curr)] 23 | 24 | type CovidIndicator (code:string, name:string, values:Covid.Root[]) = 25 | member __.Name = name 26 | member __.Code = code 27 | member __.Deaths = values |> Seq.map (fun value -> (double (value.Date.DateTime |> XDate), double value.Deaths)) 28 | member this.NewDeaths = this.Deaths |> Seq.toList |> List.fold folder [(0.0, 0.0)] 29 | member this.TotalVsNewDeaths = totalVsNew (this.Deaths) (this.NewDeaths) 30 | member __.Confirmed = values |> Seq.map (fun value -> (double (value.Date.DateTime |> XDate), double value.Confirmed)) 31 | member this.NewConfirmed = this.Confirmed |> Seq.toList |> List.fold folder [(0.0, 0.0)] 32 | member this.TotalVsNewConfirmed = totalVsNew (this.Confirmed) (this.NewConfirmed) 33 | member __.Recovered = values |> Seq.map (fun value -> (double (value.Date.DateTime |> XDate), double value.Recovered)) 34 | member this.NewRecovered = this.Recovered |> Seq.toList |> List.fold folder [(0.0, 0.0)] 35 | member this.TotalVsNewRecovered = totalVsNew (this.Recovered) (this.NewRecovered) 36 | 37 | let AsyncFetch code name= 38 | let baseUrl = sprintf "https://api.covid19api.com/total/dayone/country/%s" code 39 | async { 40 | let! response = Http.AsyncRequestString baseUrl |> Async.Catch 41 | return 42 | match response with 43 | | Choice1Of2 json -> 44 | CovidIndicator( code, name, Covid.ParseList json) |> Some 45 | | Choice2Of2 _ -> 46 | None 47 | } 48 | -------------------------------------------------------------------------------- /DataBot/Data.fs: -------------------------------------------------------------------------------- 1 | module Data 2 | open System 3 | open FSharp.Data 4 | open System.Text.RegularExpressions 5 | 6 | let private data = WorldBankData.GetDataContext() 7 | let private _c = data.Countries 8 | 9 | type WBCountry = WorldBankData.ServiceTypes.Country 10 | type WBIndicator = Runtime.WorldBank.Indicator 11 | 12 | let (|CountryMatcher|_|) (key:string) = 13 | Countries.List 14 | |> List.tryFind (fun region -> 15 | region.Name = key 16 | || region.ThreeLetterCode = key 17 | || region.TwoLetterCode = key 18 | ) 19 | 20 | let toWBCountry (region:Countries.Country) = 21 | _c |> Seq.tryFind (fun country -> country.Code = region.ThreeLetterCode.ToUpper()) 22 | 23 | 24 | let (|CovidMatcher|_|) (key: string) = 25 | match key with 26 | | "covid" -> Some (fun (country:Countries.Country) -> 27 | CovidProvider.AsyncFetch (country.TwoLetterCode) (country.ThreeLetterCode.ToUpper())) 28 | |_ -> None 29 | 30 | let (|IndicatorMatcher|_|) (key: string) = 31 | match key with 32 | | "gdp" -> 33 | Some (fun (c:WBCountry) -> c.Indicators.``GDP (current US$)``) 34 | | "gdp/capita" -> 35 | Some (fun (c:WBCountry) -> c.Indicators.``GDP per capita (current US$)``) 36 | | "gdp/growth" -> 37 | Some (fun (c:WBCountry) -> c.Indicators.``GDP growth (annual %)``) 38 | | "literacy" | "literacy/young" -> 39 | Some (fun (c:WBCountry) -> c.Indicators.``Literacy rate, youth total (% of people ages 15-24)``) 40 | | "literacy/adult" -> 41 | Some (fun (c:WBCountry) -> c.Indicators.``Literacy rate, adult total (% of people ages 15 and above)``) 42 | | "employment" -> 43 | Some (fun (c:WBCountry) -> c.Indicators.``Employment to population ratio, 15+, total (%) (national estimate)``) 44 | | "unemployment" -> 45 | Some (fun (c:WBCountry) -> c.Indicators.``Unemployment, total (% of total labor force) (modeled ILO estimate)``) 46 | | "electricity-access" -> 47 | Some (fun (c:WBCountry) -> c.Indicators.``Access to electricity (% of population)``) 48 | | "exchange-rate/usd" -> 49 | Some (fun (c:WBCountry) -> c.Indicators.``Official exchange rate (LCU per US$, period average)``) 50 | | "poverty-gap" -> 51 | Some (fun (c:WBCountry) -> c.Indicators.``Poverty headcount ratio at national poverty lines (% of population)``) 52 | | "stock-market" -> 53 | Some (fun (c:WBCountry) -> c.Indicators.``Stocks traded, total value (current US$)``) 54 | | "external-balance" -> 55 | Some (fun (c:WBCountry) -> c.Indicators.``External balance on goods and services (current US$)``) 56 | |_ -> None 57 | 58 | let (|YearMatcher|_|) (token:string) = 59 | let m = Regex.Match( token, @"(19[0-9]{2}|20[0-9]{2})" ) 60 | if m.Success then 61 | let year = m.Groups.[1].Value 62 | Convert.ToInt32 year |> Some 63 | else 64 | None -------------------------------------------------------------------------------- /DataBot/DataBot.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | netcoreapp3.1 5 | false 6 | 7 | 8 | 9 | 10 | 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | all 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /DataBot/Graph.fs: -------------------------------------------------------------------------------- 1 | module Graph 2 | open ZedGraph 3 | open System 4 | open System.IO 5 | open System.Drawing 6 | open FSharp.Data 7 | 8 | type Indicator = Runtime.WorldBank.Indicator 9 | type ValueSeq<'X,'Y> = ('X*'Y) seq 10 | 11 | let indexToColor (i:int):Color = 12 | let Pallete = [| 13 | "#a0e85b" 14 | "#572b9e" 15 | "#b6c5f5" 16 | "#22577a" 17 | "#a2e1ca" 18 | "#096013" 19 | "#1eefc9" 20 | "#7d2b22" 21 | "#fad139" 22 | "#fd2c3b" 23 | "#42a359" 24 | "#11a0aa" 25 | "#f27ff5" 26 | "#b41270" 27 | "#8f77c0" 28 | "#8d12dc" 29 | "#51f310" 30 | "#d47767" 31 | "#f79302" 32 | "#544516" 33 | |] 34 | ColorTranslator.FromHtml <| Pallete.[ i % Pallete.Length ] 35 | 36 | let createPane (title: string) = 37 | let pane = GraphPane() 38 | pane.Rect <- RectangleF(0.0f, 0.0f, 1200.0f, 675.0f) 39 | pane.Title.Text <- title 40 | pane.XAxis.Scale.MinGrace <- 0.0 41 | pane.XAxis.Scale.MaxGrace <- 0.0 42 | pane 43 | 44 | let pointList (startValue:int option) (endValue:int option) indicator = 45 | let points = PointPairList() 46 | 47 | let dateFilter = 48 | match startValue, endValue with 49 | | Some sd, Some ed -> fun (value, _) -> value >= double sd && value <= double ed 50 | | Some sd, None -> fun (value, _) -> value >= double sd 51 | | None, Some ed -> fun (value, _) -> value <= double ed 52 | | None, None -> fun (_) -> true 53 | 54 | for (year, value) in (indicator |> Seq.filter dateFilter ) do 55 | points.Add(year, value) 56 | 57 | points 58 | 59 | let addLineToPane (startDate:int option) (endDate:int option) (pane:GraphPane) code indicator idx = 60 | let points = pointList startDate endDate indicator 61 | let curve = pane.AddCurve(code, points, indexToColor idx, SymbolType.None) 62 | curve.Line.IsSmooth <- true 63 | curve.Line.IsAntiAlias <- true 64 | curve.Line.Width <- 3.0f 65 | curve 66 | 67 | let paneToStream (pane:GraphPane) = 68 | use graph = Graphics.FromImage(new Bitmap(1200, 675)) 69 | pane.AxisChange(graph) 70 | graph.Dispose() 71 | use bmp = pane.GetImage() 72 | let stream = new MemoryStream() 73 | bmp.Save(stream, Imaging.ImageFormat.Png) 74 | stream 75 | 76 | let Line title indicators startValue endValue isDate isLogX isLogY = 77 | let pane = createPane(title) 78 | pane.XAxis.MinorGrid.IsVisible <- true 79 | pane.XAxis.MajorGrid.IsVisible <- true 80 | pane.XAxis.MajorGrid.DashOff <- 0.0f 81 | pane.XAxis.MajorGrid.Color <- Color.Gray 82 | pane.YAxis.MajorGrid.IsVisible <- true 83 | pane.YAxis.MinorGrid.IsVisible <- true 84 | pane.YAxis.MajorGrid.DashOff <- 0.0f 85 | pane.YAxis.MajorGrid.Color <- Color.Gray 86 | pane.YAxis.Scale.IsUseTenPower <- false 87 | if isDate then pane.XAxis.Type <- AxisType.Date 88 | if isLogY then pane.YAxis.Type <- AxisType.Log 89 | if isLogX then pane.XAxis.Type <- AxisType.Log 90 | let addLine = addLineToPane startValue endValue pane 91 | 92 | indicators 93 | |> Seq.iteri( fun idx (legend, indicator) -> addLine legend indicator idx |> ignore ) 94 | 95 | pane |> paneToStream 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /DataBot/Graph.fsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhogemann/FSharpDataBot/b87d3a80bf97dff5451c30d2fbf3372eddbc8ed2/DataBot/Graph.fsx -------------------------------------------------------------------------------- /DataBot/MentionsRepository.fs: -------------------------------------------------------------------------------- 1 | module MentionsRepository 2 | open System 3 | open Microsoft.EntityFrameworkCore 4 | 5 | type [] MentionEntry = { 6 | Id: int64 7 | TimeStamp: DateTime 8 | } 9 | 10 | type SqliteMentionContext (useSqlite, accountEndpoint, accountKey, databaseName) = 11 | inherit DbContext() 12 | [] 13 | val mutable mentions: DbSet 14 | member public this.Mentions with get() = this.mentions and set m = this.mentions <- m 15 | override _.OnConfiguring optionsBuilder = 16 | if useSqlite then 17 | optionsBuilder.UseSqlite("Data Source=mentions.db") |> ignore 18 | else 19 | optionsBuilder.UseCosmos(accountEndpoint, accountKey, databaseName) |> ignore 20 | 21 | 22 | let db = 23 | let getEnv = Environment.GetEnvironmentVariable 24 | let useSQLite = 25 | match "USE_SQL_LITE" |> getEnv with 26 | | null -> false 27 | | s -> s |> bool.Parse 28 | let accountEndpoint = "COSMOS_DB_ACCOUNT_ENDPOINT" |> getEnv 29 | let accountKey = "COSMOS_DB_ACCOUNT_KEY" |> getEnv 30 | let databaseName = "COSMOS_DB_DATABASE_NAME" |> getEnv 31 | 32 | new SqliteMentionContext(useSQLite, accountEndpoint, accountKey, databaseName) 33 | 34 | db.Database.EnsureCreated() |> ignore 35 | 36 | let SaveMentions mentions = 37 | for mention in mentions do 38 | db.Mentions.Add(mention) |> ignore 39 | db.SaveChangesAsync() |> Async.AwaitTask 40 | 41 | let GetLatestMention () = 42 | query { 43 | for mention in db.Mentions do 44 | sortByDescending mention.TimeStamp 45 | select mention 46 | take 1 47 | } -------------------------------------------------------------------------------- /DataBot/Program.fs: -------------------------------------------------------------------------------- 1 | module App 2 | 3 | [] 4 | let main argv = 5 | 6 | let feedReader = Twitter.FeedReader() 7 | let result = 8 | feedReader.Start() 9 | |> Async.Catch 10 | |> Async.RunSynchronously 11 | 12 | match result with 13 | | Choice1Of2 _ -> 0 14 | | Choice2Of2 e -> 15 | printfn "%A" e 16 | 1 17 | -------------------------------------------------------------------------------- /DataBot/Twitter.fs: -------------------------------------------------------------------------------- 1 | module Twitter 2 | open System 3 | open System.IO 4 | open MentionsRepository 5 | open Tweetinvi 6 | open Tweetinvi.Models 7 | open Tweetinvi.Parameters 8 | 9 | let getEnv = Environment.GetEnvironmentVariable 10 | 11 | let twitterApiKey = 12 | "TWITTER_API_KEY" |> getEnv 13 | let twitterApiSecretKey = 14 | "TWITTER_API_SECRET_KEY" |> getEnv 15 | let twitterAccessToken = 16 | "TWITTER_ACCESS_TOKEN" |> getEnv 17 | let twitterAccessTokenSecret = 18 | "TWITTER_ACCESS_TOKEN_SECRET" |> getEnv 19 | let lockFilePath = 20 | "FS_DATA_BOT_LAST_REPLY" |> getEnv 21 | 22 | type Reply = 23 | | Tweet of PublishTweetOptionalParameters 24 | | Error of string 25 | 26 | type FeedReader() = 27 | do 28 | Auth.SetUserCredentials( 29 | twitterApiKey, 30 | twitterApiSecretKey, 31 | twitterAccessToken, 32 | twitterAccessTokenSecret) |> ignore 33 | TweetinviConfig.CurrentThreadSettings.TweetMode <- TweetMode.Extended 34 | let botUser = User.GetAuthenticatedUser().ScreenName 35 | 36 | let commandToReply indicator (countries:Countries.Country seq) startValue endValue = async { 37 | match indicator with 38 | | Command.IndicatorFun.WB ifun -> 39 | let countries = 40 | countries 41 | |> Seq.map Data.toWBCountry 42 | |> Seq.filter Option.isSome 43 | |> Seq.map Option.get 44 | |> Seq.toList 45 | 46 | let title = 47 | let country = 48 | countries 49 | |> Seq.head 50 | |> ifun 51 | country.Name 52 | 53 | let indicators = 54 | countries 55 | |> Seq.map ( fun country -> 56 | let i = ifun country |> Seq.map (fun (x,y) -> (double x, double y) ) 57 | country.Name, i 58 | ) 59 | return seq { yield Graph.Line title indicators startValue endValue false false false } 60 | 61 | | Command.IndicatorFun.Covid ifun -> 62 | let! maybeIndicators = 63 | countries 64 | |> Seq.map (fun country -> 65 | country |> ifun 66 | ) 67 | |> Async.Parallel 68 | let indicators = maybeIndicators |> Seq.filter (Option.isSome) |> Seq.map (Option.get) 69 | 70 | let deaths = indicators |> Seq.map (fun country -> country.Name, country.Deaths) 71 | let confirmed = indicators |> Seq.map (fun country -> country.Name, country.Confirmed) 72 | let recovered = indicators |> Seq.map (fun country -> country.Name, country.Recovered) 73 | return seq { 74 | yield Graph.Line "Covid-19 - Deaths" deaths None None true false false 75 | yield Graph.Line "Covid-19 - Confirmed" confirmed None None true false false 76 | yield Graph.Line "Covid-19 - Recovered" recovered None None true false false 77 | } 78 | } 79 | 80 | let execute (commands:Command.Command list) = async { 81 | let! replies = 82 | commands 83 | |> Seq.map ( fun command -> async { 84 | let! lines = commandToReply (command.Indicator) (command.Countries) (command.StartYear) (command.EndYear) 85 | return lines 86 | |> Seq.map (fun line -> Upload.UploadBinary( line.ToArray())) 87 | |> Seq.chunkBySize 4 88 | |> Seq.map (ResizeArray) 89 | |> Seq.map (fun medias -> PublishTweetOptionalParameters(Medias = medias)) 90 | }) 91 | |> Async.Parallel 92 | return replies |> Seq.concat 93 | } 94 | 95 | let reply (mentions: IMention seq) = async { 96 | let! replies = 97 | mentions 98 | |> Seq.filter (fun mention -> not (isNull mention.Text)) 99 | |> Seq.map ( fun mention -> 100 | 101 | printfn "replying to %s | %s" mention.CreatedBy.ScreenName mention.Text 102 | 103 | let index = mention.Text.IndexOf(botUser) 104 | 105 | // Just use the part from "@botUser" onward... 106 | let mentionText = mention.Text.Substring(index); 107 | 108 | let commands = 109 | mentionText.ToLowerInvariant() 110 | |> Command.Parse 111 | |> List.chunkBySize 4 112 | commands, mention.Id, mention.CreatedBy.ScreenName 113 | ) 114 | |> Seq.map (fun (commands, mentionId, userHandle) -> async { 115 | let! replies = commands |> List.map execute |> Async.Sequential 116 | return 117 | replies 118 | |> Seq.concat 119 | |> Seq.map (fun reply -> reply, mentionId, userHandle) 120 | }) 121 | |> Async.Parallel 122 | 123 | return! 124 | replies 125 | |> Seq.concat 126 | |> Seq.map(fun (reply, mentionId, userHandle) -> 127 | reply.InReplyToTweetId <- Nullable(mentionId) 128 | TweetAsync.PublishTweet(sprintf "@%s" userHandle, reply) |> Async.AwaitTask 129 | ) 130 | |> Async.Parallel 131 | } 132 | 133 | let start () = async { 134 | printfn "fetching tweets" 135 | let mayBeLastMention = GetLatestMention() |> Seq.tryHead 136 | let maybeMentions = 137 | match mayBeLastMention with 138 | | Some mention -> 139 | let parameters = MentionsTimelineParameters() 140 | parameters.SinceId <- mention.Id 141 | parameters.MaximumNumberOfTweetsToRetrieve <- 100 142 | Timeline.GetMentionsTimeline(parameters) |> Option.ofObj 143 | | None -> Timeline.GetMentionsTimeline() |> Option.ofObj 144 | 145 | match maybeMentions with 146 | | Some mentions -> 147 | do! mentions 148 | |> Seq.map (fun mention -> { Id = mention.Id; TimeStamp = mention.CreatedAt }) 149 | |> SaveMentions 150 | |> Async.Ignore 151 | return! reply mentions |> Async.Ignore 152 | | None -> 153 | return () 154 | } 155 | 156 | member _.Start () = async { 157 | while true do 158 | do! start() 159 | do! Async.Sleep (1000 * 60) 160 | } -------------------------------------------------------------------------------- /DataBot/covidsample.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Country": "Brazil", 4 | "CountryCode": "BR", 5 | "Province": "", 6 | "City": "", 7 | "CityCode": "", 8 | "Lat": "-14.24", 9 | "Lon": "-51.93", 10 | "Confirmed": 22318, 11 | "Deaths": 1230, 12 | "Recovered": 173, 13 | "Active": 20915, 14 | "Date": "2020-04-13T00:00:00Z" 15 | }, 16 | { 17 | "Country": "Brazil", 18 | "CountryCode": "BR", 19 | "Province": "", 20 | "City": "", 21 | "CityCode": "", 22 | "Lat": "-14.24", 23 | "Lon": "-51.93", 24 | "Confirmed": 23430, 25 | "Deaths": 1328, 26 | "Recovered": 173, 27 | "Active": 21929, 28 | "Date": "2020-04-14T00:00:00Z" 29 | }, 30 | { 31 | "Country": "Brazil", 32 | "CountryCode": "BR", 33 | "Province": "", 34 | "City": "", 35 | "CityCode": "", 36 | "Lat": "-14.24", 37 | "Lon": "-51.93", 38 | "Confirmed": 25262, 39 | "Deaths": 1532, 40 | "Recovered": 14026, 41 | "Active": 9704, 42 | "Date": "2020-04-15T00:00:00Z" 43 | }, 44 | { 45 | "Country": "Brazil", 46 | "CountryCode": "BR", 47 | "Province": "", 48 | "City": "", 49 | "CityCode": "", 50 | "Lat": "-14.24", 51 | "Lon": "-51.93", 52 | "Confirmed": 28912, 53 | "Deaths": 1760, 54 | "Recovered": 14026, 55 | "Active": 13126, 56 | "Date": "2020-04-16T00:00:00Z" 57 | }, 58 | { 59 | "Country": "Brazil", 60 | "CountryCode": "BR", 61 | "Province": "", 62 | "City": "", 63 | "CityCode": "", 64 | "Lat": "-14.24", 65 | "Lon": "-51.93", 66 | "Confirmed": 30683, 67 | "Deaths": 1947, 68 | "Recovered": 14026, 69 | "Active": 14710, 70 | "Date": "2020-04-17T00:00:00Z" 71 | }, 72 | { 73 | "Country": "Brazil", 74 | "CountryCode": "BR", 75 | "Province": "", 76 | "City": "", 77 | "CityCode": "", 78 | "Lat": "-14.24", 79 | "Lon": "-51.93", 80 | "Confirmed": 34221, 81 | "Deaths": 2171, 82 | "Recovered": 14026, 83 | "Active": 18024, 84 | "Date": "2020-04-18T00:00:00Z" 85 | }, 86 | { 87 | "Country": "Brazil", 88 | "CountryCode": "BR", 89 | "Province": "", 90 | "City": "", 91 | "CityCode": "", 92 | "Lat": "-14.24", 93 | "Lon": "-51.93", 94 | "Confirmed": 36760, 95 | "Deaths": 2368, 96 | "Recovered": 14026, 97 | "Active": 20366, 98 | "Date": "2020-04-19T00:00:00Z" 99 | }, 100 | { 101 | "Country": "Brazil", 102 | "CountryCode": "BR", 103 | "Province": "", 104 | "City": "", 105 | "CityCode": "", 106 | "Lat": "-14.24", 107 | "Lon": "-51.93", 108 | "Confirmed": 38654, 109 | "Deaths": 2462, 110 | "Recovered": 22130, 111 | "Active": 14062, 112 | "Date": "2020-04-20T00:00:00Z" 113 | }, 114 | { 115 | "Country": "Brazil", 116 | "CountryCode": "BR", 117 | "Province": "", 118 | "City": "", 119 | "CityCode": "", 120 | "Lat": "-14.24", 121 | "Lon": "-51.93", 122 | "Confirmed": 40743, 123 | "Deaths": 2587, 124 | "Recovered": 22130, 125 | "Active": 16026, 126 | "Date": "2020-04-21T00:00:00Z" 127 | }, 128 | { 129 | "Country": "Brazil", 130 | "CountryCode": "BR", 131 | "Province": "", 132 | "City": "", 133 | "CityCode": "", 134 | "Lat": "-14.24", 135 | "Lon": "-51.93", 136 | "Confirmed": 43079, 137 | "Deaths": 2741, 138 | "Recovered": 24325, 139 | "Active": 16013, 140 | "Date": "2020-04-22T00:00:00Z" 141 | }, 142 | { 143 | "Country": "Brazil", 144 | "CountryCode": "BR", 145 | "Province": "", 146 | "City": "", 147 | "CityCode": "", 148 | "Lat": "-14.24", 149 | "Lon": "-51.93", 150 | "Confirmed": 45757, 151 | "Deaths": 2906, 152 | "Recovered": 25318, 153 | "Active": 17533, 154 | "Date": "2020-04-23T00:00:00Z" 155 | }, 156 | { 157 | "Country": "Brazil", 158 | "CountryCode": "BR", 159 | "Province": "", 160 | "City": "", 161 | "CityCode": "", 162 | "Lat": "-14.24", 163 | "Lon": "-51.93", 164 | "Confirmed": 49492, 165 | "Deaths": 3313, 166 | "Recovered": 26573, 167 | "Active": 19606, 168 | "Date": "2020-04-24T00:00:00Z" 169 | }, 170 | { 171 | "Country": "Brazil", 172 | "CountryCode": "BR", 173 | "Province": "", 174 | "City": "", 175 | "CityCode": "", 176 | "Lat": "-14.24", 177 | "Lon": "-51.93", 178 | "Confirmed": 54043, 179 | "Deaths": 3704, 180 | "Recovered": 27655, 181 | "Active": 22684, 182 | "Date": "2020-04-25T00:00:00Z" 183 | }, 184 | { 185 | "Country": "Brazil", 186 | "CountryCode": "BR", 187 | "Province": "", 188 | "City": "", 189 | "CityCode": "", 190 | "Lat": "-14.24", 191 | "Lon": "-51.93", 192 | "Confirmed": 59196, 193 | "Deaths": 4045, 194 | "Recovered": 29160, 195 | "Active": 25991, 196 | "Date": "2020-04-26T00:00:00Z" 197 | }, 198 | { 199 | "Country": "Brazil", 200 | "CountryCode": "BR", 201 | "Province": "", 202 | "City": "", 203 | "CityCode": "", 204 | "Lat": "-14.24", 205 | "Lon": "-51.93", 206 | "Confirmed": 62859, 207 | "Deaths": 4271, 208 | "Recovered": 30152, 209 | "Active": 28436, 210 | "Date": "2020-04-27T00:00:00Z" 211 | }, 212 | { 213 | "Country": "Brazil", 214 | "CountryCode": "BR", 215 | "Province": "", 216 | "City": "", 217 | "CityCode": "", 218 | "Lat": "-14.24", 219 | "Lon": "-51.93", 220 | "Confirmed": 66501, 221 | "Deaths": 4543, 222 | "Recovered": 31142, 223 | "Active": 30816, 224 | "Date": "2020-04-28T00:00:00Z" 225 | }, 226 | { 227 | "Country": "Brazil", 228 | "CountryCode": "BR", 229 | "Province": "", 230 | "City": "", 231 | "CityCode": "", 232 | "Lat": "-14.24", 233 | "Lon": "-51.93", 234 | "Confirmed": 101826, 235 | "Deaths": 7051, 236 | "Recovered": 42991, 237 | "Active": 51784, 238 | "Date": "2020-05-04T00:00:00Z" 239 | }, 240 | { 241 | "Country": "Brazil", 242 | "CountryCode": "BR", 243 | "Province": "", 244 | "City": "", 245 | "CityCode": "", 246 | "Lat": "-14.24", 247 | "Lon": "-51.93", 248 | "Confirmed": 107844, 249 | "Deaths": 7328, 250 | "Recovered": 45815, 251 | "Active": 54701, 252 | "Date": "2020-05-05T00:00:00Z" 253 | }, 254 | { 255 | "Country": "Brazil", 256 | "CountryCode": "BR", 257 | "Province": "", 258 | "City": "", 259 | "CityCode": "", 260 | "Lat": "-14.24", 261 | "Lon": "-51.93", 262 | "Confirmed": 115455, 263 | "Deaths": 7938, 264 | "Recovered": 48221, 265 | "Active": 59296, 266 | "Date": "2020-05-06T00:00:00Z" 267 | } 268 | ] -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 as base 2 | RUN apt-get update 3 | RUN apt-get install -y wget 4 | RUN wget https://packages.microsoft.com/config/ubuntu/21.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb 5 | RUN dpkg -i packages-microsoft-prod.deb 6 | RUN rm packages-microsoft-prod.deb 7 | RUN apt-get update 8 | RUN apt-get install -y apt-transport-https 9 | RUN apt-get install -y dotnet-sdk-3.1 10 | RUN apt-get install -y libgdiplus 11 | WORKDIR /app 12 | 13 | FROM base as restore 14 | WORKDIR /src 15 | COPY ["DataBot/DataBot.fsproj", "DataBot/"] 16 | RUN dotnet restore "DataBot/DataBot.fsproj" 17 | 18 | FROM restore as build 19 | WORKDIR /src 20 | COPY . . 21 | RUN dotnet build "DataBot/DataBot.fsproj" -c Release -o /app/build 22 | 23 | FROM build as publish 24 | RUN dotnet publish "DataBot/DataBot.fsproj" -c Release -o /app/publish 25 | 26 | FROM base AS final 27 | WORKDIR /app 28 | COPY --from=publish /app/publish . 29 | 30 | ARG TWITTER_CONSUMER_SECRET 31 | ENV TWITTER_CONSUMER_SECRET=$TWITTER_CONSUMER_SECRET 32 | 33 | ARG TWITTER_CONSUMER_KEY 34 | ENV TWITTER_CONSUMER_KEY=$TWITTER_CONSUMER_KEY 35 | 36 | ARG TWITTER_TOKEN_SECRET 37 | ENV TWITTER_TOKEN_SECRET=$TWITTER_TOKEN_SECRET 38 | 39 | ARG TWITTER_TOKEN 40 | ENV TWITTER_TOKEN=$TWITTER_TOKEN 41 | 42 | ARG USE_SQLITE 43 | ENV USE_SQLITE=$USE_SQLITE 44 | 45 | ARG COSMOS_DB_ACCOUNT_ENDPOINT 46 | ENV COSMOS_DB_ACCOUNT_ENDPOINT=$COSMOS_DB_ACCOUNT_ENDPOINT 47 | 48 | ARG COSMOS_DB_ACCOUNT_KEY 49 | ENV COSMOS_DB_ACCOUNT_KEY=$COSMOS_DB_ACCOUNT_KEY 50 | 51 | ARG COSMOS_DB_DATABASE_NAME 52 | ENV COSMOS_DB_DATABASE_NAME=$COSMOS_DB_DATABASE_NAME 53 | 54 | ENTRYPOINT ["dotnet", "DataBot.dll"] 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FSharpDataBot 2 | Twitter robot that outputs Graphs and Statistics from the World Bank Data 3 | 4 | ![Travis](https://travis-ci.com/vhogemann/FSharpDataBot.svg?branch=master) 5 | ![FsharpDataBot Log](doc/img/FSharpDataBotBanner.jpg) 6 | 7 | ## Usage 8 | 9 | This bot is listening for any mention to the [@fsharp_data_bot](https://twitter.com/fsharp_data_bot) 10 | on Twitter. To get a response from it, just send it a message in the format 11 | 12 | > @fsharp_data_bot COUNTRIES INDICATORS START_YEAR END_YEAR 13 | 14 | PARAM | Description 15 | ----- | ------ 16 | COUNTRIES | Any list of country names (no spaces) or their two or three ISO codes. Ex: BR, USA, Italy 17 | INDICATORS | Any of the indicators listed bellow 18 | START_YEAR | Four digit year. Ex.: 1997, 2008 19 | END_YEAR | Four digit year. Ex.: 1997, 2008 20 | 21 | Examples: 22 | 23 | To plot the GDP from 2010 to 2020 for Brazil, Italy and the United States do: 24 | 25 | > @fsharp_data_bot brazil italy usa gdp 2010 2020 26 | 27 | ![Example Reply](doc/img/Screenshot%202020-05-27%20at%2016.32.32.png) 28 | 29 | ### World Bank Indicators 30 | 31 | These indicators fetch data from the [World Bank Open Data](https://data.worldbank.org/) 32 | API: 33 | 34 | Indicator | Description 35 | --------- | ----------- 36 | gdp | Gross Domestic Product in USD 37 | gdp/capita | Gross Domestic Product per Capita in USD 38 | gdp/growth | Gross Domestic Product annual growth % 39 | literacy | Literacy rate, youth total (% of people ages 15-24) 40 | literacy/young | Same as above 41 | literacy/adult | Literacy rate, adult total (% of people ages 15 and above) 42 | employment | Employment to population ratio, 15+, total (%) (national estimate) 43 | unemployment | Unemployment, total (% of total labor force) (modeled ILO estimate) 44 | electricity-access | Access to electricity (% of population) 45 | exchange-rate/usd | Official exchange rate (LCU per US$, period average) 46 | poverty-gap | Poverty headcount ratio at national poverty lines (% of population) 47 | stock-market | Stocks traded, total value (current US$) 48 | external-balance | External balance on goods and services (current US$) 49 | 50 | ### Covid19Api Indicators 51 | 52 | Indicators using the [Covid19 Api](https://covid19api.com/): 53 | 54 | Indicator | Description 55 | --------- | ----------- 56 | covid | Coronavirus confirmed cases, recoveries and deaths since day 1 -------------------------------------------------------------------------------- /Tests/DataTests.fs: -------------------------------------------------------------------------------- 1 | module DataTests 2 | 3 | open Data 4 | open Xunit 5 | open Swensen.Unquote 6 | 7 | [] 8 | let ``Country Matcher`` () = 9 | 10 | test 11 | <@ 12 | match "brazil" with 13 | | CountryMatcher country -> 14 | country.TwoLetterCode = "br" 15 | | _ -> false 16 | @> 17 | 18 | test 19 | <@ 20 | match "brasil" with 21 | | CountryMatcher country -> 22 | country.TwoLetterCode = "br" 23 | | _ -> false 24 | @> 25 | 26 | // Lots of people use UK instead of the correct GB code 27 | test 28 | <@ 29 | match "uk" with 30 | | CountryMatcher country -> 31 | country.TwoLetterCode = "gb" 32 | | _ -> false 33 | @> 34 | [] 35 | let ``Year Matcher`` () = 36 | test 37 | <@ 38 | match "2020" with 39 | | YearMatcher year -> 40 | year = 2020 41 | | _ -> false 42 | @> 43 | 44 | test 45 | <@ 46 | match "1920" with 47 | | YearMatcher year -> 48 | year = 1920 49 | | _ -> false 50 | @> 51 | 52 | [] 53 | let ``Indicator Matcher`` () = 54 | test 55 | <@ 56 | match "gdp" with 57 | | IndicatorMatcher indicator -> 58 | true 59 | | _ -> false 60 | @> 61 | -------------------------------------------------------------------------------- /Tests/GraphTests.fs: -------------------------------------------------------------------------------- 1 | module GraphTests 2 | 3 | open Xunit 4 | open Graph 5 | open System.IO 6 | open CovidProvider 7 | 8 | [] 9 | let ``Covid - NewDeaths`` () = 10 | let json = File.ReadAllText "covidsample.json" 11 | let parsed = Covid.ParseList(json) 12 | let country = CovidIndicator("br", "Brazil", parsed) 13 | using 14 | (Line "test" (seq { yield country.Name, country.Deaths }) None None true false false) 15 | (fun line -> using (new FileStream("test.png", FileMode.Create, FileAccess.Write)) line.WriteTo) 16 | -------------------------------------------------------------------------------- /Tests/Program.fs: -------------------------------------------------------------------------------- 1 | module Program = let [] main _ = 0 2 | -------------------------------------------------------------------------------- /Tests/Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Tests/covidsample.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Country": "Brazil", 4 | "CountryCode": "BR", 5 | "Province": "", 6 | "City": "", 7 | "CityCode": "", 8 | "Lat": "-14.24", 9 | "Lon": "-51.93", 10 | "Confirmed": 1, 11 | "Deaths": 0, 12 | "Recovered": 0, 13 | "Active": 1, 14 | "Date": "2020-02-26T00:00:00Z" 15 | }, 16 | { 17 | "Country": "Brazil", 18 | "CountryCode": "BR", 19 | "Province": "", 20 | "City": "", 21 | "CityCode": "", 22 | "Lat": "-14.24", 23 | "Lon": "-51.93", 24 | "Confirmed": 1, 25 | "Deaths": 0, 26 | "Recovered": 0, 27 | "Active": 1, 28 | "Date": "2020-02-27T00:00:00Z" 29 | }, 30 | { 31 | "Country": "Brazil", 32 | "CountryCode": "BR", 33 | "Province": "", 34 | "City": "", 35 | "CityCode": "", 36 | "Lat": "-14.24", 37 | "Lon": "-51.93", 38 | "Confirmed": 1, 39 | "Deaths": 0, 40 | "Recovered": 0, 41 | "Active": 1, 42 | "Date": "2020-02-28T00:00:00Z" 43 | }, 44 | { 45 | "Country": "Brazil", 46 | "CountryCode": "BR", 47 | "Province": "", 48 | "City": "", 49 | "CityCode": "", 50 | "Lat": "-14.24", 51 | "Lon": "-51.93", 52 | "Confirmed": 2, 53 | "Deaths": 0, 54 | "Recovered": 0, 55 | "Active": 2, 56 | "Date": "2020-02-29T00:00:00Z" 57 | }, 58 | { 59 | "Country": "Brazil", 60 | "CountryCode": "BR", 61 | "Province": "", 62 | "City": "", 63 | "CityCode": "", 64 | "Lat": "-14.24", 65 | "Lon": "-51.93", 66 | "Confirmed": 2, 67 | "Deaths": 0, 68 | "Recovered": 0, 69 | "Active": 2, 70 | "Date": "2020-03-01T00:00:00Z" 71 | }, 72 | { 73 | "Country": "Brazil", 74 | "CountryCode": "BR", 75 | "Province": "", 76 | "City": "", 77 | "CityCode": "", 78 | "Lat": "-14.24", 79 | "Lon": "-51.93", 80 | "Confirmed": 2, 81 | "Deaths": 0, 82 | "Recovered": 0, 83 | "Active": 2, 84 | "Date": "2020-03-02T00:00:00Z" 85 | }, 86 | { 87 | "Country": "Brazil", 88 | "CountryCode": "BR", 89 | "Province": "", 90 | "City": "", 91 | "CityCode": "", 92 | "Lat": "-14.24", 93 | "Lon": "-51.93", 94 | "Confirmed": 2, 95 | "Deaths": 0, 96 | "Recovered": 0, 97 | "Active": 2, 98 | "Date": "2020-03-03T00:00:00Z" 99 | }, 100 | { 101 | "Country": "Brazil", 102 | "CountryCode": "BR", 103 | "Province": "", 104 | "City": "", 105 | "CityCode": "", 106 | "Lat": "-14.24", 107 | "Lon": "-51.93", 108 | "Confirmed": 4, 109 | "Deaths": 0, 110 | "Recovered": 0, 111 | "Active": 4, 112 | "Date": "2020-03-04T00:00:00Z" 113 | }, 114 | { 115 | "Country": "Brazil", 116 | "CountryCode": "BR", 117 | "Province": "", 118 | "City": "", 119 | "CityCode": "", 120 | "Lat": "-14.24", 121 | "Lon": "-51.93", 122 | "Confirmed": 4, 123 | "Deaths": 0, 124 | "Recovered": 0, 125 | "Active": 4, 126 | "Date": "2020-03-05T00:00:00Z" 127 | }, 128 | { 129 | "Country": "Brazil", 130 | "CountryCode": "BR", 131 | "Province": "", 132 | "City": "", 133 | "CityCode": "", 134 | "Lat": "-14.24", 135 | "Lon": "-51.93", 136 | "Confirmed": 13, 137 | "Deaths": 0, 138 | "Recovered": 0, 139 | "Active": 13, 140 | "Date": "2020-03-06T00:00:00Z" 141 | }, 142 | { 143 | "Country": "Brazil", 144 | "CountryCode": "BR", 145 | "Province": "", 146 | "City": "", 147 | "CityCode": "", 148 | "Lat": "-14.24", 149 | "Lon": "-51.93", 150 | "Confirmed": 13, 151 | "Deaths": 0, 152 | "Recovered": 0, 153 | "Active": 13, 154 | "Date": "2020-03-07T00:00:00Z" 155 | }, 156 | { 157 | "Country": "Brazil", 158 | "CountryCode": "BR", 159 | "Province": "", 160 | "City": "", 161 | "CityCode": "", 162 | "Lat": "-14.24", 163 | "Lon": "-51.93", 164 | "Confirmed": 20, 165 | "Deaths": 0, 166 | "Recovered": 0, 167 | "Active": 20, 168 | "Date": "2020-03-08T00:00:00Z" 169 | }, 170 | { 171 | "Country": "Brazil", 172 | "CountryCode": "BR", 173 | "Province": "", 174 | "City": "", 175 | "CityCode": "", 176 | "Lat": "-14.24", 177 | "Lon": "-51.93", 178 | "Confirmed": 25, 179 | "Deaths": 0, 180 | "Recovered": 0, 181 | "Active": 25, 182 | "Date": "2020-03-09T00:00:00Z" 183 | }, 184 | { 185 | "Country": "Brazil", 186 | "CountryCode": "BR", 187 | "Province": "", 188 | "City": "", 189 | "CityCode": "", 190 | "Lat": "-14.24", 191 | "Lon": "-51.93", 192 | "Confirmed": 31, 193 | "Deaths": 0, 194 | "Recovered": 0, 195 | "Active": 31, 196 | "Date": "2020-03-10T00:00:00Z" 197 | }, 198 | { 199 | "Country": "Brazil", 200 | "CountryCode": "BR", 201 | "Province": "", 202 | "City": "", 203 | "CityCode": "", 204 | "Lat": "-14.24", 205 | "Lon": "-51.93", 206 | "Confirmed": 38, 207 | "Deaths": 0, 208 | "Recovered": 0, 209 | "Active": 38, 210 | "Date": "2020-03-11T00:00:00Z" 211 | }, 212 | { 213 | "Country": "Brazil", 214 | "CountryCode": "BR", 215 | "Province": "", 216 | "City": "", 217 | "CityCode": "", 218 | "Lat": "-14.24", 219 | "Lon": "-51.93", 220 | "Confirmed": 52, 221 | "Deaths": 0, 222 | "Recovered": 0, 223 | "Active": 52, 224 | "Date": "2020-03-12T00:00:00Z" 225 | }, 226 | { 227 | "Country": "Brazil", 228 | "CountryCode": "BR", 229 | "Province": "", 230 | "City": "", 231 | "CityCode": "", 232 | "Lat": "-14.24", 233 | "Lon": "-51.93", 234 | "Confirmed": 151, 235 | "Deaths": 0, 236 | "Recovered": 0, 237 | "Active": 151, 238 | "Date": "2020-03-13T00:00:00Z" 239 | }, 240 | { 241 | "Country": "Brazil", 242 | "CountryCode": "BR", 243 | "Province": "", 244 | "City": "", 245 | "CityCode": "", 246 | "Lat": "-14.24", 247 | "Lon": "-51.93", 248 | "Confirmed": 151, 249 | "Deaths": 0, 250 | "Recovered": 0, 251 | "Active": 151, 252 | "Date": "2020-03-14T00:00:00Z" 253 | }, 254 | { 255 | "Country": "Brazil", 256 | "CountryCode": "BR", 257 | "Province": "", 258 | "City": "", 259 | "CityCode": "", 260 | "Lat": "-14.24", 261 | "Lon": "-51.93", 262 | "Confirmed": 162, 263 | "Deaths": 0, 264 | "Recovered": 0, 265 | "Active": 162, 266 | "Date": "2020-03-15T00:00:00Z" 267 | }, 268 | { 269 | "Country": "Brazil", 270 | "CountryCode": "BR", 271 | "Province": "", 272 | "City": "", 273 | "CityCode": "", 274 | "Lat": "-14.24", 275 | "Lon": "-51.93", 276 | "Confirmed": 200, 277 | "Deaths": 0, 278 | "Recovered": 1, 279 | "Active": 199, 280 | "Date": "2020-03-16T00:00:00Z" 281 | }, 282 | { 283 | "Country": "Brazil", 284 | "CountryCode": "BR", 285 | "Province": "", 286 | "City": "", 287 | "CityCode": "", 288 | "Lat": "-14.24", 289 | "Lon": "-51.93", 290 | "Confirmed": 321, 291 | "Deaths": 1, 292 | "Recovered": 2, 293 | "Active": 318, 294 | "Date": "2020-03-17T00:00:00Z" 295 | }, 296 | { 297 | "Country": "Brazil", 298 | "CountryCode": "BR", 299 | "Province": "", 300 | "City": "", 301 | "CityCode": "", 302 | "Lat": "-14.24", 303 | "Lon": "-51.93", 304 | "Confirmed": 372, 305 | "Deaths": 3, 306 | "Recovered": 2, 307 | "Active": 367, 308 | "Date": "2020-03-18T00:00:00Z" 309 | }, 310 | { 311 | "Country": "Brazil", 312 | "CountryCode": "BR", 313 | "Province": "", 314 | "City": "", 315 | "CityCode": "", 316 | "Lat": "-14.24", 317 | "Lon": "-51.93", 318 | "Confirmed": 621, 319 | "Deaths": 6, 320 | "Recovered": 2, 321 | "Active": 613, 322 | "Date": "2020-03-19T00:00:00Z" 323 | }, 324 | { 325 | "Country": "Brazil", 326 | "CountryCode": "BR", 327 | "Province": "", 328 | "City": "", 329 | "CityCode": "", 330 | "Lat": "-14.24", 331 | "Lon": "-51.93", 332 | "Confirmed": 793, 333 | "Deaths": 11, 334 | "Recovered": 2, 335 | "Active": 780, 336 | "Date": "2020-03-20T00:00:00Z" 337 | }, 338 | { 339 | "Country": "Brazil", 340 | "CountryCode": "BR", 341 | "Province": "", 342 | "City": "", 343 | "CityCode": "", 344 | "Lat": "-14.24", 345 | "Lon": "-51.93", 346 | "Confirmed": 1021, 347 | "Deaths": 15, 348 | "Recovered": 2, 349 | "Active": 1004, 350 | "Date": "2020-03-21T00:00:00Z" 351 | }, 352 | { 353 | "Country": "Brazil", 354 | "CountryCode": "BR", 355 | "Province": "", 356 | "City": "", 357 | "CityCode": "", 358 | "Lat": "-14.24", 359 | "Lon": "-51.93", 360 | "Confirmed": 1546, 361 | "Deaths": 25, 362 | "Recovered": 2, 363 | "Active": 1519, 364 | "Date": "2020-03-22T00:00:00Z" 365 | }, 366 | { 367 | "Country": "Brazil", 368 | "CountryCode": "BR", 369 | "Province": "", 370 | "City": "", 371 | "CityCode": "", 372 | "Lat": "-14.24", 373 | "Lon": "-51.93", 374 | "Confirmed": 1924, 375 | "Deaths": 34, 376 | "Recovered": 2, 377 | "Active": 1888, 378 | "Date": "2020-03-23T00:00:00Z" 379 | }, 380 | { 381 | "Country": "Brazil", 382 | "CountryCode": "BR", 383 | "Province": "", 384 | "City": "", 385 | "CityCode": "", 386 | "Lat": "-14.24", 387 | "Lon": "-51.93", 388 | "Confirmed": 2247, 389 | "Deaths": 46, 390 | "Recovered": 2, 391 | "Active": 2199, 392 | "Date": "2020-03-24T00:00:00Z" 393 | }, 394 | { 395 | "Country": "Brazil", 396 | "CountryCode": "BR", 397 | "Province": "", 398 | "City": "", 399 | "CityCode": "", 400 | "Lat": "-14.24", 401 | "Lon": "-51.93", 402 | "Confirmed": 2554, 403 | "Deaths": 59, 404 | "Recovered": 2, 405 | "Active": 2493, 406 | "Date": "2020-03-25T00:00:00Z" 407 | }, 408 | { 409 | "Country": "Brazil", 410 | "CountryCode": "BR", 411 | "Province": "", 412 | "City": "", 413 | "CityCode": "", 414 | "Lat": "-14.24", 415 | "Lon": "-51.93", 416 | "Confirmed": 2985, 417 | "Deaths": 77, 418 | "Recovered": 6, 419 | "Active": 2902, 420 | "Date": "2020-03-26T00:00:00Z" 421 | }, 422 | { 423 | "Country": "Brazil", 424 | "CountryCode": "BR", 425 | "Province": "", 426 | "City": "", 427 | "CityCode": "", 428 | "Lat": "-14.24", 429 | "Lon": "-51.93", 430 | "Confirmed": 3417, 431 | "Deaths": 92, 432 | "Recovered": 6, 433 | "Active": 3319, 434 | "Date": "2020-03-27T00:00:00Z" 435 | }, 436 | { 437 | "Country": "Brazil", 438 | "CountryCode": "BR", 439 | "Province": "", 440 | "City": "", 441 | "CityCode": "", 442 | "Lat": "-14.24", 443 | "Lon": "-51.93", 444 | "Confirmed": 3904, 445 | "Deaths": 111, 446 | "Recovered": 6, 447 | "Active": 3787, 448 | "Date": "2020-03-28T00:00:00Z" 449 | }, 450 | { 451 | "Country": "Brazil", 452 | "CountryCode": "BR", 453 | "Province": "", 454 | "City": "", 455 | "CityCode": "", 456 | "Lat": "-14.24", 457 | "Lon": "-51.93", 458 | "Confirmed": 4256, 459 | "Deaths": 136, 460 | "Recovered": 6, 461 | "Active": 4114, 462 | "Date": "2020-03-29T00:00:00Z" 463 | }, 464 | { 465 | "Country": "Brazil", 466 | "CountryCode": "BR", 467 | "Province": "", 468 | "City": "", 469 | "CityCode": "", 470 | "Lat": "-14.24", 471 | "Lon": "-51.93", 472 | "Confirmed": 4579, 473 | "Deaths": 159, 474 | "Recovered": 120, 475 | "Active": 4300, 476 | "Date": "2020-03-30T00:00:00Z" 477 | }, 478 | { 479 | "Country": "Brazil", 480 | "CountryCode": "BR", 481 | "Province": "", 482 | "City": "", 483 | "CityCode": "", 484 | "Lat": "-14.24", 485 | "Lon": "-51.93", 486 | "Confirmed": 5717, 487 | "Deaths": 201, 488 | "Recovered": 127, 489 | "Active": 5389, 490 | "Date": "2020-03-31T00:00:00Z" 491 | }, 492 | { 493 | "Country": "Brazil", 494 | "CountryCode": "BR", 495 | "Province": "", 496 | "City": "", 497 | "CityCode": "", 498 | "Lat": "-14.24", 499 | "Lon": "-51.93", 500 | "Confirmed": 6836, 501 | "Deaths": 240, 502 | "Recovered": 127, 503 | "Active": 6469, 504 | "Date": "2020-04-01T00:00:00Z" 505 | }, 506 | { 507 | "Country": "Brazil", 508 | "CountryCode": "BR", 509 | "Province": "", 510 | "City": "", 511 | "CityCode": "", 512 | "Lat": "-14.24", 513 | "Lon": "-51.93", 514 | "Confirmed": 8044, 515 | "Deaths": 324, 516 | "Recovered": 127, 517 | "Active": 7593, 518 | "Date": "2020-04-02T00:00:00Z" 519 | }, 520 | { 521 | "Country": "Brazil", 522 | "CountryCode": "BR", 523 | "Province": "", 524 | "City": "", 525 | "CityCode": "", 526 | "Lat": "-14.24", 527 | "Lon": "-51.93", 528 | "Confirmed": 9056, 529 | "Deaths": 359, 530 | "Recovered": 127, 531 | "Active": 8570, 532 | "Date": "2020-04-03T00:00:00Z" 533 | }, 534 | { 535 | "Country": "Brazil", 536 | "CountryCode": "BR", 537 | "Province": "", 538 | "City": "", 539 | "CityCode": "", 540 | "Lat": "-14.24", 541 | "Lon": "-51.93", 542 | "Confirmed": 10360, 543 | "Deaths": 445, 544 | "Recovered": 127, 545 | "Active": 9788, 546 | "Date": "2020-04-04T00:00:00Z" 547 | }, 548 | { 549 | "Country": "Brazil", 550 | "CountryCode": "BR", 551 | "Province": "", 552 | "City": "", 553 | "CityCode": "", 554 | "Lat": "-14.24", 555 | "Lon": "-51.93", 556 | "Confirmed": 11130, 557 | "Deaths": 486, 558 | "Recovered": 127, 559 | "Active": 10517, 560 | "Date": "2020-04-05T00:00:00Z" 561 | }, 562 | { 563 | "Country": "Brazil", 564 | "CountryCode": "BR", 565 | "Province": "", 566 | "City": "", 567 | "CityCode": "", 568 | "Lat": "-14.24", 569 | "Lon": "-51.93", 570 | "Confirmed": 12161, 571 | "Deaths": 564, 572 | "Recovered": 127, 573 | "Active": 11470, 574 | "Date": "2020-04-06T00:00:00Z" 575 | }, 576 | { 577 | "Country": "Brazil", 578 | "CountryCode": "BR", 579 | "Province": "", 580 | "City": "", 581 | "CityCode": "", 582 | "Lat": "-14.24", 583 | "Lon": "-51.93", 584 | "Confirmed": 14034, 585 | "Deaths": 686, 586 | "Recovered": 127, 587 | "Active": 13221, 588 | "Date": "2020-04-07T00:00:00Z" 589 | }, 590 | { 591 | "Country": "Brazil", 592 | "CountryCode": "BR", 593 | "Province": "", 594 | "City": "", 595 | "CityCode": "", 596 | "Lat": "-14.24", 597 | "Lon": "-51.93", 598 | "Confirmed": 16170, 599 | "Deaths": 819, 600 | "Recovered": 127, 601 | "Active": 15224, 602 | "Date": "2020-04-08T00:00:00Z" 603 | }, 604 | { 605 | "Country": "Brazil", 606 | "CountryCode": "BR", 607 | "Province": "", 608 | "City": "", 609 | "CityCode": "", 610 | "Lat": "-14.24", 611 | "Lon": "-51.93", 612 | "Confirmed": 18092, 613 | "Deaths": 950, 614 | "Recovered": 173, 615 | "Active": 16969, 616 | "Date": "2020-04-09T00:00:00Z" 617 | }, 618 | { 619 | "Country": "Brazil", 620 | "CountryCode": "BR", 621 | "Province": "", 622 | "City": "", 623 | "CityCode": "", 624 | "Lat": "-14.24", 625 | "Lon": "-51.93", 626 | "Confirmed": 19638, 627 | "Deaths": 1057, 628 | "Recovered": 173, 629 | "Active": 18408, 630 | "Date": "2020-04-10T00:00:00Z" 631 | }, 632 | { 633 | "Country": "Brazil", 634 | "CountryCode": "BR", 635 | "Province": "", 636 | "City": "", 637 | "CityCode": "", 638 | "Lat": "-14.24", 639 | "Lon": "-51.93", 640 | "Confirmed": 20727, 641 | "Deaths": 1124, 642 | "Recovered": 173, 643 | "Active": 19430, 644 | "Date": "2020-04-11T00:00:00Z" 645 | }, 646 | { 647 | "Country": "Brazil", 648 | "CountryCode": "BR", 649 | "Province": "", 650 | "City": "", 651 | "CityCode": "", 652 | "Lat": "-14.24", 653 | "Lon": "-51.93", 654 | "Confirmed": 22192, 655 | "Deaths": 1223, 656 | "Recovered": 173, 657 | "Active": 20796, 658 | "Date": "2020-04-12T00:00:00Z" 659 | }, 660 | { 661 | "Country": "Brazil", 662 | "CountryCode": "BR", 663 | "Province": "", 664 | "City": "", 665 | "CityCode": "", 666 | "Lat": "-14.24", 667 | "Lon": "-51.93", 668 | "Confirmed": 23430, 669 | "Deaths": 1328, 670 | "Recovered": 173, 671 | "Active": 21929, 672 | "Date": "2020-04-13T00:00:00Z" 673 | }, 674 | { 675 | "Country": "Brazil", 676 | "CountryCode": "BR", 677 | "Province": "", 678 | "City": "", 679 | "CityCode": "", 680 | "Lat": "-14.24", 681 | "Lon": "-51.93", 682 | "Confirmed": 25262, 683 | "Deaths": 1532, 684 | "Recovered": 3046, 685 | "Active": 20684, 686 | "Date": "2020-04-14T00:00:00Z" 687 | }, 688 | { 689 | "Country": "Brazil", 690 | "CountryCode": "BR", 691 | "Province": "", 692 | "City": "", 693 | "CityCode": "", 694 | "Lat": "-14.24", 695 | "Lon": "-51.93", 696 | "Confirmed": 28320, 697 | "Deaths": 1736, 698 | "Recovered": 14026, 699 | "Active": 12558, 700 | "Date": "2020-04-15T00:00:00Z" 701 | }, 702 | { 703 | "Country": "Brazil", 704 | "CountryCode": "BR", 705 | "Province": "", 706 | "City": "", 707 | "CityCode": "", 708 | "Lat": "-14.24", 709 | "Lon": "-51.93", 710 | "Confirmed": 30425, 711 | "Deaths": 1924, 712 | "Recovered": 14026, 713 | "Active": 14475, 714 | "Date": "2020-04-16T00:00:00Z" 715 | }, 716 | { 717 | "Country": "Brazil", 718 | "CountryCode": "BR", 719 | "Province": "", 720 | "City": "", 721 | "CityCode": "", 722 | "Lat": "-14.24", 723 | "Lon": "-51.93", 724 | "Confirmed": 33682, 725 | "Deaths": 2141, 726 | "Recovered": 14026, 727 | "Active": 17515, 728 | "Date": "2020-04-17T00:00:00Z" 729 | }, 730 | { 731 | "Country": "Brazil", 732 | "CountryCode": "BR", 733 | "Province": "", 734 | "City": "", 735 | "CityCode": "", 736 | "Lat": "-14.24", 737 | "Lon": "-51.93", 738 | "Confirmed": 36658, 739 | "Deaths": 2354, 740 | "Recovered": 14026, 741 | "Active": 20278, 742 | "Date": "2020-04-18T00:00:00Z" 743 | }, 744 | { 745 | "Country": "Brazil", 746 | "CountryCode": "BR", 747 | "Province": "", 748 | "City": "", 749 | "CityCode": "", 750 | "Lat": "-14.24", 751 | "Lon": "-51.93", 752 | "Confirmed": 38654, 753 | "Deaths": 2462, 754 | "Recovered": 22130, 755 | "Active": 14062, 756 | "Date": "2020-04-19T00:00:00Z" 757 | }, 758 | { 759 | "Country": "Brazil", 760 | "CountryCode": "BR", 761 | "Province": "", 762 | "City": "", 763 | "CityCode": "", 764 | "Lat": "-14.24", 765 | "Lon": "-51.93", 766 | "Confirmed": 40743, 767 | "Deaths": 2587, 768 | "Recovered": 22130, 769 | "Active": 16026, 770 | "Date": "2020-04-20T00:00:00Z" 771 | }, 772 | { 773 | "Country": "Brazil", 774 | "CountryCode": "BR", 775 | "Province": "", 776 | "City": "", 777 | "CityCode": "", 778 | "Lat": "-14.24", 779 | "Lon": "-51.93", 780 | "Confirmed": 43079, 781 | "Deaths": 2741, 782 | "Recovered": 22991, 783 | "Active": 17347, 784 | "Date": "2020-04-21T00:00:00Z" 785 | }, 786 | { 787 | "Country": "Brazil", 788 | "CountryCode": "BR", 789 | "Province": "", 790 | "City": "", 791 | "CityCode": "", 792 | "Lat": "-14.24", 793 | "Lon": "-51.93", 794 | "Confirmed": 45757, 795 | "Deaths": 2906, 796 | "Recovered": 25318, 797 | "Active": 17533, 798 | "Date": "2020-04-22T00:00:00Z" 799 | }, 800 | { 801 | "Country": "Brazil", 802 | "CountryCode": "BR", 803 | "Province": "", 804 | "City": "", 805 | "CityCode": "", 806 | "Lat": "-14.24", 807 | "Lon": "-51.93", 808 | "Confirmed": 50036, 809 | "Deaths": 3331, 810 | "Recovered": 26573, 811 | "Active": 20132, 812 | "Date": "2020-04-23T00:00:00Z" 813 | }, 814 | { 815 | "Country": "Brazil", 816 | "CountryCode": "BR", 817 | "Province": "", 818 | "City": "", 819 | "CityCode": "", 820 | "Lat": "-14.24", 821 | "Lon": "-51.93", 822 | "Confirmed": 54043, 823 | "Deaths": 3704, 824 | "Recovered": 27655, 825 | "Active": 22684, 826 | "Date": "2020-04-24T00:00:00Z" 827 | }, 828 | { 829 | "Country": "Brazil", 830 | "CountryCode": "BR", 831 | "Province": "", 832 | "City": "", 833 | "CityCode": "", 834 | "Lat": "-14.24", 835 | "Lon": "-51.93", 836 | "Confirmed": 59324, 837 | "Deaths": 4057, 838 | "Recovered": 29160, 839 | "Active": 26107, 840 | "Date": "2020-04-25T00:00:00Z" 841 | }, 842 | { 843 | "Country": "Brazil", 844 | "CountryCode": "BR", 845 | "Province": "", 846 | "City": "", 847 | "CityCode": "", 848 | "Lat": "-14.24", 849 | "Lon": "-51.93", 850 | "Confirmed": 63100, 851 | "Deaths": 4286, 852 | "Recovered": 30152, 853 | "Active": 28662, 854 | "Date": "2020-04-26T00:00:00Z" 855 | }, 856 | { 857 | "Country": "Brazil", 858 | "CountryCode": "BR", 859 | "Province": "", 860 | "City": "", 861 | "CityCode": "", 862 | "Lat": "-14.24", 863 | "Lon": "-51.93", 864 | "Confirmed": 67446, 865 | "Deaths": 4603, 866 | "Recovered": 31142, 867 | "Active": 31701, 868 | "Date": "2020-04-27T00:00:00Z" 869 | }, 870 | { 871 | "Country": "Brazil", 872 | "CountryCode": "BR", 873 | "Province": "", 874 | "City": "", 875 | "CityCode": "", 876 | "Lat": "-14.24", 877 | "Lon": "-51.93", 878 | "Confirmed": 73235, 879 | "Deaths": 5083, 880 | "Recovered": 32544, 881 | "Active": 35608, 882 | "Date": "2020-04-28T00:00:00Z" 883 | }, 884 | { 885 | "Country": "Brazil", 886 | "CountryCode": "BR", 887 | "Province": "", 888 | "City": "", 889 | "CityCode": "", 890 | "Lat": "-14.24", 891 | "Lon": "-51.93", 892 | "Confirmed": 79685, 893 | "Deaths": 5513, 894 | "Recovered": 34132, 895 | "Active": 40040, 896 | "Date": "2020-04-29T00:00:00Z" 897 | }, 898 | { 899 | "Country": "Brazil", 900 | "CountryCode": "BR", 901 | "Province": "", 902 | "City": "", 903 | "CityCode": "", 904 | "Lat": "-14.24", 905 | "Lon": "-51.93", 906 | "Confirmed": 87187, 907 | "Deaths": 6006, 908 | "Recovered": 35935, 909 | "Active": 45246, 910 | "Date": "2020-04-30T00:00:00Z" 911 | }, 912 | { 913 | "Country": "Brazil", 914 | "CountryCode": "BR", 915 | "Province": "", 916 | "City": "", 917 | "CityCode": "", 918 | "Lat": "-14.24", 919 | "Lon": "-51.93", 920 | "Confirmed": 92202, 921 | "Deaths": 6412, 922 | "Recovered": 38039, 923 | "Active": 47751, 924 | "Date": "2020-05-01T00:00:00Z" 925 | }, 926 | { 927 | "Country": "Brazil", 928 | "CountryCode": "BR", 929 | "Province": "", 930 | "City": "", 931 | "CityCode": "", 932 | "Lat": "-14.24", 933 | "Lon": "-51.93", 934 | "Confirmed": 97100, 935 | "Deaths": 6761, 936 | "Recovered": 40937, 937 | "Active": 49402, 938 | "Date": "2020-05-02T00:00:00Z" 939 | }, 940 | { 941 | "Country": "Brazil", 942 | "CountryCode": "BR", 943 | "Province": "", 944 | "City": "", 945 | "CityCode": "", 946 | "Lat": "-14.24", 947 | "Lon": "-51.93", 948 | "Confirmed": 101826, 949 | "Deaths": 7051, 950 | "Recovered": 42991, 951 | "Active": 51784, 952 | "Date": "2020-05-03T00:00:00Z" 953 | }, 954 | { 955 | "Country": "Brazil", 956 | "CountryCode": "BR", 957 | "Province": "", 958 | "City": "", 959 | "CityCode": "", 960 | "Lat": "-14.24", 961 | "Lon": "-51.93", 962 | "Confirmed": 108620, 963 | "Deaths": 7367, 964 | "Recovered": 45815, 965 | "Active": 55438, 966 | "Date": "2020-05-04T00:00:00Z" 967 | }, 968 | { 969 | "Country": "Brazil", 970 | "CountryCode": "BR", 971 | "Province": "", 972 | "City": "", 973 | "CityCode": "", 974 | "Lat": "-14.24", 975 | "Lon": "-51.93", 976 | "Confirmed": 115455, 977 | "Deaths": 7938, 978 | "Recovered": 48221, 979 | "Active": 59296, 980 | "Date": "2020-05-05T00:00:00Z" 981 | }, 982 | { 983 | "Country": "Brazil", 984 | "CountryCode": "BR", 985 | "Province": "", 986 | "City": "", 987 | "CityCode": "", 988 | "Lat": "-14.24", 989 | "Lon": "-51.93", 990 | "Confirmed": 126611, 991 | "Deaths": 8588, 992 | "Recovered": 51370, 993 | "Active": 66653, 994 | "Date": "2020-05-06T00:00:00Z" 995 | }, 996 | { 997 | "Country": "Brazil", 998 | "CountryCode": "BR", 999 | "Province": "", 1000 | "City": "", 1001 | "CityCode": "", 1002 | "Lat": "-14.24", 1003 | "Lon": "-51.93", 1004 | "Confirmed": 135773, 1005 | "Deaths": 9190, 1006 | "Recovered": 55350, 1007 | "Active": 71233, 1008 | "Date": "2020-05-07T00:00:00Z" 1009 | }, 1010 | { 1011 | "Country": "Brazil", 1012 | "CountryCode": "BR", 1013 | "Province": "", 1014 | "City": "", 1015 | "CityCode": "", 1016 | "Lat": "-14.24", 1017 | "Lon": "-51.93", 1018 | "Confirmed": 146894, 1019 | "Deaths": 10017, 1020 | "Recovered": 59297, 1021 | "Active": 77580, 1022 | "Date": "2020-05-08T00:00:00Z" 1023 | }, 1024 | { 1025 | "Country": "Brazil", 1026 | "CountryCode": "BR", 1027 | "Province": "", 1028 | "City": "", 1029 | "CityCode": "", 1030 | "Lat": "-14.24", 1031 | "Lon": "-51.93", 1032 | "Confirmed": 156061, 1033 | "Deaths": 10656, 1034 | "Recovered": 61685, 1035 | "Active": 83720, 1036 | "Date": "2020-05-09T00:00:00Z" 1037 | }, 1038 | { 1039 | "Country": "Brazil", 1040 | "CountryCode": "BR", 1041 | "Province": "", 1042 | "City": "", 1043 | "CityCode": "", 1044 | "Lat": "-14.24", 1045 | "Lon": "-51.93", 1046 | "Confirmed": 162699, 1047 | "Deaths": 11123, 1048 | "Recovered": 64957, 1049 | "Active": 86619, 1050 | "Date": "2020-05-10T00:00:00Z" 1051 | }, 1052 | { 1053 | "Country": "Brazil", 1054 | "CountryCode": "BR", 1055 | "Province": "", 1056 | "City": "", 1057 | "CityCode": "", 1058 | "Lat": "-14.24", 1059 | "Lon": "-51.93", 1060 | "Confirmed": 169594, 1061 | "Deaths": 11653, 1062 | "Recovered": 67384, 1063 | "Active": 90557, 1064 | "Date": "2020-05-11T00:00:00Z" 1065 | }, 1066 | { 1067 | "Country": "Brazil", 1068 | "CountryCode": "BR", 1069 | "Province": "", 1070 | "City": "", 1071 | "CityCode": "", 1072 | "Lat": "-14.24", 1073 | "Lon": "-51.93", 1074 | "Confirmed": 178214, 1075 | "Deaths": 12461, 1076 | "Recovered": 72597, 1077 | "Active": 93156, 1078 | "Date": "2020-05-12T00:00:00Z" 1079 | }, 1080 | { 1081 | "Country": "Brazil", 1082 | "CountryCode": "BR", 1083 | "Province": "", 1084 | "City": "", 1085 | "CityCode": "", 1086 | "Lat": "-14.24", 1087 | "Lon": "-51.93", 1088 | "Confirmed": 190137, 1089 | "Deaths": 13240, 1090 | "Recovered": 78424, 1091 | "Active": 98473, 1092 | "Date": "2020-05-13T00:00:00Z" 1093 | }, 1094 | { 1095 | "Country": "Brazil", 1096 | "CountryCode": "BR", 1097 | "Province": "", 1098 | "City": "", 1099 | "CityCode": "", 1100 | "Lat": "-14.24", 1101 | "Lon": "-51.93", 1102 | "Confirmed": 203165, 1103 | "Deaths": 13999, 1104 | "Recovered": 79479, 1105 | "Active": 109687, 1106 | "Date": "2020-05-14T00:00:00Z" 1107 | }, 1108 | { 1109 | "Country": "Brazil", 1110 | "CountryCode": "BR", 1111 | "Province": "", 1112 | "City": "", 1113 | "CityCode": "", 1114 | "Lat": "-14.24", 1115 | "Lon": "-51.93", 1116 | "Confirmed": 220291, 1117 | "Deaths": 14962, 1118 | "Recovered": 84970, 1119 | "Active": 120359, 1120 | "Date": "2020-05-15T00:00:00Z" 1121 | }, 1122 | { 1123 | "Country": "Brazil", 1124 | "CountryCode": "BR", 1125 | "Province": "", 1126 | "City": "", 1127 | "CityCode": "", 1128 | "Lat": "-14.24", 1129 | "Lon": "-51.93", 1130 | "Confirmed": 233511, 1131 | "Deaths": 15662, 1132 | "Recovered": 89672, 1133 | "Active": 128177, 1134 | "Date": "2020-05-16T00:00:00Z" 1135 | }, 1136 | { 1137 | "Country": "Brazil", 1138 | "CountryCode": "BR", 1139 | "Province": "", 1140 | "City": "", 1141 | "CityCode": "", 1142 | "Lat": "-14.24", 1143 | "Lon": "-51.93", 1144 | "Confirmed": 241080, 1145 | "Deaths": 16118, 1146 | "Recovered": 94122, 1147 | "Active": 130840, 1148 | "Date": "2020-05-17T00:00:00Z" 1149 | }, 1150 | { 1151 | "Country": "Brazil", 1152 | "CountryCode": "BR", 1153 | "Province": "", 1154 | "City": "", 1155 | "CityCode": "", 1156 | "Lat": "-14.24", 1157 | "Lon": "-51.93", 1158 | "Confirmed": 255368, 1159 | "Deaths": 16853, 1160 | "Recovered": 100459, 1161 | "Active": 138056, 1162 | "Date": "2020-05-18T00:00:00Z" 1163 | }, 1164 | { 1165 | "Country": "Brazil", 1166 | "CountryCode": "BR", 1167 | "Province": "", 1168 | "City": "", 1169 | "CityCode": "", 1170 | "Lat": "-14.24", 1171 | "Lon": "-51.93", 1172 | "Confirmed": 271885, 1173 | "Deaths": 17983, 1174 | "Recovered": 106794, 1175 | "Active": 147108, 1176 | "Date": "2020-05-19T00:00:00Z" 1177 | }, 1178 | { 1179 | "Country": "Brazil", 1180 | "CountryCode": "BR", 1181 | "Province": "", 1182 | "City": "", 1183 | "CityCode": "", 1184 | "Lat": "-14.24", 1185 | "Lon": "-51.93", 1186 | "Confirmed": 291579, 1187 | "Deaths": 18859, 1188 | "Recovered": 116683, 1189 | "Active": 156037, 1190 | "Date": "2020-05-20T00:00:00Z" 1191 | }, 1192 | { 1193 | "Country": "Brazil", 1194 | "CountryCode": "BR", 1195 | "Province": "", 1196 | "City": "", 1197 | "CityCode": "", 1198 | "Lat": "-14.24", 1199 | "Lon": "-51.93", 1200 | "Confirmed": 310087, 1201 | "Deaths": 20047, 1202 | "Recovered": 125960, 1203 | "Active": 164080, 1204 | "Date": "2020-05-21T00:00:00Z" 1205 | }, 1206 | { 1207 | "Country": "Brazil", 1208 | "CountryCode": "BR", 1209 | "Province": "", 1210 | "City": "", 1211 | "CityCode": "", 1212 | "Lat": "-14.24", 1213 | "Lon": "-51.93", 1214 | "Confirmed": 330890, 1215 | "Deaths": 21048, 1216 | "Recovered": 135430, 1217 | "Active": 174412, 1218 | "Date": "2020-05-22T00:00:00Z" 1219 | }, 1220 | { 1221 | "Country": "Brazil", 1222 | "CountryCode": "BR", 1223 | "Province": "", 1224 | "City": "", 1225 | "CityCode": "", 1226 | "Lat": "-14.24", 1227 | "Lon": "-51.93", 1228 | "Confirmed": 347398, 1229 | "Deaths": 22013, 1230 | "Recovered": 142587, 1231 | "Active": 182798, 1232 | "Date": "2020-05-23T00:00:00Z" 1233 | }, 1234 | { 1235 | "Country": "Brazil", 1236 | "CountryCode": "BR", 1237 | "Province": "", 1238 | "City": "", 1239 | "CityCode": "", 1240 | "Lat": "-14.24", 1241 | "Lon": "-51.93", 1242 | "Confirmed": 363211, 1243 | "Deaths": 22666, 1244 | "Recovered": 149911, 1245 | "Active": 190634, 1246 | "Date": "2020-05-24T00:00:00Z" 1247 | }, 1248 | { 1249 | "Country": "Brazil", 1250 | "CountryCode": "BR", 1251 | "Province": "", 1252 | "City": "", 1253 | "CityCode": "", 1254 | "Lat": "-14.24", 1255 | "Lon": "-51.93", 1256 | "Confirmed": 374898, 1257 | "Deaths": 23473, 1258 | "Recovered": 153833, 1259 | "Active": 197592, 1260 | "Date": "2020-05-25T00:00:00Z" 1261 | }, 1262 | { 1263 | "Country": "Brazil", 1264 | "CountryCode": "BR", 1265 | "Province": "", 1266 | "City": "", 1267 | "CityCode": "", 1268 | "Lat": "-14.24", 1269 | "Lon": "-51.93", 1270 | "Confirmed": 391222, 1271 | "Deaths": 24512, 1272 | "Recovered": 158593, 1273 | "Active": 208117, 1274 | "Date": "2020-05-26T00:00:00Z" 1275 | }, 1276 | { 1277 | "Country": "Brazil", 1278 | "CountryCode": "BR", 1279 | "Province": "", 1280 | "City": "", 1281 | "CityCode": "", 1282 | "Lat": "-14.24", 1283 | "Lon": "-51.93", 1284 | "Confirmed": 411821, 1285 | "Deaths": 25598, 1286 | "Recovered": 166647, 1287 | "Active": 219576, 1288 | "Date": "2020-05-27T00:00:00Z" 1289 | }, 1290 | { 1291 | "Country": "Brazil", 1292 | "CountryCode": "BR", 1293 | "Province": "", 1294 | "City": "", 1295 | "CityCode": "", 1296 | "Lat": "-14.24", 1297 | "Lon": "-51.93", 1298 | "Confirmed": 438238, 1299 | "Deaths": 26754, 1300 | "Recovered": 177604, 1301 | "Active": 233880, 1302 | "Date": "2020-05-28T00:00:00Z" 1303 | } 1304 | ] 1305 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Docker 2 | # Build and push an image to Azure Container Registry 3 | # https://docs.microsoft.com/azure/devops/pipelines/languages/docker 4 | 5 | trigger: 6 | - master 7 | 8 | resources: 9 | - repo: self 10 | 11 | variables: 12 | # Container registry service connection established during pipeline creation 13 | dockerRegistryServiceConnection: 'c453c7d2-8704-4ad9-9e15-8b7035385b11' 14 | imageRepository: 'fsharpdatabot' 15 | containerRegistry: 'twitterbots.azurecr.io' 16 | dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile' 17 | tag: '$(Build.BuildId)' 18 | 19 | # Agent VM image name 20 | vmImageName: 'ubuntu-latest' 21 | 22 | stages: 23 | - stage: Build 24 | displayName: Build and push stage 25 | jobs: 26 | - job: Build 27 | displayName: Build 28 | pool: 29 | vmImage: $(vmImageName) 30 | steps: 31 | - task: Docker@2 32 | displayName: Build and push an image to container registry 33 | inputs: 34 | command: buildAndPush 35 | repository: $(imageRepository) 36 | dockerfile: $(dockerfilePath) 37 | containerRegistry: $(dockerRegistryServiceConnection) 38 | tags: | 39 | $(tag) 40 | -------------------------------------------------------------------------------- /doc/img/FSharpDataBotBanner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhogemann/FSharpDataBot/b87d3a80bf97dff5451c30d2fbf3372eddbc8ed2/doc/img/FSharpDataBotBanner.jpg -------------------------------------------------------------------------------- /doc/img/Screenshot 2020-05-27 at 16.32.32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vhogemann/FSharpDataBot/b87d3a80bf97dff5451c30d2fbf3372eddbc8ed2/doc/img/Screenshot 2020-05-27 at 16.32.32.png --------------------------------------------------------------------------------