From 057324dae7209f618086cd4fa8101e0c4a9cc723 Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:50:47 +0000 Subject: [PATCH] cleanup readme --- .gitignore | 3 +- README.md | 115 ++++------------- app/test.ipynb | 333 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 360 insertions(+), 91 deletions(-) create mode 100644 app/test.ipynb diff --git a/.gitignore b/.gitignore index f002cf0..9bb39a9 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ local_archive_test *db-shm copy-files.sh temp/ -.python-version \ No newline at end of file +.python-version +orchestration2.yaml \ No newline at end of file diff --git a/README.md b/README.md index b292107..7b4fcf9 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,19 @@ A web API that uses celery workers to process URL archive requests via [bellingcat/auto-archiver](https://github.com/bellingcat/auto-archiver), it allows authentication via Google OAuth Apps and enables CORS, everything runs on docker but development can be done without docker (except for redis). -### setup -To properly set up the API you need to install `docker` and to edit 2 files: -1. a `.env` to configure the API, stays at the root level +## setup +To properly set up the API you need to install `docker` and to edit 3 files: +1. a `.env.prod` and `.env.dev` to configure the API, stays at the root level 2. a `user-groups.yaml` to manage user permissions -Do not commit those files, they are .gitignored by default. + 1. note that all local files referenced in `user-groups.yaml` and any orchestration.yaml files should be relative to the home directory so if your service account is in `secrets/orchestration.yaml` use that path and not just `orchestration.yaml`. -We have examples for both of those, and here's how to set them up whether you're in development or production: +Do not commit those files, they are .gitignored by default. +We also advise you to keep any sensitive files in the `secrets/` folder which is pinned and gitignored. -#### setup for DEVELOPMENT + +We have examples for both of those files (`.env.example` and `user-groups.example.yaml`), and here's how to set them up whether you're in development or production: + +### setup for DEVELOPMENT ```bash # copy and modify the .env.dev file according to your needs cp .env.example .env.dev @@ -26,7 +30,7 @@ curl 'http://localhost:8004/health' ``` now go to http://localhost:8004/docs#/ and you should see the API documentation -#### setup for PRODUCTION +### setup for PRODUCTION ```bash # copy and modify the .env.prod file according to your needs cp .env.example .env.prod @@ -49,86 +53,36 @@ there are 2 ways to access the API The permissions are defined solely via the `user-groups.yaml` file - users belong to groups which determine their access level/quotas/orchestration setup - users are assigned to groups explicitly (via email) - - users are assigned to groups implicitly (via email domains) - - domains are associated to groups + - users are assigned to groups implicitly (via email domains) as domains can be associated to groups - users that are not explicitly or implicitly in the system belong to the `default` group, restrict their permissions if you do not wish them to be able to search/archive - if a user is assigned to one group which is not explicitly defined, a warning will be thrown, it may be necessary to do that if you discontinue a given group but the database still has entries for it and so - groups determine - - which orchestrator to use for single URL archives and for spreadsheet archives + - which orchestrator to use for single URL archives and for spreadsheet archives see [GroupPermissions](app/shared/user_groups.py) - a set of permissions - `read` can be [`all`], [] or a comma separated list of group names, meaning people in this group can access either all, none, or those belonging to explicitly listed groups. - the group itself must be included in the list, otherwise the user cannot search archives of that group + - `read_public` a boolean that enables the user to search public archives - `archive_url` a boolean that enables the user to archive links in this group - `archive_sheet` a boolean that enables the user to archive spreadsheets + - `manually_trigger_sheet` a boolean that enables the user to manually trigger a sheet archive for sheets in this group - `sheet_frequency` a list of options for the sheet archiving frequency, currently max permissions is `["hourly", "daily"]` - `max_sheets` defines the maximum amount of spreadsheets someone can have in total (`-1` means no limit) - `max_archive_lifespan_months` defines the lifespan of an archive before being deleted from S3, users will be notified 1 month in advance with instructions to download TODO - - `monthly_urls` how many total URLs someone can archive per month (`-1` means no limit) - - `monthly_mbs` how many MBs of data someone can archive per month (`-1` means no limit) + - `max_monthly_urls` how many total URLs someone can archive per month (`-1` means no limit) + - `max_monthly_mbs` how many MBs of data someone can archive per month (`-1` means no limit) - `priority` one of `high` or `low`, this will be used to give archiving priority - group names are all lower-case -To figure out: -- workshop participants should be able to test this. `public` -- how can people bring their own storage/api keys? -- how to implement lifespan of archives? 6 months lifespan example. they should expect a way to download all archives locally. -- how to deactivate unused sheets and notify? -- how to mark URLs for deletion, and then do a hard delete? -- what actions can people take: - - URL (P=needs permission, O=open) - - P archive - - P search - - O find own links - - DISABLED find by id - - P delete archive (soft) - - Sheets - - P create a new sheet - - O get my sheets - - O delete a sheet - - P archive a sheet now +## development of web/worker without docker - -## Development -http://localhost:8004 - -TODO: update .env file instructions, should use .env.prod and .env.dev and only use .env for always overwriting dev/prod settings. - -requires `src/.env` - -cd /src -* console 1 - `docker compose up redis` optionally add `web` if not running uvicorn locally -* console 2 - `pipenv shell` + `celery worker --app=worker.celery --loglevel=info --logfile=logs/celery_dev.log` - * `celery --app=worker.celery worker --loglevel=info --logfile=logs/celery_dev.log` celery 5 - * or with watchdog for dev auto-reload `watchmedo auto-restart -d ./ -- celery --app=worker.celery worker --loglevel=info --logfile=logs/celery_dev.log` -* console 3 - `pipenv shell` + `uvicorn main:app --host 0.0.0.0 --reload` -orchestration must be from the console(?) -* turn off VPNs if connection to docker is not working +We advise you to use `make prod` but you can also spin up redis and run the API (uvicorn) and worker (celery) individually like so: +* console 1 - `make dev-redis-only` to spin up redis, turn off any VPNs +* console 2 - `export ENVIRONMENT_FILE=.env.dev` then `poetry run celery --app=app.worker.main.celery worker --loglevel=debug --logfile=/aa-api/logs/celery.log -Q high_priority,low_priority --concurrency=1` + * or with watchdog for dev auto-reload `watchmedo auto-restart --patterns="*.py" --recursive --ignore-directories -- celery -- --app=app.worker.main.celery worker --loglevel=debug --logfile=/aa-api/logs/celery.log -Q high_priority,low_priority --concurrency=1` +* console 3 - `export ENVIRONMENT_FILE=.env.dev` then `poetry run uvicorn main:app --host 0.0.0.0 --reload` -## User management -TODO: update description and example -- users/domains/groups -Copy [example.user-groups.yaml](src/example.user-groups.yaml) into a new file and set the environment variable `USER_GROUPS_FILENAME` to that filename (defaults to `user-groups.yaml`). - -This file contains 2 parts user-groups specifications. Each user can archive URLs publicly, privately, or privately for a group so long as they are declared as part of that group. In the example bellow `email1` has 2 groups while `email3` has none. -```yaml -users: - email1@example.com: - - group1 - - group2 - email2@example.com: - - group2 - email3@example-no-group.com: -``` - -Auto-archiver orchestrator files configurations. For each archiving task an orchestrator is chosen, either from a specified group (if group-level visibility) or the first group the user is assigned to in the above file or the `default` orchestration file which is a required config. -```yaml -orchestrators: - group1: secrets/orchestration-group1.yaml - group2: secrets/orchestration-group2.yaml - default: secrets/orchestration-default:orchestration.yaml -``` ## Database migrations check https://alembic.sqlalchemy.org/en/latest/tutorial.html#the-migration-environment @@ -143,32 +97,13 @@ poetry run alembic upgrade head poetry run alembic downgrade -1 ``` -* create migrations with `alembic revision -m "create account table"` -* if running in the normal pipenv environment use `PIPENV_DOTENV_LOCATION=.env.alembic pipenv run` followed by: - * migrate to most recent with `alembic upgrade head` - * downgrade with `alembic downgrade -1` - ## Release -Update `main.py:VERSION`. +Update the version in [config.py](app/web/config.py) -Copy `.env` and `src/.env` to deployment, along with the contents of `secrets/` including `secrets/orchestration.yaml`. +Make sure environment and user-groups files are up to date. Then `make prod`. -#### updating packages/app/access -If pipenv packages are updated: `make prod` to build images with new packages. - -New users should be added to the `src/.env` file `ALLOWED_EMAILS` prop. - -Run `pipenv update auto-archiver` inside `src` to update the auto-archiver version being used, then test with `make dev`. - - -```bash -# CALL /sheet POST endpoint -curl -XPOST -H "Authorization: Bearer GOOGLE_OAUTH_TOKEN" -H "Content-type: application/json" -d '{"sheet_id": "SHEET_ID", "header": 1}' 'http://localhost:8004/sheet' - -``` - ### Testing ```bash diff --git a/app/test.ipynb b/app/test.ipynb new file mode 100644 index 0000000..21cb64c --- /dev/null +++ b/app/test.ipynb @@ -0,0 +1,333 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + } + ], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [], + "source": [ + "from shared.user_groups import *" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2025-02-03 11:10:33.769\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mshared.user_groups\u001b[0m:\u001b[36mcheck_groups_consistency\u001b[0m:\u001b[36m121\u001b[0m - \u001b[33m\u001b[1mThese groups are associated to DOMAINS but not defined in the GROUPS section, the domains settings may not work as expected: {'edmo', 'witness'}\u001b[0m\n", + "\u001b[32m2025-02-03 11:10:33.771\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mshared.user_groups\u001b[0m:\u001b[36mcheck_groups_consistency\u001b[0m:\u001b[36m123\u001b[0m - \u001b[33m\u001b[1mThese groups are associated to USERS but not defined in the GROUPS section, the users settings may not work as expected: {'witness'}\u001b[0m\n" + ] + } + ], + "source": [ + "ug = UserGroups(\"user-groups.dev.yaml\")" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_items([('bellingcat', GroupModel(description='Bellingcat staff', orchestrator='secrets/orchestration-bcat.yaml', orchestrator_sheet='secrets/orchestration-bcat-sheet.yaml', permissions=GroupPermissions(read={'all'}, archive_url=True, archive_sheet=True, sheet_frequency={'hourly', 'daily'}, max_sheets=-1, max_archive_lifespan_months=-1, max_monthly_urls=-1, max_monthly_mbs=-1, priority='high'))), ('janda', GroupModel(description='J&A team - Ukraine Project', orchestrator='secrets/orchestration-janda-ukr.yaml', orchestrator_sheet='secrets/orchestration-janda-ukr-sheet.yaml', permissions=GroupPermissions(read={'all'}, archive_url=True, archive_sheet=True, sheet_frequency={'hourly', 'daily'}, max_sheets=-1, max_archive_lifespan_months=-1, max_monthly_urls=-1, max_monthly_mbs=-1, priority='low'))), ('ukraine', GroupModel(description='Members working on the Ukraine Civharm/Witness projects', orchestrator='secrets/orchestration-default.yaml', orchestrator_sheet='secrets/orchestration-default.yaml', permissions=GroupPermissions(read={'ukraine', 'witness'}, archive_url=True, archive_sheet=True, sheet_frequency={'daily'}, max_sheets=5, max_archive_lifespan_months=48, max_monthly_urls=1000, max_monthly_mbs=1000, priority='low'))), ('friends-1', GroupModel(description='Friends of Bellingcat, 1', orchestrator='secrets/orchestration-default.yaml', orchestrator_sheet='secrets/orchestration-default.yaml', permissions=GroupPermissions(read={'friends-1'}, archive_url=False, archive_sheet=False, sheet_frequency=[], max_sheets=0, max_archive_lifespan_months=12, max_monthly_urls=0, max_monthly_mbs=0, priority='low'))), ('default', GroupModel(description='Public access', orchestrator='secrets/orchestration-default.yaml', orchestrator_sheet='secrets/orchestration-default.yaml', permissions=GroupPermissions(read=set(), archive_url=False, archive_sheet=False, sheet_frequency=[], max_sheets=0, max_archive_lifespan_months=12, max_monthly_urls=0, max_monthly_mbs=0, priority='low')))])" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ug.groups.items()" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "GroupModel(description='Bellingcat staff', orchestrator='secrets/orchestration-bcat.yaml', orchestrator_sheet='secrets/orchestration-bcat-sheet.yaml', permissions=GroupPermissions(read={'all'}, archive_url=True, archive_sheet=True, sheet_frequency={'hourly', 'daily'}, max_sheets=-1, max_archive_lifespan_months=-1, max_monthly_urls=-1, max_monthly_mbs=-1, priority='high'))" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ug.groups[\"bellingcat\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"bellingcat\" in ug.groups" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "AssertionError\n", + "that was unexpected\n" + ] + } + ], + "source": [ + "try:\n", + "\tassert 123 == \"as\", \"that was unexpected\"\n", + "\traise ValueError(\"this is a test\")\n", + "except Exception as e:\n", + "\tprint(type(e))\n", + "\tprint(e.__class__.__name__)\n", + "\tprint(e)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "def custom_hash(s: str) -> int:\n", + " hash = 0\n", + " for char in s:\n", + " hash = (hash * 31 + ord(char)) & 0xFFFFFFFF # 31 is a prime number, used in Java hash\n", + " return hash % 60 # Ensure it fits within 60 minutes" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "def fnv1a_hash_mod(s: str, modulo:int) -> int:\n", + " hash = 0x811c9dc5 # FNV offset basis\n", + " fnv_prime = 0x01000193 # FNV prime\n", + " for char in s:\n", + " hash ^= ord(char)\n", + " hash *= fnv_prime\n", + " hash &= 0xFFFFFFFF # Keep it 32-bit\n", + " return (hash if hash < 0x80000000 else hash - 0x100000000) % modulo" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'N7wlA3HmjrDxENPpshxpkiTY6FVUB4OlKGgIMuui1M0p'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#generate random google sheet ids\n", + "import random\n", + "import string\n", + "def random_sheet_id():\n", + " return ''.join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=44))\n", + "random_sheet_id()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "from tqdm import tqdm\n", + "from collections import defaultdict, Counter\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 1000000/1000000 [00:16<00:00, 60730.44it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Counter({1: 42205, 2: 41980, 4: 41915, 7: 41851, 14: 41785, 12: 41781, 10: 41744, 23: 41734, 6: 41709, 5: 41707, 18: 41705, 8: 41697, 15: 41654, 16: 41651, 9: 41642, 11: 41562, 22: 41555, 21: 41533, 3: 41481, 13: 41461, 19: 41453, 20: 41453, 0: 41447, 17: 41295})\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "modulo_counter = Counter()\n", + "for i in tqdm(range(1_000_000)):\n", + "\tid = random_sheet_id()\n", + "\thash = fnv1a_hash_mod(id, 24)\n", + "\t# hash = custom_hash(id)\n", + "\tmodulo_counter[hash] += 1\n", + "print(modulo_counter)\n", + "# custom: [00:16<00:00, 60651.26it/s]" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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dxd0yHn//fd1zz336J577pEkeTwe3XPPPSosLFRERIQOHTqkf//3f9ddd92lnJwcpaam6s9//rPsdru1xqZNmzRy5EhNnTpVM2bM0He+852Av4ETHR2tnTt36vjx40pNTdV//ud/qrCwMOBv6XzrW99SaWmpfv3rX2vs2LH6/e9/rzfffFOjR4++nvMBAAAMEfSNx5MnT5bf77/i/I4dO666RkxMjEpLS7usSUlJ0Z///Ocuax5++GE9/PDDV30/AADQ8/DdVQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIwUdcnbv3q0HHnhA8fHxCgsL05tvvhkw7/f7VVhYqMGDB6tPnz5KT0/Xxx9/HFDz+eefa/bs2XI4HBowYIBycnJ05syZgJpDhw7p3nvvVWRkpBISErRy5cpLetm6datGjhypyMhIjRkzRtu3bw/2cAAAgKGCDjktLS0aO3asioqKLju/cuVKvfjiiyouLta+ffvUt29fud1unTt3zqqZPXu2jhw5ooqKCm3btk27d+/WM888Y837fD5NmzZNiYmJqq2t1c9//nMtXbpUv/71r62aPXv26NFHH1VOTo4OHjyomTNnaubMmfrggw+CPSQAAGCgXsHukJGRoYyMjMvO+f1+rV27VosXL9aDDz4oSfrNb36juLg4vfnmm3rkkUf04Ycfqry8XO+9954mTJggSfrlL3+pGTNm6Be/+IXi4+O1adMmtbW1acOGDbLZbLr77rtVV1en1atXW2Fo3bp1mj59uhYuXChJWr58uSoqKrR+/XoVFxdf08kAAADmuKH35Bw/flxer1fp6enWtujoaKWlpammpkaSVFNTowEDBlgBR5LS09MVHh6uffv2WTX33XefbDabVeN2u1VfX68vvvjCqrn4fTprOt/nclpbW+Xz+QIGAAAw0w0NOV6vV5IUFxcXsD0uLs6a83q9io2NDZjv1auXYmJiAmout8bF73Glms75y1mxYoWi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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.bar(modulo_counter.keys(), modulo_counter.values())\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjkAAAGdCAYAAADwjmIIAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAMXVJREFUeJzt3X9UVWW+x/EPiAfMBEQvHM8NiTs14m9Ni06ZY8kSlRwtx5tF5ZpIbw1USsvKe40crSjK3zIyTmM6a2Ay545OaYMyOEoloqJclRyyGSa4OQfuXQonLQFl3z9msW8n0aQOIo/v11rPWp7n+e69n/0Ap0+bvTkBlmVZAgAAMExgR08AAACgPRByAACAkQg5AADASIQcAABgJEIOAAAwEiEHAAAYiZADAACMRMgBAABGCuroCXSk5uZmHT9+XD169FBAQEBHTwcAAFwCy7L0+eefy+VyKTDwwtdrruqQc/z4cUVHR3f0NAAAwLdQXV2t66677oLjV3XI6dGjh6R/LFJoaGgHzwYAAFwKr9er6Oho+7/jF3JVh5yWX1GFhoYScgAA6GS+6VYTbjwGAABGIuQAAAAjEXIAAICRCDkAAMBIhBwAAGAkQg4AADASIQcAABiJkAMAAIxEyAEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMFJQR08AwPmuf25rq/1/eyXpMs8EADovruQAAAAjcSUHPriCcOlYKwC4shFyDMN/eNFR+N7zxXoAHY+QAwCXWWcMQJ1xzp0Va+0/hJxO6Lv8AHTGH56OmvOVulZX29f/asTX6crH16hzIOS0E34ALh/W+sr3TV+jK/VreCXO60qck3Tlzuu76Iz/Q9FZf9baCyEHlwU/eJ0fXyPztdfX+EL7bdn3d3l/uNreW9rzfDpjqPsmhBxcsiv1m/hirtQ3QN6oLt3V+DX8Lr5LIABMQ8jpILzZAMDViff/y4eQA+CqxX9srm58/dumM64XIecK1Bm/keCLryEAdDxCDgBjfdMNrwDMRsiB33D1AgBwJeEDOgEAgJEIOQAAwEhtDjlFRUWaNGmSXC6XAgICtHnz5vNqjh49qh/+8IcKCwtT9+7ddfPNN6uqqsoeP3PmjFJTU9WrVy9de+21mjp1qmpqanz2UVVVpaSkJF1zzTWKjIzU3LlzdfbsWZ+anTt36qabblJwcLBuuOEGrVu3rq2nAwAADNXmkHP69GkNHTpU2dnZrY7/5S9/0ahRoxQXF6edO3fq0KFDev755xUSEmLXzJkzR++++642btyoXbt26fjx47r33nvt8XPnzikpKUmNjY3avXu31q9fr3Xr1ikjI8OuqaysVFJSku68806VlZVp9uzZevTRR7Vt27a2nhIAADBQm288njBhgiZMmHDB8f/4j//QxIkTlZWVZfd973vfs/9dX1+vX/7yl8rLy9Ndd90lSXrzzTfVv39/7dmzR7feequ2b9+ujz76SH/84x8VFRWlYcOGadGiRXr22We1YMECORwO5eTkKDY2VosXL5Yk9e/fXx988IGWLl2qxMTEtp4WAAAwjF/vyWlubtbWrVv1/e9/X4mJiYqMjFR8fLzPr7RKS0vV1NSkhIQEuy8uLk59+/ZVcXGxJKm4uFiDBw9WVFSUXZOYmCiv16vy8nK75qv7aKlp2UdrGhoa5PV6fRoAADCTX0NObW2tTp06pVdeeUXjx4/X9u3bdc899+jee+/Vrl27JEkej0cOh0Ph4eE+20ZFRcnj8dg1Xw04LeMtYxer8Xq9+vLLL1udX2ZmpsLCwuwWHR39nc8ZAABcmfx+JUeSJk+erDlz5mjYsGF67rnndPfddysnJ8efh/pW5s2bp/r6ertVV1d39JQAAEA78WvI6d27t4KCgjRgwACf/v79+9tPVzmdTjU2Nqqurs6npqamRk6n0675+tNWLa+/qSY0NFTdunVrdX7BwcEKDQ31aQAAwEx+DTkOh0M333yzKioqfPo//vhjxcTESJJGjBihrl27qrCw0B6vqKhQVVWV3G63JMntduvw4cOqra21awoKChQaGmoHKLfb7bOPlpqWfQAAgKtbm5+uOnXqlD755BP7dWVlpcrKyhQREaG+fftq7ty5uu+++zR69Gjdeeedys/P17vvvqudO3dKksLCwpSSkqL09HRFREQoNDRUTzzxhNxut2699VZJ0rhx4zRgwAA99NBDysrKksfj0fz585Wamqrg4GBJ0mOPPaZVq1bpmWee0SOPPKIdO3bo7bff1tatF/6sGgAAcPVoc8jZv3+/7rzzTvt1enq6JGnGjBlat26d7rnnHuXk5CgzM1NPPvmk+vXrp//8z//UqFGj7G2WLl2qwMBATZ06VQ0NDUpMTNTPfvYze7xLly7asmWLHn/8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x3kfNHs1r9fhw4d17bXXKjg4WI899pg2bdqkAQMGsFateOutt3TgwAFlZmaeN8Z6+YqPj9e6deuUn5+v1atXq7KyUnfccYc+//xz1upr/vrXv2r16tW68cYbtW3bNj3++ON68skntX79ekmX932+U3+sA3A1SU1N1ZEjR3zuA8D5+vXrp7KyMtXX1+u3v/2tZsyYoV27dnX0tK441dXVeuqpp1RQUKCQkJCOns4Vb8KECfa/hwwZovj4eMXExOjtt99Wt27dOnBmV57m5maNHDlSL7/8siRp+PDhOnLkiHJycjRjxozLOheu5LST3r17q0uXLufdXV9TUyOn09lBs7rytawN6+YrLS1NW7Zs0Z/+9Cddd911dr/T6VRjY6Pq6up86q/m9XI4HLrhhhs0YsQIZWZmaujQoVq+fDlr9TWlpaWqra3VTTfdpKCgIAUFBWnXrl1asWKFgoKCFBUVxXpdRHh4uL7//e/rk08+4Xvra/r06aMBAwb49PXv39/+9d7lfJ8n5LQTh8OhESNGqLCw0O5rbm5WYWGh3G53B87syhYbGyun0+mzbl6vVyUlJVflulmWpbS0NG3atEk7duxQbGysz/iIESPUtWtXn/WqqKhQVVXVVblerWlublZDQwNr9TVjx47V4cOHVVZWZreRI0cqOTnZ/jfrdWGnTp3SX/7yF/Xp04fvra+5/fbbz/tTFx9//LFiYmIkXeb3eb/exgwfb731lhUcHGytW7fO+uijj6xZs2ZZ4eHhlsfj6eipdajPP//cOnjwoHXw4EFLkrVkyRLr4MGD1qeffmpZlmW98sorVnh4uPX73//eOnTokDV58mQrNjbW+vLLLzt45pff448/boWFhVk7d+60/v73v9vtiy++sGsee+wxq2/fvtaOHTus/fv3W26323K73R04647z3HPPWbt27bIqKyutQ4cOWc8995wVEBBgbd++3bIs1uqbfPXpKstivb7q6aeftnbu3GlVVlZaH374oZWQkGD17t3bqq2ttSyLtfqqvXv3WkFBQdZLL71kHTt2zMrNzbWuueYa69e//rVdc7ne5wk57WzlypVW3759LYfDYd1yyy3Wnj17OnpKHe5Pf/qTJem8NmPGDMuy/vF44fPPP29FRUVZwcHB1tixY62KioqOnXQHaW2dJFlvvvmmXfPll19aP/nJT6yePXta11xzjXXPPfdYf//73ztu0h3okUcesWJiYiyHw2H90z/9kzV27Fg74FgWa/VNvh5yWK//d99991l9+vSxHA6H9c///M/WfffdZ33yySf2OGvl691337UGDRpkBQcHW3FxcdaaNWt8xi/X+3yAZVmWf68NAQAAdDzuyQEAAEYi5AAAACMRcgAAgJEIOQAAwEiEHAAAYCRCDgAAMBIhBwAAGImQAwAAjETIAQAARiLkAAAAIxFyAACAkQg5AADASP8He2oeTQhz1ZAAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.bar(modulo_counter.keys(), modulo_counter.values())\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "leaks-ArvCfdH_", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}