├── .gitignore ├── icon.png ├── requirements.txt ├── notification-updates.json ├── flags ├── alabama.svg ├── colorado.svg ├── new-mexico.svg ├── texas.svg ├── district-of-columbia.svg ├── arizona.svg ├── hawaii.svg ├── maryland.svg ├── tennessee.svg ├── alaska.svg ├── ohio.svg ├── indiana.svg ├── rhode-island.svg ├── arkansas.svg ├── north-carolina.svg ├── south-carolina.svg ├── mississippi.svg └── georgia.svg ├── .github ├── screenshot │ ├── package.json │ ├── index.js │ └── package-lock.json ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md ├── workflows │ ├── scrape.yml │ └── pr.yml └── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── battleground-state-changes.xml ├── all-state-changes.xml ├── battleground-state-changes.html.tmpl └── print-battleground-state-changes /.gitignore: -------------------------------------------------------------------------------- 1 | _cache/ 2 | node_modules/ 3 | screenshot-*.png 4 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex/nyt-2020-election-scraper/HEAD/icon.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tabulate==0.8.7 2 | pybind11==2.6.0 3 | pysimdjson==3.1.0 4 | GitPython==3.1.11 5 | -------------------------------------------------------------------------------- /notification-updates.json: -------------------------------------------------------------------------------- 1 | {"_comment": "this file is deprecated and should not be used", "results_hash": "b969c2a7b0d8dc2f85417cc8c97dd17fd2c9e1ffc497cd39681c4425480b3945", "states_updated": []} -------------------------------------------------------------------------------- /flags/alabama.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/screenshot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "finalhandler": "^1.1.2", 4 | "playwright-webkit": "^1.5.2", 5 | "puppeteer": "^5.4.1", 6 | "serve-static": "^1.14.1" 7 | }, 8 | "scripts": { 9 | "start": "node index.js" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /flags/colorado.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flags/new-mexico.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flags/texas.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flags/district-of-columbia.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flags/arizona.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to any of these? We are not pursuing these at the moment.** 11 | 12 | - [ ] graphs and data visualizations 13 | - [ ] projections and regression models 14 | - [ ] additional data added to the top of the page 15 | - [ ] changes to the hurdle rate. please see https://github.com/alex/nyt-2020-election-scraper/issues/194; the hurdle rate is not incorrect. 16 | 17 | **Is your feature request related to an issue? Please cite the issue.** 18 | 19 | **Clearly describe the solution you'd like** 20 | -------------------------------------------------------------------------------- /flags/hawaii.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Welcome! :wave: 4 | 5 | We're happy to consider new contributions to the project. That being said, a core aim of this project is comparative simplicity in the user interface, and we need to balance new features against the cost of added complexity. We feel we're approaching a logical point of feature completion, and at this point are focusing more on refinements that improve performance and reduce bugginess. 6 | 7 | This is to say that we ask two things, before you open a pull request: 8 | 9 | 1. Please open an issue to discuss your proposal with the developers. 10 | 11 | 2. Please follow the [pull request template](https://github.com/alex/nyt-2020-election-scraper/blob/master/.github/pull_request_template.md). 12 | -------------------------------------------------------------------------------- /flags/maryland.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flags/tennessee.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flags/alaska.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | ###### Motivation 11 | 12 | ###### Changes 13 | 14 | 15 | 16 | - [ ] Ensured that you have [rebased your branch](https://stackoverflow.com/a/7244456) with this repo's latest master branch. 17 | - [ ] Ensured that relevant issues are linked, if this PR resolves any outstanding. 18 | - [ ] Added a screenshot for all UI changes (you can drag the file into this edit box and it will be uploaded). 19 | - [ ] Ensured that changes to auto-generated files have not been committed; in particular, `*.html`, `*.csv`, `*.xml` and `*.json` files. 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Individual Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /flags/ohio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | Please make sure you're not reporting that the hurdle calculation is incorrect! Please see https://github.com/alex/nyt-2020-election-scraper/pull/367; the block breakdown might not match the hurdle because of the way third-party candidates affect the calculations. We don't have plans to change this. 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Desktop (please complete the following information):** 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | - Device: [e.g. iPhone6] 34 | - OS: [e.g. iOS8.1] 35 | - Browser [e.g. stock browser, safari] 36 | - Version [e.g. 22] 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NYT Vote Scraper 2 | Scrapes the NYT Votes Remaining Page JSON and commits it back to this repo. The goal is to be able to view history and diffs of `results.json`. 3 | 4 | ## Outputted files 5 | 6 | - 7 | - 8 | - 9 | - 10 | 11 | 12 | ## Inspired By 13 | Simon Willison: 14 | 15 | 16 | 17 | ## Development 18 | 19 | Dependencies 20 | 21 | * Python 3 is required 22 | 23 | 24 | ``` 25 | pip install -r requirements.txt 26 | ``` 27 | 28 | Contributions are welcome, but please make sure you read and fill out the [the pull request template](.github/pull_request_template.md) when submitting your changes. We would also appreciate it if you could read the short [contributing guide](https://github.com/alex/nyt-2020-election-scraper/blob/master/CONTRIBUTING.md). 29 | 30 | Please do not modify any of the static files (html, csv, txt, or xml). These files are dynamically generated. 31 | 32 | ## To Support The Creators 33 | We'd rather any money go to a good cause. Send any donations instead to ! 34 | -------------------------------------------------------------------------------- /.github/workflows/scrape.yml: -------------------------------------------------------------------------------- 1 | name: Scrape latest data 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | scheduled: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out this repo 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - name: Fetch latest data 18 | run: |- 19 | curl "https://static01.nyt.com/elections-assets/2020/data/api/2020-11-03/votes-remaining-page/national/president.json" | jq . > results.json 20 | - name: Commit and push if it changed 21 | run: |- 22 | git config user.name "Automated" 23 | git config user.email "actions@users.noreply.github.com" 24 | git add -A 25 | timestamp=$(date -u) 26 | git commit -m "Latest data: ${timestamp}" || exit 0 27 | git push 28 | - name: Set up Python 3.8 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: "3.8" 32 | - name: Install Python requirements 33 | run: |- 34 | python -m pip install -U pip 35 | pip install -r requirements.txt 36 | - name: Generate battleground-state-changes.txt/html 37 | run: |- 38 | ./print-battleground-state-changes 39 | - name: Commit and push if it changed 40 | run: |- 41 | git config user.name "Automated" 42 | git config user.email "actions@users.noreply.github.com" 43 | git add -A 44 | timestamp=$(date -u) 45 | git commit -m "Regenerate battleground-state-changes.txt/html" || exit 0 46 | git push 47 | -------------------------------------------------------------------------------- /battleground-state-changes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NYT 2020 Election Scraper RSS Feed 5 | https://alex.github.io/nyt-2020-election-scraper/battleground-state-changes.html 6 | Latest results from battleground states. 7 | Wed, 06 Jan 2021 01:11:54 -0000 8 | 9 | Alaska (EV: 3): Trump +36173 10 | Wed, 02 Dec 2020 19:04:28 -0000 11 | alaska@1606935868.768 12 | 13 | 14 | Arizona (EV: 11): Biden +10457 15 | Mon, 30 Nov 2020 21:15:17 -0000 16 | arizona@1606770917.713 17 | 18 | 19 | Georgia (EV: 16): Biden +11779 20 | Mon, 07 Dec 2020 20:17:22 -0000 21 | georgia@1607372242.36 22 | 23 | 24 | North Carolina (EV: 15): Trump +74481 25 | Fri, 27 Nov 2020 15:38:21 -0000 26 | north-carolina@1606491501.898 27 | 28 | 29 | Nevada (EV: 6): Biden +33596 30 | Wed, 06 Jan 2021 01:11:54 -0000 31 | nevada@1609895514.671 32 | 33 | 34 | Pennsylvania (EV: 20): Biden +81660 35 | Tue, 08 Dec 2020 20:01:43 -0000 36 | pennsylvania@1607457703.781 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out this repo 11 | uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - name: Set up Python 3.8 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: "3.8" 18 | - name: Install Python requirements 19 | run: |- 20 | python -m pip install -U pip 21 | pip install -r requirements.txt 22 | 23 | - id: files 24 | uses: jitterbit/get-changed-files@v1 25 | continue-on-error: true 26 | - name: Check for changed static files 27 | run: | 28 | for changed_file in ${{ steps.files.outputs.modified }}; do 29 | if [[ "${changed_file}" =~ ^((all|battleground)-state-changes.csv|(all|battleground)-state-changes.html|(all|battleground)-state-changes.txt|(all|battleground)-state-changes.xml|notification-updates.json)$ ]]; then 30 | echo "Modifications to static files are not allowed." 31 | exit 1 32 | fi 33 | done 34 | 35 | - name: Generate battleground-state-changes.txt/html 36 | run: |- 37 | ./print-battleground-state-changes 38 | - name: Upload test artifacts 39 | uses: actions/upload-artifact@v2 40 | with: 41 | name: "state-changes" 42 | path: | 43 | battleground-state-changes.* 44 | all-state-changes.* 45 | 46 | - name: Generate screenshots 47 | run: |- 48 | sudo apt-get install libwebkit2gtk-4.0-37 libgles2 gstreamer1.0-libav libgstreamer-plugins-bad1.0-0 xvfb 49 | npm install --prefix .github/screenshot 50 | npm start --prefix .github/screenshot 51 | # WebKit doesn't work properly in headless mode, use an Xvfb server 52 | PUPPETEER_PRODUCT=webkit xvfb-run npm start --prefix .github/screenshot 53 | PUPPETEER_PRODUCT=firefox npm install --prefix .github/screenshot puppeteer 54 | PUPPETEER_PRODUCT=firefox npm start --prefix .github/screenshot 55 | - name: Upload screenshots 56 | uses: actions/upload-artifact@v2 57 | with: 58 | name: screenshots 59 | path: | 60 | screenshot-*.png 61 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at alex.gaynor@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/screenshot/index.js: -------------------------------------------------------------------------------- 1 | const finalhandler = require("finalhandler"); 2 | const http = require("http"); 3 | const path = require("path"); 4 | const serveStatic = require("serve-static"); 5 | 6 | let puppeteer; 7 | let puppeteerDevices; 8 | let puppeteerProduct; 9 | let puppeteerHeadless; 10 | let puppeteerSetViewport; 11 | let puppeteerEmulate; 12 | 13 | if (process.env.PUPPETEER_PRODUCT === "webkit") { 14 | puppeteer = require("playwright-webkit").webkit; 15 | puppeteerDevices = require("playwright-webkit").devices; 16 | puppeteerProduct = "webkit"; 17 | puppeteerHeadless = false; 18 | puppeteerSetViewport = "setViewportSize"; 19 | puppeteerEmulate = async function (browser, device) { 20 | return await browser.newPage(device); 21 | } 22 | } else { 23 | puppeteer = require("puppeteer"); 24 | puppeteerDevices = puppeteer.devices; 25 | puppeteerProduct = puppeteer.product; 26 | puppeteerHeadless = true; 27 | puppeteerSetViewport = "setViewport"; 28 | puppeteerEmulate = async function (browser, device) { 29 | const page = await browser.newPage(); 30 | await page.emulate(device); 31 | return page; 32 | } 33 | } 34 | 35 | const DEVICES = { 36 | "iphone": "iPhone X", 37 | "iphone-landscape": "iPhone X landscape", 38 | "ipad": "iPad Pro", 39 | "ipad-landscape": "iPad Pro landscape", 40 | }; 41 | 42 | const REPOSITORY_ROOT = path.resolve(__dirname, "../.."); 43 | 44 | /* Exit on uncaught asynchronous exceptions to propagate the error to CI */ 45 | process.on("unhandledRejection", (reason, promise) => { 46 | console.error(reason); 47 | process.exit(1); 48 | }); 49 | 50 | (async function () { 51 | /* 52 | * Firefox's Puppeteer implementation doesn't play well with file:// URLs, 53 | * see https://github.com/puppeteer/puppeteer/issues/5504. 54 | * 55 | * As a workaround, run an HTTP server. :( 56 | */ 57 | const serve = serveStatic(REPOSITORY_ROOT); 58 | const server = http.createServer((request, response) => { 59 | /* https://expressjs.com/en/resources/middleware/serve-static.html#serve-files-with-vanilla-nodejs-http-server */ 60 | serve(request, response, finalhandler(request, response)); 61 | }); 62 | server.listen(); 63 | 64 | const pageUrl = `http://127.0.0.1:${server.address().port}/battleground-state-changes.html`; 65 | console.log(`Server listening at ${pageUrl}`); 66 | 67 | /* 68 | * Puppeteer refuses to install Chromium and Firefox at the same time, so 69 | * we have to use PUPPETEER_PRODUCT to choose which browser to install and 70 | * use. Sigh. 71 | */ 72 | console.log(`Launching ${puppeteerProduct}`); 73 | 74 | const browser = await puppeteer.launch({ headless: puppeteerHeadless }); 75 | 76 | async function screenshot(page, filename) { 77 | console.log(`Generating ${filename}`); 78 | await page.goto(pageUrl, { waitUntil: "load" }); 79 | 80 | await page.screenshot({ 81 | path: path.join(REPOSITORY_ROOT, `screenshot-${puppeteerProduct}-${filename}.png`), 82 | fullPage: true, 83 | }); 84 | 85 | await page.evaluate(() => { 86 | (() => { 87 | const feature = features["shrunk"]; 88 | feature.onDisable($(feature.buttonId)); 89 | 90 | document.querySelector("#arizona tr:nth-last-child(5)").scrollIntoView(true); 91 | })(); 92 | }); 93 | await page.screenshot({ 94 | path: path.join(REPOSITORY_ROOT, `screenshot-${puppeteerProduct}-${filename}-scrolled.png`), 95 | }); 96 | } 97 | 98 | const page = await browser.newPage(); 99 | await page[puppeteerSetViewport]({ 100 | width: 1280, 101 | height: 720, 102 | }); 103 | await screenshot(page, "desktop"); 104 | await page.close(); 105 | 106 | for (const [filename, name] of Object.entries(DEVICES)) { 107 | const page = await puppeteerEmulate(browser, puppeteerDevices[name]); 108 | await screenshot(page, filename); 109 | await page.close(); 110 | } 111 | 112 | await browser.close(); 113 | server.close(); 114 | })(); 115 | -------------------------------------------------------------------------------- /flags/indiana.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flags/rhode-island.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flags/arkansas.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flags/north-carolina.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /all-state-changes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NYT 2020 Election Scraper RSS Feed 5 | https://alex.github.io/nyt-2020-election-scraper/battleground-state-changes.html 6 | Latest results from battleground states. 7 | Wed, 06 Jan 2021 01:11:54 -0000 8 | 9 | Alaska (EV: 3): Trump +36173 10 | Wed, 02 Dec 2020 19:04:28 -0000 11 | alaska@1606935868.768 12 | 13 | 14 | Alabama (EV: 9): Trump +591546 15 | Tue, 05 Jan 2021 15:21:52 -0000 16 | alabama@1609860112.799 17 | 18 | 19 | Arkansas (EV: 6): Trump +336715 20 | Mon, 30 Nov 2020 19:11:18 -0000 21 | arkansas@1606763478.738 22 | 23 | 24 | Arizona (EV: 11): Biden +10457 25 | Mon, 30 Nov 2020 21:15:17 -0000 26 | arizona@1606770917.713 27 | 28 | 29 | California (EV: 55): Biden +5103803 30 | Sat, 05 Dec 2020 21:12:18 -0000 31 | california@1607202738.17 32 | 33 | 34 | Colorado (EV: 9): Biden +439745 35 | Sun, 06 Dec 2020 16:22:00 -0000 36 | colorado@1607271720.015 37 | 38 | 39 | Connecticut (EV: 7): Biden +365389 40 | Wed, 09 Dec 2020 20:46:27 -0000 41 | connecticut@1607546787.877 42 | 43 | 44 | District of Columbia (EV: 3): Biden +298737 45 | Tue, 24 Nov 2020 13:30:24 -0000 46 | district-of-columbia@1606224624.093 47 | 48 | 49 | Delaware (EV: 3): Biden +95665 50 | Wed, 11 Nov 2020 22:51:34 -0000 51 | delaware@1605135094.682 52 | 53 | 54 | Florida (EV: 29): Trump +371686 55 | Tue, 17 Nov 2020 17:19:42 -0000 56 | florida@1605633582.245 57 | 58 | 59 | Georgia (EV: 16): Biden +11779 60 | Mon, 07 Dec 2020 20:17:22 -0000 61 | georgia@1607372242.36 62 | 63 | 64 | Hawaii (EV: 4): Biden +169266 65 | Thu, 19 Nov 2020 20:34:03 -0000 66 | hawaii@1605818043.889 67 | 68 | 69 | Iowa (EV: 6): Trump +138611 70 | Sat, 05 Dec 2020 23:17:06 -0000 71 | iowa@1607210226.334 72 | 73 | 74 | Idaho (EV: 4): Trump +267097 75 | Tue, 05 Jan 2021 15:21:52 -0000 76 | idaho@1609860112.799 77 | 78 | 79 | Illinois (EV: 20): Biden +1025024 80 | Mon, 04 Jan 2021 17:16:55 -0000 81 | illinois@1609780615.978 82 | 83 | 84 | Indiana (EV: 11): Trump +487359 85 | Tue, 05 Jan 2021 23:08:20 -0000 86 | indiana@1609888100.57 87 | 88 | 89 | Kansas (EV: 6): Trump +201083 90 | Fri, 04 Dec 2020 18:16:41 -0000 91 | kansas@1607105801.791 92 | 93 | 94 | Kentucky (EV: 8): Trump +554172 95 | Fri, 20 Nov 2020 20:00:11 -0000 96 | kentucky@1605902411.666 97 | 98 | 99 | Louisiana (EV: 8): Trump +399742 100 | Sat, 28 Nov 2020 05:00:22 -0000 101 | louisiana@1606539622.883 102 | 103 | 104 | Massachusetts (EV: 11): Biden +1215000 105 | Fri, 11 Dec 2020 22:11:35 -0000 106 | massachusetts@1607724695.707 107 | 108 | 109 | Maryland (EV: 10): Biden +1008609 110 | Tue, 08 Dec 2020 02:02:06 -0000 111 | maryland@1607392926.697 112 | 113 | 114 | Maine (EV: 4): Biden +74335 115 | Wed, 09 Dec 2020 22:56:32 -0000 116 | maine@1607554592.243 117 | 118 | 119 | Michigan (EV: 16): Biden +154188 120 | Mon, 30 Nov 2020 03:32:17 -0000 121 | michigan@1606707137.498 122 | 123 | 124 | Minnesota (EV: 10): Biden +233012 125 | Mon, 30 Nov 2020 15:54:17 -0000 126 | minnesota@1606751657.517 127 | 128 | 129 | Missouri (EV: 10): Trump +465722 130 | Thu, 10 Dec 2020 13:11:40 -0000 131 | missouri@1607605900.386 132 | 133 | 134 | Mississippi (EV: 6): Trump +217366 135 | Wed, 16 Dec 2020 22:06:34 -0000 136 | mississippi@1608156394.948 137 | 138 | 139 | Montana (EV: 3): Trump +98816 140 | Thu, 19 Nov 2020 18:47:41 -0000 141 | montana@1605811661.684 142 | 143 | 144 | North Carolina (EV: 15): Trump +74481 145 | Fri, 27 Nov 2020 15:38:21 -0000 146 | north-carolina@1606491501.898 147 | 148 | 149 | North Dakota (EV: 3): Trump +120693 150 | Tue, 17 Nov 2020 03:18:53 -0000 151 | north-dakota@1605583133.774 152 | 153 | 154 | Nebraska (EV: 5): Trump +182263 155 | Thu, 10 Dec 2020 21:01:36 -0000 156 | nebraska@1607634096.753 157 | 158 | 159 | New Hampshire (EV: 4): Biden +59267 160 | Fri, 20 Nov 2020 20:43:32 -0000 161 | new-hampshire@1605905012.73 162 | 163 | 164 | New Jersey (EV: 14): Biden +725061 165 | Mon, 07 Dec 2020 23:47:03 -0000 166 | new-jersey@1607384823.41 167 | 168 | 169 | New Mexico (EV: 5): Biden +99720 170 | Wed, 25 Nov 2020 03:46:13 -0000 171 | new-mexico@1606275973.19 172 | 173 | 174 | Nevada (EV: 6): Biden +33596 175 | Wed, 06 Jan 2021 01:11:54 -0000 176 | nevada@1609895514.671 177 | 178 | 179 | New York (EV: 29): Biden +1993776 180 | Tue, 15 Dec 2020 14:46:47 -0000 181 | new-york@1608043607.044 182 | 183 | 184 | Ohio (EV: 18): Trump +475669 185 | Fri, 27 Nov 2020 22:28:15 -0000 186 | ohio@1606516095.134 187 | 188 | 189 | Oklahoma (EV: 7): Trump +516390 190 | Wed, 11 Nov 2020 23:14:54 -0000 191 | oklahoma@1605136494.799 192 | 193 | 194 | Oregon (EV: 7): Biden +381935 195 | Sun, 06 Dec 2020 01:41:50 -0000 196 | oregon@1607218910.915 197 | 198 | 199 | Pennsylvania (EV: 20): Biden +81660 200 | Tue, 08 Dec 2020 20:01:43 -0000 201 | pennsylvania@1607457703.781 202 | 203 | 204 | Rhode Island (EV: 4): Biden +107564 205 | Fri, 11 Dec 2020 16:26:38 -0000 206 | rhode-island@1607703998.81 207 | 208 | 209 | South Carolina (EV: 9): Trump +293562 210 | Thu, 12 Nov 2020 15:36:23 -0000 211 | south-carolina@1605195383.904 212 | 213 | 214 | South Dakota (EV: 3): Trump +110572 215 | Thu, 12 Nov 2020 03:12:23 -0000 216 | south-dakota@1605150743.995 217 | 218 | 219 | Tennessee (EV: 11): Trump +709035 220 | Tue, 15 Dec 2020 23:01:39 -0000 221 | tennessee@1608073299.475 222 | 223 | 224 | Texas (EV: 38): Trump +631221 225 | Fri, 27 Nov 2020 15:26:14 -0000 226 | texas@1606490774.796 227 | 228 | 229 | Utah (EV: 6): Trump +304858 230 | Tue, 24 Nov 2020 21:34:17 -0000 231 | utah@1606253657.205 232 | 233 | 234 | Virginia (EV: 13): Biden +451138 235 | Sun, 22 Nov 2020 15:33:41 -0000 236 | virginia@1606059221.127 237 | 238 | 239 | Vermont (EV: 3): Biden +130116 240 | Mon, 16 Nov 2020 22:18:52 -0000 241 | vermont@1605565132.813 242 | 243 | 244 | Washington (EV: 12): Biden +784961 245 | Tue, 08 Dec 2020 16:27:04 -0000 246 | washington@1607444824.77 247 | 248 | 249 | Wisconsin (EV: 10): Biden +20608 250 | Tue, 08 Dec 2020 01:37:07 -0000 251 | wisconsin@1607391427.412 252 | 253 | 254 | West Virginia (EV: 5): Trump +309398 255 | Sun, 06 Dec 2020 14:16:59 -0000 256 | west-virginia@1607264219.735 257 | 258 | 259 | Wyoming (EV: 3): Trump +120068 260 | Wed, 11 Nov 2020 22:19:53 -0000 261 | wyoming@1605133193.904 262 | 263 | 264 | 265 | 266 | -------------------------------------------------------------------------------- /battleground-state-changes.html.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 134 | Election 2020 Results 135 | 136 | 139 | 142 | 145 | 148 | 151 | 152 | 198 | 199 | 200 | 201 |
202 |
203 |
204 |

205 | All data below is sourced from an unofficial API powering the New York Times’ election site. 206 | The estimated votes remaining and county-level breakdown values might be off. 207 | Consult state websites and officials for the most accurate and up-to-date figures. 208 | This website is open source and was written by these people. 209 |

210 |

{% OTHER_PAGE_TEXT %}

211 |
212 |
213 |
214 |
215 |

Last scrape: {% SCRAPE_TIME %} | Last batch: {% BATCH_TIME %} {% LAST_BATCH %}

216 | 217 | 218 |
219 |
220 |

221 | 222 | 223 |

224 |
225 |
226 |
227 |
228 | {% TABLES %} 229 |
230 |
231 |
232 | 235 | 238 | 277 | 278 | 296 | 297 | 417 | 418 | -------------------------------------------------------------------------------- /flags/south-carolina.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /print-battleground-state-changes: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import csv 4 | import collections 5 | import datetime 6 | import email.utils 7 | import git 8 | import hashlib 9 | import itertools 10 | import os 11 | import simdjson 12 | import subprocess 13 | import json 14 | from textwrap import dedent, indent 15 | from tabulate import tabulate 16 | from typing import Dict, Tuple 17 | 18 | AK_INDEX = 0 19 | AZ_INDEX = 3 20 | GA_INDEX = 10 21 | NC_INDEX = 27 22 | NV_INDEX = 33 23 | PA_INDEX = 38 24 | 25 | BATTLEGROUND_STATES = ["Alaska", "Arizona", "Georgia", "North Carolina", "Nevada", "Pennsylvania"] 26 | STATE_INDEXES = range(51) # 50 States + DC 27 | 28 | CACHE_DIR = '_cache' 29 | # Bump this with any changes to `fetch_all_records` 30 | CACHE_VERSION = 2 31 | 32 | def git_commits_for(path): 33 | return subprocess.check_output(['git', 'log', "--format=%H", path]).strip().decode().splitlines() 34 | 35 | def git_show(ref, name, repo_client): 36 | commit_tree = repo_client.commit(ref).tree 37 | 38 | return commit_tree[name].data_stream.read() 39 | 40 | def fetch_all_records(): 41 | commits = git_commits_for("results.json") 42 | 43 | repo = git.Repo('.', odbt=git.db.GitCmdObjectDB) 44 | 45 | out = [] 46 | 47 | parser = simdjson.Parser() 48 | for ref in commits: 49 | cache_path = os.path.join(CACHE_DIR, ref[:2], ref[2:] + ".json") 50 | if os.path.exists(cache_path): 51 | with open(cache_path) as fh: 52 | try: 53 | record = simdjson.load(fh) 54 | except ValueError: 55 | continue 56 | if record['version'] == CACHE_VERSION: 57 | for row in record['rows']: 58 | out.append(InputRecord(*row)) 59 | continue 60 | blob = git_show(ref, 'results.json', repo) 61 | json = parser.parse(blob) 62 | timestamp = json['meta']['timestamp'] 63 | rows = [] 64 | for index in STATE_INDEXES: 65 | race = json['data']['races'][index] 66 | 67 | county_tot_exp_vote = [county['tot_exp_vote'] for county in race['counties']] 68 | if all(county_tot_exp_vote): 69 | tot_exp_vote = sum(county_tot_exp_vote) 70 | else: 71 | # NYT's data doesn't seem to have a county-by-county vote 72 | # estimate for the following states: Connecticut, Massachusetts, 73 | # Maine, New Hampshire, Rhode Island, and Vermont. So we have to 74 | # fall back to the state wide one. 75 | tot_exp_vote = race['tot_exp_vote'] 76 | 77 | record = InputRecord( 78 | timestamp, 79 | race['state_name'], 80 | race['state_id'], 81 | race['electoral_votes'], 82 | race['candidates'].as_list(), 83 | race['votes'], 84 | tot_exp_vote, 85 | race['precincts_total'], 86 | race['precincts_reporting'], 87 | {n['name']: n['votes'] for n in race['counties']}, 88 | ) 89 | rows.append(record) 90 | out.append(record) 91 | try: 92 | os.makedirs(os.path.dirname(cache_path)) 93 | except FileExistsError: 94 | pass 95 | with open(cache_path, 'w') as fh: 96 | simdjson.dump({"version": CACHE_VERSION, "rows": rows}, fh) 97 | 98 | out.sort(key=lambda row: row.timestamp) 99 | grouped = collections.defaultdict(list) 100 | for row in out: 101 | grouped[row.state_name].append(row) 102 | 103 | return grouped 104 | 105 | InputRecord = collections.namedtuple( 106 | 'InputRecord', 107 | [ 108 | 'timestamp', 109 | 'state_name', 110 | 'state_abbrev', 111 | 'electoral_votes', 112 | 'candidates', 113 | 'votes', 114 | 'expected_votes', 115 | 'precincts_total', 116 | 'precincts_reporting', 117 | 'counties', 118 | ], 119 | ) 120 | 121 | # Information that is shared across loop iterations 122 | IterationInfo = collections.namedtuple( 123 | 'IterationInfo', 124 | ['vote_diff', 'votes', 'precincts_reporting', 'hurdle', 'leading_candidate_name', 'counties', 'candidate_votes'] 125 | ) 126 | 127 | IterationSummary = collections.namedtuple( 128 | 'IterationSummary', 129 | [ 130 | 'timestamp', 131 | 'leading_candidate_name', 132 | 'trailing_candidate_name', 133 | 'leading_candidate_votes', 134 | 'trailing_candidate_votes', 135 | 'vote_differential', 136 | 'votes_remaining', 137 | 'new_votes', 138 | 'new_votes_relevant', 139 | 'new_votes_formatted', 140 | 'leading_candidate_partition', 141 | 'trailing_candidate_partition', 142 | 'precincts_reporting', 143 | 'precincts_total', 144 | 'hurdle', 145 | 'hurdle_change', 146 | 'hurdle_mov_avg', 147 | 'counties_partition', 148 | 'total_votes_count', 149 | ] 150 | ) 151 | 152 | def compute_hurdle_sma(summarized_state_data, newest_votes, new_partition_pct, trailing_candidate_name): 153 | """ 154 | trend gain of last 30k (or more) votes for trailing candidate 155 | """ 156 | hurdle_moving_average = None 157 | MIN_AGG_VOTES = 30000 158 | 159 | agg_votes = newest_votes 160 | agg_c2_votes = round(new_partition_pct * newest_votes) 161 | step = 0 162 | while step < len(summarized_state_data) and agg_votes < MIN_AGG_VOTES: 163 | this_summary = summarized_state_data[step] 164 | step += 1 165 | if this_summary.new_votes_relevant > 0: 166 | if this_summary.trailing_candidate_name == trailing_candidate_name: 167 | trailing_candidate_partition = this_summary.trailing_candidate_partition 168 | else: 169 | # Broken for 3 way race 170 | trailing_candidate_partition = this_summary.leading_candidate_partition 171 | 172 | if this_summary.new_votes_relevant + agg_votes > MIN_AGG_VOTES: 173 | subset_pct = (MIN_AGG_VOTES - agg_votes) / float(this_summary.new_votes_relevant) 174 | agg_votes += round(this_summary.new_votes_relevant * subset_pct) 175 | agg_c2_votes += round(trailing_candidate_partition * this_summary.new_votes_relevant * subset_pct) 176 | else: 177 | agg_votes += this_summary.new_votes_relevant 178 | agg_c2_votes += round(trailing_candidate_partition * this_summary.new_votes_relevant) 179 | 180 | if agg_votes: 181 | hurdle_moving_average = float(agg_c2_votes) / agg_votes 182 | 183 | return hurdle_moving_average 184 | 185 | 186 | def string_summary(summary): 187 | thirty_ago = (datetime.datetime.utcnow() - datetime.timedelta(minutes=30)) 188 | 189 | bumped = summary.leading_candidate_partition 190 | bumped_name = summary.leading_candidate_name 191 | if bumped < summary.trailing_candidate_partition: 192 | bumped = summary.trailing_candidate_partition 193 | bumped_name = summary.trailing_candidate_name 194 | bumped -= 0.50 195 | 196 | lead_part = summary.leading_candidate_partition - 50 197 | 198 | visible_hurdle = f'{summary.trailing_candidate_name} needs {summary.hurdle:.2%}' if summary.hurdle > 0 else 'Unknown' 199 | 200 | return [ 201 | f'{summary.timestamp.strftime("%Y-%m-%d %H:%M")}', 202 | '***' if summary.timestamp > thirty_ago else '---', 203 | f'{summary.leading_candidate_name} up {summary.vote_differential:,}', 204 | f'Left (est.): {summary.votes_remaining:,}' if summary.votes_remaining > 0 else 'Unknown', 205 | f'Δ: {summary.new_votes_formatted} ({f"{bumped_name} +{bumped:5.01%}" if (summary.leading_candidate_partition or summary.trailing_candidate_partition) else "n/a"})', 206 | f'{summary.precincts_reporting/summary.precincts_total:.2%} precincts', 207 | f'{visible_hurdle}', 208 | f'& trends {f"{summary.hurdle_mov_avg:.2%}" if summary.hurdle_mov_avg else "n/a"}' 209 | ] 210 | 211 | def html_write_state_head(state: str, state_slug: str, summary: IterationSummary): 212 | percentage_candidate1 = summary.leading_candidate_votes / summary.total_votes_count 213 | percentage_candidate2 = summary.trailing_candidate_votes / summary.total_votes_count 214 | return f''' 215 | 216 | 217 | 218 | 219 | {state_formatted_name[state]} 220 | 221 |
222 | {summary.leading_candidate_name} leads with {summary.leading_candidate_votes:,} votes ({percentage_candidate1:5.01%}), {summary.trailing_candidate_name} trails with {summary.trailing_candidate_votes:,} votes ({percentage_candidate2:5.01%}). 223 | 224 | 225 | 226 | Timestamp 227 | In The Lead 228 | Vote Margin 229 | Votes Remaining (est.) 230 | Change 231 | Batch Breakdown 232 | Batch Trend 233 | Hurdle 234 | 235 | 236 | ''' 237 | 238 | def html_summary(state_slug: str, summary: IterationSummary, always_visible: bool): 239 | if summary.counties_partition: 240 | counties_partition = sorted(summary.counties_partition.items(), key=lambda x: x[1], reverse=True) 241 | counties_total = sum(summary.counties_partition.values()) 242 | counties_tooltip_attributes = ( 243 | 'class="has-tip numeric" data-toggle="tooltip" data-html="true" data-title="' + 244 | 'Estimated county-level breakdown:
' + 245 | '
'.join(f'{name}: {value / counties_total:.0%}' for name, value in counties_partition) + 246 | '" title="' + 247 | 'Estimated county-level breakdown: ' + 248 | ', '.join(f'{name}: {value / counties_total:.0%}' for name, value in counties_partition) + 249 | '"' 250 | ) 251 | else: 252 | counties_tooltip_attributes = 'class="numeric"' 253 | shown_votes_remaining = f'{summary.votes_remaining:,}' if summary.votes_remaining > 0 else 'Unknown' 254 | always_visible_attribute = ' class="always-visible"' if always_visible else '' 255 | html = f''' 256 | 257 | {summary.timestamp.strftime('%Y-%m-%d %H:%M:%S')} UTC 258 | {summary.leading_candidate_name} 259 | {summary.vote_differential:,} 260 | {shown_votes_remaining} 261 | {summary.new_votes_formatted} 262 | ''' 263 | 264 | if summary.leading_candidate_partition == 0 and summary.trailing_candidate_partition == 0: 265 | html += 'N/A' 266 | elif summary.leading_candidate_partition >= 0 or summary.trailing_candidate_partition >= 0: 267 | # Since both must now positive or zero, so we're abs them because `-0` 268 | # is a thing for floats. 269 | html += f''' 270 | 271 | {summary.leading_candidate_name} {abs(summary.leading_candidate_partition):5.01%} / 272 | {abs(summary.trailing_candidate_partition):5.01%} {summary.trailing_candidate_name} 273 | 274 | ''' 275 | else: 276 | html += 'Unknown' 277 | 278 | if summary.hurdle_mov_avg and summary.hurdle_mov_avg >= 0: 279 | html += f''' 280 | 281 | {summary.trailing_candidate_name} is averaging {summary.hurdle_mov_avg:5.01%} 282 | 283 | ''' 284 | elif summary.hurdle_mov_avg and summary.hurdle_mov_avg < 0: 285 | html += 'Unknown' 286 | else: 287 | html += 'N/A' 288 | 289 | visible_hurdle = f'{summary.trailing_candidate_name} needs {summary.hurdle:.1%}' if summary.hurdle > 0 else 'Unknown' 290 | html += f''' 291 | {visible_hurdle} 292 | 293 | ''' 294 | 295 | return html 296 | 297 | # Capture the time at the top of the main script logic so it's closer to when the pull of data happened 298 | scrape_time = datetime.datetime.utcnow() 299 | 300 | # Dict[str, List[InputRecords]] 301 | records = fetch_all_records() 302 | 303 | # Where we’ll aggregate the data from the JSON files 304 | summarized = {} 305 | state_formatted_name = {} 306 | state_abbrev = {} 307 | 308 | def json_to_summary( 309 | state_name: str, 310 | row: InputRecord, 311 | last_iteration_info: IterationInfo, 312 | latest_candidate_votes: Dict[str, int], 313 | expected_votes: int, 314 | batch_time: datetime.datetime, 315 | ) -> Tuple[IterationInfo, IterationSummary]: 316 | timestamp = datetime.datetime.strptime(row.timestamp, '%Y-%m-%dT%H:%M:%S.%fZ') 317 | 318 | # Retrieve relevant data from the state’s JSON blob 319 | candidate1 = row.candidates[0] # Leading candidate 320 | candidate2 = row.candidates[1] # Trailing candidate 321 | candidate1_name = candidate1['last_name'] 322 | candidate2_name = candidate2['last_name'] 323 | candidate1_votes = candidate1['votes'] 324 | candidate2_votes = candidate2['votes'] 325 | candidate1_key = candidate1['candidate_key'] 326 | candidate2_key = candidate2['candidate_key'] 327 | total_votes = sum([candidate['votes'] for candidate in row.candidates]) 328 | vote_diff = candidate1_votes - candidate2_votes 329 | votes = row.votes 330 | votes_remaining = expected_votes - votes 331 | precincts_reporting = row.precincts_reporting 332 | precincts_total = row.precincts_total 333 | new_votes = 0 if last_iteration_info.votes is None else (votes - last_iteration_info.votes) 334 | 335 | counties_partition = {} 336 | if new_votes != 0: 337 | assert row.counties.keys() == last_iteration_info.counties.keys() 338 | for k, v in row.counties.items(): 339 | partition = (v - last_iteration_info.counties[k]) 340 | if partition > 0: 341 | counties_partition[k] = partition 342 | 343 | bumped = candidate1_name != last_iteration_info.leading_candidate_name 344 | 345 | # If we're trying to estimate the proportion of third-party votes across 346 | # the entire vote population, we want to use the largest sample size we 347 | # have - i.e. the newest data we've received 348 | latest_relevant_proportion = (latest_candidate_votes[candidate1_key] + latest_candidate_votes[candidate2_key]) / sum(latest_candidate_votes.values()) 349 | votes_remaining_relevant = votes_remaining * latest_relevant_proportion 350 | hurdle = (vote_diff + votes_remaining_relevant) / (2 * votes_remaining_relevant) if votes_remaining_relevant > 0 else 0 351 | 352 | candidate_votes = {c['candidate_key']: c['votes'] for c in row.candidates} 353 | 354 | # We need to use the votes delta for our two leading candidates, not for 355 | # all the candidates, when calculating the breakdown - especially since our 356 | # data source frequently revises write-in and third party figures. 357 | if new_votes == 0: 358 | new_votes_relevant = 0 359 | else: 360 | new_votes_relevant = sum(candidate_votes[k] - last_iteration_info.candidate_votes[k] for k in (candidate1_key, candidate2_key)) 361 | 362 | if new_votes_relevant == 0: 363 | trailing_candidate_partition = 0 364 | leading_candidate_partition = 0 365 | else: 366 | trailing_candidate_partition = (candidate2_votes - last_iteration_info.candidate_votes[candidate2_key]) / new_votes_relevant 367 | leading_candidate_partition = 1 - trailing_candidate_partition 368 | 369 | # Info we’ll need for the next loop iteration 370 | iteration_info = IterationInfo( 371 | vote_diff=vote_diff, 372 | votes=votes, 373 | precincts_reporting=precincts_reporting, 374 | hurdle=hurdle, 375 | leading_candidate_name=candidate1_name, 376 | counties=row.counties, 377 | candidate_votes=candidate_votes, 378 | ) 379 | 380 | # Compute aggregate of last 5 hurdle, if available 381 | hurdle_mov_avg = compute_hurdle_sma(summarized[state_name], new_votes_relevant, trailing_candidate_partition, candidate2_name) 382 | 383 | summary = IterationSummary( 384 | batch_time, 385 | candidate1_name, 386 | candidate2_name, 387 | candidate1_votes, 388 | candidate2_votes, 389 | vote_diff, 390 | votes_remaining, 391 | new_votes, 392 | new_votes_relevant, 393 | f"{new_votes_relevant:7,}" if new_votes_relevant >= 0 else "Unknown", 394 | leading_candidate_partition, 395 | trailing_candidate_partition, 396 | precincts_reporting, 397 | precincts_total, 398 | hurdle, 399 | hurdle-(1-last_iteration_info.hurdle if bumped else last_iteration_info.hurdle), 400 | hurdle_mov_avg, 401 | counties_partition, 402 | total_votes, 403 | ) 404 | 405 | return iteration_info, summary 406 | 407 | states_updated = [] 408 | 409 | for rows in records.values(): 410 | latest_batch_time = datetime.datetime.strptime(rows[-1].timestamp, '%Y-%m-%dT%H:%M:%S.%fZ') 411 | 412 | state_name = rows[0].state_name 413 | 414 | summarized[state_name] = [] 415 | state_formatted_name[state_name] = f"{state_name} (EV: {rows[0].electoral_votes})" 416 | state_abbrev[state_name] = rows[0].state_abbrev 417 | 418 | last_iteration_info = IterationInfo( 419 | vote_diff=None, 420 | votes=None, 421 | precincts_reporting=None, 422 | hurdle=0, 423 | leading_candidate_name=None, 424 | counties=None, 425 | candidate_votes=None, 426 | ) 427 | 428 | latest_candidate_votes = {c['candidate_key']: c['votes'] for c in rows[-1].candidates} 429 | latest_expected_votes = rows[-1].expected_votes 430 | 431 | for row in rows: 432 | iteration_info, summary = json_to_summary( 433 | state_name, 434 | row, 435 | last_iteration_info, 436 | latest_candidate_votes, 437 | latest_expected_votes, 438 | batch_time=datetime.datetime.strptime(row.timestamp, '%Y-%m-%dT%H:%M:%S.%fZ'), 439 | ) 440 | 441 | # Avoid writing duplicate rows 442 | if last_iteration_info == iteration_info: 443 | continue 444 | 445 | # Generate the string we’ll output and store it 446 | summarized[state_name].insert(0, summary) 447 | 448 | # Save info for the next iteration 449 | last_iteration_info = iteration_info 450 | 451 | if summarized[state_name] and summarized[state_name][0].timestamp == latest_batch_time: 452 | states_updated.append(state_name) 453 | 454 | # Pull out the battleground state summaries 455 | battlegrounds_summarized = { 456 | state: summarized[state] 457 | for state in BATTLEGROUND_STATES 458 | } 459 | battleground_states_updated = [ 460 | state for state in states_updated 461 | if state in BATTLEGROUND_STATES 462 | ] 463 | 464 | # print the summaries 465 | batch_time = max(itertools.chain.from_iterable(battlegrounds_summarized.values()), key=lambda s: s.timestamp).timestamp 466 | with open("results.json", "r", encoding='utf8') as f: 467 | RESULTS_HASH = hashlib.sha256(f.read().encode('utf8')).hexdigest() 468 | 469 | def txt_output(path, summarized, states_updated): 470 | with open(path, "w") as f: 471 | print(tabulate([ 472 | ["Last updated:", scrape_time.strftime("%Y-%m-%d %H:%M UTC")], 473 | ["Latest batch received: {}".format(f"({', '.join(states_updated)})" if states_updated else ""), batch_time.strftime("%Y-%m-%d %H:%M UTC")], 474 | ["Prettier web version:", "https://alex.github.io/nyt-2020-election-scraper/battleground-state-changes.html"], 475 | ]), file=f) 476 | 477 | for (state, timestamped_results) in sorted(summarized.items()): 478 | print(f'\n{state_formatted_name[state]} Total Votes: ({timestamped_results[0][1]}: {timestamped_results[0][3]:,}, {timestamped_results[0][2]}: {timestamped_results[0][4]:,})', file=f) 479 | print(tabulate([string_summary(summary) for summary in timestamped_results]), file=f) 480 | 481 | txt_output("battleground-state-changes.txt", battlegrounds_summarized, battleground_states_updated) 482 | txt_output("all-state-changes.txt", summarized, states_updated) 483 | 484 | def html_table(summarized): 485 | # The NYTimes array of states is not sorted alphabetically, so we'll use `sorted` 486 | for (state, timestamped_results) in sorted(summarized.items()): 487 | # 'Alaska (3)' -> 'alaska', 'North Carolina (15)' -> 'north-carolina' 488 | state_slug = state.split('(')[0].strip().replace(' ', '-').lower() 489 | yield f"
" 490 | yield html_write_state_head(state, state_slug, timestamped_results[0]) 491 | visible_rows = 0 492 | last_change_value = None 493 | for summary in timestamped_results: 494 | change_value = summary.new_votes_formatted.strip() 495 | if visible_rows >= 3: 496 | always_visible = False 497 | elif {change_value, last_change_value} <= {'0', 'Unknown'}: 498 | always_visible = False 499 | else: 500 | always_visible = True 501 | visible_rows += 1 502 | 503 | yield html_summary(state_slug=state_slug, summary=summary, always_visible=always_visible) 504 | last_change_value = change_value 505 | yield "

" 506 | 507 | def html_output(path, html_table, states_updated, other_page_html): 508 | html_template = "\n\n" 509 | with open("battleground-state-changes.html.tmpl", "r", encoding='utf8') as f: 510 | html_template += f.read() 511 | TEMPLATE_HASH = hashlib.sha256(html_template.encode('utf8')).hexdigest() 512 | 513 | states_updated_abbrev = [state_abbrev[state_name] for state_name in states_updated] 514 | 515 | with open(path,"w", encoding='utf8') as f: 516 | page_metadata = json.dumps({ 517 | "template_hash": TEMPLATE_HASH, 518 | "results_hash": RESULTS_HASH, 519 | "states_updated": states_updated_abbrev, 520 | }) 521 | 522 | html = html_template \ 523 | .replace('{% TABLES %}', "\n".join(html_table)) \ 524 | .replace('{% SCRAPE_TIME %}', scrape_time.strftime('%Y-%m-%d %H:%M:%S UTC')) \ 525 | .replace('{% BATCH_TIME %}', batch_time.strftime('%Y-%m-%d %H:%M:%S UTC')) \ 526 | .replace('{% LAST_BATCH %}', f"({', '.join(states_updated)})" if states_updated else "") \ 527 | .replace('{% TEMPLATE_HASH %}', TEMPLATE_HASH) \ 528 | .replace('{% PAGE_METADATA %}', page_metadata) \ 529 | .replace('{% OTHER_PAGE_TEXT %}', other_page_html) 530 | 531 | f.write(html) 532 | 533 | html_output( 534 | "battleground-state-changes.html", 535 | html_table(battlegrounds_summarized), 536 | battleground_states_updated, 537 | 'Data for all 50 states and DC is also available.' 538 | ) 539 | html_output( 540 | "all-state-changes.html", 541 | html_table(summarized), 542 | states_updated, 543 | 'View battleground states only.' 544 | ) 545 | 546 | def csv_output(path, summarized): 547 | with open(path, 'w') as csvfile: 548 | wr = csv.writer(csvfile) 549 | wr.writerow(('state',) + IterationSummary._fields) 550 | for state, results in summarized.items(): 551 | for row in results: 552 | wr.writerow((state_formatted_name[state],) + row) 553 | 554 | csv_output('battleground-state-changes.csv', battlegrounds_summarized) 555 | csv_output('all-state-changes.csv', summarized) 556 | 557 | def rss_output(path, summarized): 558 | with open(path, 'w') as rssfile: 559 | print(dedent(f''' 560 | 561 | 562 | 563 | NYT 2020 Election Scraper RSS Feed 564 | https://alex.github.io/nyt-2020-election-scraper/battleground-state-changes.html 565 | Latest results from battleground states. 566 | {email.utils.formatdate(batch_time.timestamp())} 567 | ''').strip(), file=rssfile) 568 | 569 | for state, results in summarized.items(): 570 | try: 571 | result = results[0] 572 | except IndexError: 573 | continue 574 | state_slug = state.split('(')[0].strip().replace(' ', '-').lower() 575 | timestamp = result.timestamp.timestamp() 576 | print(indent(dedent(f''' 577 | 578 | {state_formatted_name[state]}: {result.leading_candidate_name} +{result.vote_differential} 579 | {email.utils.formatdate(timestamp)} 580 | {state_slug}@{timestamp} 581 | 582 | ''').strip(), " "), file=rssfile) 583 | 584 | print(dedent(''' 585 | 586 | ''' 587 | ), file=rssfile) 588 | 589 | rss_output('battleground-state-changes.xml', battlegrounds_summarized) 590 | rss_output('all-state-changes.xml', summarized) 591 | 592 | # this file is deprecated and should not be used! it will be removed soontm 593 | with open("notification-updates.json", "w") as f: 594 | f.write(json.dumps({ 595 | "_comment": "this file is deprecated and should not be used", 596 | "results_hash": RESULTS_HASH, 597 | "states_updated": battleground_states_updated, 598 | })) 599 | -------------------------------------------------------------------------------- /.github/screenshot/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "@types/node": { 6 | "version": "14.14.6", 7 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz", 8 | "integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==", 9 | "optional": true 10 | }, 11 | "@types/yauzl": { 12 | "version": "2.9.1", 13 | "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", 14 | "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", 15 | "optional": true, 16 | "requires": { 17 | "@types/node": "*" 18 | } 19 | }, 20 | "agent-base": { 21 | "version": "5.1.1", 22 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", 23 | "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==" 24 | }, 25 | "balanced-match": { 26 | "version": "1.0.0", 27 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 28 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 29 | }, 30 | "base64-js": { 31 | "version": "1.3.1", 32 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 33 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" 34 | }, 35 | "bl": { 36 | "version": "4.0.3", 37 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", 38 | "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", 39 | "requires": { 40 | "buffer": "^5.5.0", 41 | "inherits": "^2.0.4", 42 | "readable-stream": "^3.4.0" 43 | } 44 | }, 45 | "brace-expansion": { 46 | "version": "1.1.11", 47 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 48 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 49 | "requires": { 50 | "balanced-match": "^1.0.0", 51 | "concat-map": "0.0.1" 52 | } 53 | }, 54 | "buffer": { 55 | "version": "5.7.1", 56 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 57 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 58 | "requires": { 59 | "base64-js": "^1.3.1", 60 | "ieee754": "^1.1.13" 61 | } 62 | }, 63 | "buffer-crc32": { 64 | "version": "0.2.13", 65 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 66 | "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" 67 | }, 68 | "chownr": { 69 | "version": "1.1.4", 70 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 71 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 72 | }, 73 | "concat-map": { 74 | "version": "0.0.1", 75 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 76 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 77 | }, 78 | "debug": { 79 | "version": "4.2.0", 80 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", 81 | "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", 82 | "requires": { 83 | "ms": "2.1.2" 84 | } 85 | }, 86 | "depd": { 87 | "version": "1.1.2", 88 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 89 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 90 | }, 91 | "destroy": { 92 | "version": "1.0.4", 93 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 94 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 95 | }, 96 | "devtools-protocol": { 97 | "version": "0.0.809251", 98 | "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.809251.tgz", 99 | "integrity": "sha512-pf+2OY6ghMDPjKkzSWxHMq+McD+9Ojmq5XVRYpv/kPd9sTMQxzEt21592a31API8qRjro0iYYOc3ag46qF/1FA==" 100 | }, 101 | "ee-first": { 102 | "version": "1.1.1", 103 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 104 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 105 | }, 106 | "encodeurl": { 107 | "version": "1.0.2", 108 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 109 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 110 | }, 111 | "end-of-stream": { 112 | "version": "1.4.4", 113 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 114 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 115 | "requires": { 116 | "once": "^1.4.0" 117 | } 118 | }, 119 | "escape-html": { 120 | "version": "1.0.3", 121 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 122 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 123 | }, 124 | "etag": { 125 | "version": "1.8.1", 126 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 127 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 128 | }, 129 | "extract-zip": { 130 | "version": "2.0.1", 131 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", 132 | "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", 133 | "requires": { 134 | "@types/yauzl": "^2.9.1", 135 | "debug": "^4.1.1", 136 | "get-stream": "^5.1.0", 137 | "yauzl": "^2.10.0" 138 | } 139 | }, 140 | "fd-slicer": { 141 | "version": "1.1.0", 142 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", 143 | "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", 144 | "requires": { 145 | "pend": "~1.2.0" 146 | } 147 | }, 148 | "finalhandler": { 149 | "version": "1.1.2", 150 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 151 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 152 | "requires": { 153 | "debug": "2.6.9", 154 | "encodeurl": "~1.0.2", 155 | "escape-html": "~1.0.3", 156 | "on-finished": "~2.3.0", 157 | "parseurl": "~1.3.3", 158 | "statuses": "~1.5.0", 159 | "unpipe": "~1.0.0" 160 | }, 161 | "dependencies": { 162 | "debug": { 163 | "version": "2.6.9", 164 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 165 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 166 | "requires": { 167 | "ms": "2.0.0" 168 | } 169 | }, 170 | "ms": { 171 | "version": "2.0.0", 172 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 173 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 174 | } 175 | } 176 | }, 177 | "find-up": { 178 | "version": "4.1.0", 179 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 180 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 181 | "requires": { 182 | "locate-path": "^5.0.0", 183 | "path-exists": "^4.0.0" 184 | } 185 | }, 186 | "fresh": { 187 | "version": "0.5.2", 188 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 189 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 190 | }, 191 | "fs-constants": { 192 | "version": "1.0.0", 193 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 194 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" 195 | }, 196 | "fs.realpath": { 197 | "version": "1.0.0", 198 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 199 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 200 | }, 201 | "get-stream": { 202 | "version": "5.2.0", 203 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", 204 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", 205 | "requires": { 206 | "pump": "^3.0.0" 207 | } 208 | }, 209 | "glob": { 210 | "version": "7.1.6", 211 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 212 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 213 | "requires": { 214 | "fs.realpath": "^1.0.0", 215 | "inflight": "^1.0.4", 216 | "inherits": "2", 217 | "minimatch": "^3.0.4", 218 | "once": "^1.3.0", 219 | "path-is-absolute": "^1.0.0" 220 | } 221 | }, 222 | "graceful-fs": { 223 | "version": "4.2.4", 224 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", 225 | "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" 226 | }, 227 | "http-errors": { 228 | "version": "1.7.3", 229 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", 230 | "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", 231 | "requires": { 232 | "depd": "~1.1.2", 233 | "inherits": "2.0.4", 234 | "setprototypeof": "1.1.1", 235 | "statuses": ">= 1.5.0 < 2", 236 | "toidentifier": "1.0.0" 237 | } 238 | }, 239 | "https-proxy-agent": { 240 | "version": "4.0.0", 241 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", 242 | "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", 243 | "requires": { 244 | "agent-base": "5", 245 | "debug": "4" 246 | } 247 | }, 248 | "ieee754": { 249 | "version": "1.2.1", 250 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 251 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" 252 | }, 253 | "inflight": { 254 | "version": "1.0.6", 255 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 256 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 257 | "requires": { 258 | "once": "^1.3.0", 259 | "wrappy": "1" 260 | } 261 | }, 262 | "inherits": { 263 | "version": "2.0.4", 264 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 265 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 266 | }, 267 | "jpeg-js": { 268 | "version": "0.4.2", 269 | "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.2.tgz", 270 | "integrity": "sha512-+az2gi/hvex7eLTMTlbRLOhH6P6WFdk2ITI8HJsaH2VqYO0I594zXSYEP+tf4FW+8Cy68ScDXoAsQdyQanv3sw==" 271 | }, 272 | "locate-path": { 273 | "version": "5.0.0", 274 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 275 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 276 | "requires": { 277 | "p-locate": "^4.1.0" 278 | } 279 | }, 280 | "mime": { 281 | "version": "1.6.0", 282 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 283 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 284 | }, 285 | "minimatch": { 286 | "version": "3.0.4", 287 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 288 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 289 | "requires": { 290 | "brace-expansion": "^1.1.7" 291 | } 292 | }, 293 | "mkdirp-classic": { 294 | "version": "0.5.3", 295 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 296 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" 297 | }, 298 | "ms": { 299 | "version": "2.1.2", 300 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 301 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 302 | }, 303 | "node-fetch": { 304 | "version": "2.6.1", 305 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 306 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" 307 | }, 308 | "on-finished": { 309 | "version": "2.3.0", 310 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 311 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 312 | "requires": { 313 | "ee-first": "1.1.1" 314 | } 315 | }, 316 | "once": { 317 | "version": "1.4.0", 318 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 319 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 320 | "requires": { 321 | "wrappy": "1" 322 | } 323 | }, 324 | "p-limit": { 325 | "version": "2.3.0", 326 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", 327 | "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 328 | "requires": { 329 | "p-try": "^2.0.0" 330 | } 331 | }, 332 | "p-locate": { 333 | "version": "4.1.0", 334 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 335 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 336 | "requires": { 337 | "p-limit": "^2.2.0" 338 | } 339 | }, 340 | "p-try": { 341 | "version": "2.2.0", 342 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 343 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" 344 | }, 345 | "parseurl": { 346 | "version": "1.3.3", 347 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 348 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 349 | }, 350 | "path-exists": { 351 | "version": "4.0.0", 352 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 353 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" 354 | }, 355 | "path-is-absolute": { 356 | "version": "1.0.1", 357 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 358 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 359 | }, 360 | "pend": { 361 | "version": "1.2.0", 362 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 363 | "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" 364 | }, 365 | "pkg-dir": { 366 | "version": "4.2.0", 367 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", 368 | "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", 369 | "requires": { 370 | "find-up": "^4.0.0" 371 | } 372 | }, 373 | "playwright-webkit": { 374 | "version": "1.5.2", 375 | "resolved": "https://registry.npmjs.org/playwright-webkit/-/playwright-webkit-1.5.2.tgz", 376 | "integrity": "sha512-E8bMYxueeOe+r/Gb+/zAcJRrf+uUb32f3b6iU5IZxi/T+qVUpO63uk9Yz69x/IC9MLo6n7/4bLk3kVyogRatfA==", 377 | "requires": { 378 | "debug": "^4.1.1", 379 | "extract-zip": "^2.0.1", 380 | "https-proxy-agent": "^5.0.0", 381 | "jpeg-js": "^0.4.2", 382 | "mime": "^2.4.6", 383 | "pngjs": "^5.0.0", 384 | "progress": "^2.0.3", 385 | "proper-lockfile": "^4.1.1", 386 | "proxy-from-env": "^1.1.0", 387 | "rimraf": "^3.0.2", 388 | "ws": "^7.3.1" 389 | }, 390 | "dependencies": { 391 | "agent-base": { 392 | "version": "6.0.2", 393 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 394 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 395 | "requires": { 396 | "debug": "4" 397 | } 398 | }, 399 | "https-proxy-agent": { 400 | "version": "5.0.0", 401 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", 402 | "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", 403 | "requires": { 404 | "agent-base": "6", 405 | "debug": "4" 406 | } 407 | }, 408 | "mime": { 409 | "version": "2.4.6", 410 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", 411 | "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" 412 | } 413 | } 414 | }, 415 | "pngjs": { 416 | "version": "5.0.0", 417 | "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", 418 | "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" 419 | }, 420 | "progress": { 421 | "version": "2.0.3", 422 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 423 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" 424 | }, 425 | "proper-lockfile": { 426 | "version": "4.1.1", 427 | "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.1.tgz", 428 | "integrity": "sha512-1w6rxXodisVpn7QYvLk706mzprPTAPCYAqxMvctmPN3ekuRk/kuGkGc82pangZiAt4R3lwSuUzheTTn0/Yb7Zg==", 429 | "requires": { 430 | "graceful-fs": "^4.1.11", 431 | "retry": "^0.12.0", 432 | "signal-exit": "^3.0.2" 433 | } 434 | }, 435 | "proxy-from-env": { 436 | "version": "1.1.0", 437 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 438 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 439 | }, 440 | "pump": { 441 | "version": "3.0.0", 442 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 443 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 444 | "requires": { 445 | "end-of-stream": "^1.1.0", 446 | "once": "^1.3.1" 447 | } 448 | }, 449 | "puppeteer": { 450 | "version": "5.4.1", 451 | "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-5.4.1.tgz", 452 | "integrity": "sha512-8u6r9tFm3gtMylU4uCry1W/CeAA8uczKMONvGvivkTsGqKA7iB7DWO2CBFYlB9GY6/IEoq9vkI5slJWzUBkwNw==", 453 | "requires": { 454 | "debug": "^4.1.0", 455 | "devtools-protocol": "0.0.809251", 456 | "extract-zip": "^2.0.0", 457 | "https-proxy-agent": "^4.0.0", 458 | "node-fetch": "^2.6.1", 459 | "pkg-dir": "^4.2.0", 460 | "progress": "^2.0.1", 461 | "proxy-from-env": "^1.0.0", 462 | "rimraf": "^3.0.2", 463 | "tar-fs": "^2.0.0", 464 | "unbzip2-stream": "^1.3.3", 465 | "ws": "^7.2.3" 466 | } 467 | }, 468 | "range-parser": { 469 | "version": "1.2.1", 470 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 471 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 472 | }, 473 | "readable-stream": { 474 | "version": "3.6.0", 475 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 476 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 477 | "requires": { 478 | "inherits": "^2.0.3", 479 | "string_decoder": "^1.1.1", 480 | "util-deprecate": "^1.0.1" 481 | } 482 | }, 483 | "retry": { 484 | "version": "0.12.0", 485 | "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", 486 | "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" 487 | }, 488 | "rimraf": { 489 | "version": "3.0.2", 490 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 491 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 492 | "requires": { 493 | "glob": "^7.1.3" 494 | } 495 | }, 496 | "safe-buffer": { 497 | "version": "5.2.1", 498 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 499 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 500 | }, 501 | "send": { 502 | "version": "0.17.1", 503 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 504 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 505 | "requires": { 506 | "debug": "2.6.9", 507 | "depd": "~1.1.2", 508 | "destroy": "~1.0.4", 509 | "encodeurl": "~1.0.2", 510 | "escape-html": "~1.0.3", 511 | "etag": "~1.8.1", 512 | "fresh": "0.5.2", 513 | "http-errors": "~1.7.2", 514 | "mime": "1.6.0", 515 | "ms": "2.1.1", 516 | "on-finished": "~2.3.0", 517 | "range-parser": "~1.2.1", 518 | "statuses": "~1.5.0" 519 | }, 520 | "dependencies": { 521 | "debug": { 522 | "version": "2.6.9", 523 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 524 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 525 | "requires": { 526 | "ms": "2.0.0" 527 | }, 528 | "dependencies": { 529 | "ms": { 530 | "version": "2.0.0", 531 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 532 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 533 | } 534 | } 535 | }, 536 | "ms": { 537 | "version": "2.1.1", 538 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 539 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 540 | } 541 | } 542 | }, 543 | "serve-static": { 544 | "version": "1.14.1", 545 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 546 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 547 | "requires": { 548 | "encodeurl": "~1.0.2", 549 | "escape-html": "~1.0.3", 550 | "parseurl": "~1.3.3", 551 | "send": "0.17.1" 552 | } 553 | }, 554 | "setprototypeof": { 555 | "version": "1.1.1", 556 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 557 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 558 | }, 559 | "signal-exit": { 560 | "version": "3.0.3", 561 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 562 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" 563 | }, 564 | "statuses": { 565 | "version": "1.5.0", 566 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 567 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 568 | }, 569 | "string_decoder": { 570 | "version": "1.3.0", 571 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 572 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 573 | "requires": { 574 | "safe-buffer": "~5.2.0" 575 | } 576 | }, 577 | "tar-fs": { 578 | "version": "2.1.1", 579 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", 580 | "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", 581 | "requires": { 582 | "chownr": "^1.1.1", 583 | "mkdirp-classic": "^0.5.2", 584 | "pump": "^3.0.0", 585 | "tar-stream": "^2.1.4" 586 | } 587 | }, 588 | "tar-stream": { 589 | "version": "2.1.4", 590 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz", 591 | "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", 592 | "requires": { 593 | "bl": "^4.0.3", 594 | "end-of-stream": "^1.4.1", 595 | "fs-constants": "^1.0.0", 596 | "inherits": "^2.0.3", 597 | "readable-stream": "^3.1.1" 598 | } 599 | }, 600 | "through": { 601 | "version": "2.3.8", 602 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 603 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 604 | }, 605 | "toidentifier": { 606 | "version": "1.0.0", 607 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 608 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 609 | }, 610 | "unbzip2-stream": { 611 | "version": "1.4.3", 612 | "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", 613 | "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", 614 | "requires": { 615 | "buffer": "^5.2.1", 616 | "through": "^2.3.8" 617 | } 618 | }, 619 | "unpipe": { 620 | "version": "1.0.0", 621 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 622 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 623 | }, 624 | "util-deprecate": { 625 | "version": "1.0.2", 626 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 627 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 628 | }, 629 | "wrappy": { 630 | "version": "1.0.2", 631 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 632 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 633 | }, 634 | "ws": { 635 | "version": "7.3.1", 636 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", 637 | "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" 638 | }, 639 | "yauzl": { 640 | "version": "2.10.0", 641 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", 642 | "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", 643 | "requires": { 644 | "buffer-crc32": "~0.2.3", 645 | "fd-slicer": "~1.1.0" 646 | } 647 | } 648 | } 649 | } 650 | -------------------------------------------------------------------------------- /flags/mississippi.svg: -------------------------------------------------------------------------------- 1 | image/svg+xml.st0{fill:#EAAB22;} .st1{fill:#C21F32;} .st2{fill:#081F40;} .st3{fill:#FFFFFF;} -------------------------------------------------------------------------------- /flags/georgia.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------