From 0ce42f4b3a3291bdc72348051447455637344318 Mon Sep 17 00:00:00 2001 From: Mikael Siidorow Date: Sun, 16 Apr 2023 23:30:09 +0300 Subject: [PATCH 1/8] refactor!: switch to Poetry, Ruff, upgrade to 3.10 --- .dockerignore | 4 +- .env.example | 2 - .github/workflows/lint.yaml | 31 +- .github/workflows/main.yaml | 2 +- .gitignore | 4 +- .pylintrc | 633 --------- Dockerfile | 16 +- Pipfile | 32 - Pipfile.lock | 1004 -------------- README.md | 15 +- dev.Dockerfile | 17 +- kipubot/__init__.py | 31 +- kipubot/bot.py | 25 +- kipubot/constants.py | 102 +- kipubot/db.py | 170 ++- kipubot/handlers/__init__.py | 2 +- kipubot/handlers/_bot_added_handler.py | 16 +- kipubot/handlers/_error_handler.py | 23 +- kipubot/handlers/_excel_file_handler.py | 35 +- kipubot/handlers/_graph_handlers.py | 40 +- kipubot/handlers/_moro_handler.py | 14 +- kipubot/handlers/_no_dm_handler.py | 9 +- kipubot/handlers/_raffle_setup_handler.py | 421 +++--- kipubot/handlers/_start_handler.py | 8 +- kipubot/handlers/_winner_handler.py | 40 +- kipubot/utils.py | 204 +-- poetry.lock | 1466 +++++++++++++++++++++ pyproject.toml | 43 + test.Dockerfile | 17 +- tests/test_utils.py | 99 +- 30 files changed, 2275 insertions(+), 2250 deletions(-) delete mode 100644 .pylintrc delete mode 100644 Pipfile delete mode 100644 Pipfile.lock create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.dockerignore b/.dockerignore index 7540d98..ee1d473 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,6 @@ ** # Except for dependencies and source code -!Pipfile -!Pipfile.lock +!pyproject.toml +!poetry.lock !kipubot \ No newline at end of file diff --git a/.env.example b/.env.example index 1987d62..42abd03 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,2 @@ -PYTHONPATH=${PYTHONPATH}:kipubot - BOT_TOKEN= DATABASE_URL= diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index c37fe00..f266411 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -23,29 +23,28 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install pipenv + - name: Install poetry run: | python3 -m pip install --upgrade pip - pip install pipenv + python3 -m pip install --user pipx + python3 -m pipx ensurepath + pipx install poetry - - name: Install dependencies - run: | - pipenv install --dev + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: "3.10" + cache: "poetry" - - name: Set PYTHONPATH + - name: Install dependencies run: | - echo "PYTHONPATH=${PYTHONPATH}:kipubot" >> $GITHUB_ENV + poetry install - name: Lint with pylint run: | - pipenv run lint + poetry run poe lint - name: Set testing BOT_TOKEN run: | @@ -53,12 +52,12 @@ jobs: - name: Type-check with pytype run: | - pipenv run pytype + poetry run poe type env: DATABASE_URL: "postgres://postgres:postgres@localhost:5432/postgres" - name: Run unit tests run: | - pipenv run pytest + poetry run poe test env: DATABASE_URL: "postgres://postgres:postgres@localhost:5432/postgres" diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 60e6c82..a086487 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest needs: [lint] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: akhileshns/heroku-deploy@v3.12.12 with: heroku_api_key: ${{secrets.HEROKU_API_KEY}} diff --git a/.gitignore b/.gitignore index 455844f..e0a7860 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ env/ .env data/ .pytype/ -.pytest_cache/ \ No newline at end of file +.pytest_cache/ +__pycache__/ +.vscode \ No newline at end of file diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 107c6ea..0000000 --- a/.pylintrc +++ /dev/null @@ -1,633 +0,0 @@ -[MAIN] -disable=missing-module-docstring, - missing-class-docstring, - missing-function-docstring - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Load and enable all available extensions. Use --list-extensions to see a list -# all available extensions. -#enable-all-extensions= - -# In error mode, messages with a category besides ERROR or FATAL are -# suppressed, and no reports are done by default. Error mode is compatible with -# disabling specific errors. -#errors-only= - -# Always return a 0 (non-error) status code, even if lint errors are found. -# This is primarily useful in continuous integration scripts. -#exit-zero= - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-allow-list= - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. (This is an alternative name to extension-pkg-allow-list -# for backward compatibility.) -extension-pkg-whitelist= - -# Return non-zero exit code if any of these messages/categories are detected, -# even if score is above --fail-under value. Syntax same as enable. Messages -# specified are enabled, while categories only check already-enabled messages. -fail-on= - -# Specify a score threshold to be exceeded before program exits with error. -fail-under=10 - -# Interpret the stdin as a python script, whose filename needs to be passed as -# the module_or_package argument. -#from-stdin= - -# Files or directories to be skipped. They should be base names, not paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the ignore-list. The -# regex matches against paths and can be in Posix or Windows format. -ignore-paths= - -# Files or directories matching the regex patterns are skipped. The regex -# matches against base names, not paths. The default value ignores Emacs file -# locks -ignore-patterns=^\.# - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use, and will cap the count on Windows to -# avoid hangs. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Minimum Python version to use for version dependent checks. Will default to -# the version used to run pylint. -py-version=3.8 - -# Discover python modules and packages in the file system subtree. -recursive=no - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# In verbose mode, extra non-checker-related info will be displayed. -#verbose= - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'fatal', 'error', 'warning', 'refactor', -# 'convention', and 'info' which contain the number of messages in each -# category, as well as 'statement' which is the total number of statements -# analyzed. This score is used by the global evaluation report (RP0004). -evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -#output-format= - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, -# UNDEFINED. -confidence=HIGH, - CONTROL_FLOW, - INFERENCE, - INFERENCE_FAILURE, - UNDEFINED - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then re-enable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of symbolic message names to ignore for Mixin members. -ignored-checks-for-mixins=no-member, - not-async-context-manager, - not-context-manager, - attribute-defined-outside-init - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# Regex pattern to define which classes are considered mixins. -mixin-class-rgx=.*[Mm]ixin - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[SIMILARITIES] - -# Comments are removed from the similarity computation -ignore-comments=yes - -# Docstrings are removed from the similarity computation -ignore-docstrings=yes - -# Imports are removed from the similarity computation -ignore-imports=yes - -# Signatures are removed from the similarity computation -ignore-signatures=yes - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it work, -# install the 'python-enchant' package. -spelling-dict= - -# List of comma separated words that should be considered directives if they -# appear at the beginning of a comment and should not be checked. -spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no - -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when caught. -overgeneral-exceptions=BaseException, - Exception - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules= - -# Output a graph (.gv or any supported image format) of external dependencies -# to the given file (report RP0402 must not be disabled). -ext-import-graph= - -# Output a graph (.gv or any supported image format) of all (i.e. internal and -# external) dependencies to the given file (report RP0402 must not be -# disabled). -import-graph= - -# Output a graph (.gv or any supported image format) of internal dependencies -# to the given file (report RP0402 must not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of names allowed to shadow builtins -allowed-redefined-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[DESIGN] - -# List of regular expressions of class ancestor names to ignore when counting -# public methods (see R0903) -exclude-too-few-public-methods= - -# List of qualified class names to ignore when counting class parents (see -# R0901) -ignored-parents= - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. If left empty, argument names will be checked with the set -# naming style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. If left empty, attribute names will be checked with the set naming -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. If left empty, class attribute names will be checked -# with the set naming style. -#class-attribute-rgx= - -# Naming style matching correct class constant names. -class-const-naming-style=UPPER_CASE - -# Regular expression matching correct class constant names. Overrides class- -# const-naming-style. If left empty, class constant names will be checked with -# the set naming style. -#class-const-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. If left empty, class names will be checked with the set naming style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. If left empty, constant names will be checked with the set naming -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. If left empty, function names will be checked with the set -# naming style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - df, - f, - e, - x, - y, - ax, - n, - a, - b, - px, - py, - dx, - dy, - yx, - yx, - xd, - yd, - sx, - yp, - p, - _ - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. If left empty, inline iteration names will be checked -# with the set naming style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. If left empty, method names will be checked with the set naming style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. If left empty, module names will be checked with the set naming style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Regular expression matching correct type variable names. If left empty, type -# variable names will be checked with the set naming style. -#typevar-rgx= - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. If left empty, variable names will be checked with the set -# naming style. -#variable-rgx= - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit,argparse.parse_error - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expression of note tags to take in consideration. -notes-rgx= - - -[CLASSES] - -# Warn about protected attribute access inside special methods -check-protected-access-in-special-methods=no - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls diff --git a/Dockerfile b/Dockerfile index 39df9d8..ce6ec5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,22 @@ # syntax=docker/dockerfile:1 -FROM python:3.8-slim-buster as base +FROM python:3.10-slim-buster as base # Setup ENV variables here (if needed in the future) FROM base as python-deps -# Install pipenv -RUN pip3 install pipenv +# Install pipx and poetry +RUN pip3 install --user pipx +ENV PATH=/root/.local/bin:$PATH +RUN pipx install poetry==1.4.2 +ENV PATH=/root/.local/pipx/venvs/poetry/bin:$PATH # Install python dependencies in /.venv -COPY Pipfile . -COPY Pipfile.lock . -RUN PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy +COPY pyproject.toml . +COPY poetry.lock . +RUN poetry config virtualenvs.in-project true +RUN poetry install --only main FROM base diff --git a/Pipfile b/Pipfile deleted file mode 100644 index d07b14a..0000000 --- a/Pipfile +++ /dev/null @@ -1,32 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[scripts] -start = "python3 -m kipubot" -dev = "watchfiles 'python3 -m kipubot' kipubot" -lint = "pylint kipubot" -test = "pytest" -test_hot = "watchfiles 'pytest' kipubot tests" - -[packages] -pandas = "*" -matplotlib = "*" -scipy = "*" -python-dotenv = "*" -python-telegram-bot = ">=20.0a0" -psycopg = ">=3" -openpyxl = "*" -pytz = "*" -uncertainties = "*" - -[dev-packages] -watchfiles="*" -autopep8 = "*" -pylint = "*" -pytype = "*" -pytest = "*" - -[requires] -python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 381f50f..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,1004 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "b86aa6abec32c723b4a5ccbd509740c8f6bc65601e69f08aa037354ec28d629a" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.8" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "anyio": { - "hashes": [ - "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", - "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.6.1" - }, - "apscheduler": { - "hashes": [ - "sha256:65e6574b6395498d371d045f2a8a7e4f7d50c6ad21ef7313d15b1c7cf20df1e3", - "sha256:ddc25a0ddd899de44d7f451f4375fb971887e65af51e41e5dcf681f59b8b2c9a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==3.9.1" - }, - "backports.zoneinfo": { - "hashes": [ - "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf", - "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328", - "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546", - "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6", - "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570", - "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9", - "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7", - "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987", - "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722", - "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582", - "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc", - "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b", - "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1", - "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08", - "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac", - "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2" - ], - "markers": "python_version < '3.9'", - "version": "==0.2.1" - }, - "cachetools": { - "hashes": [ - "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757", - "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db" - ], - "markers": "python_version ~= '3.7'", - "version": "==5.2.0" - }, - "certifi": { - "hashes": [ - "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", - "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.6.15" - }, - "cycler": { - "hashes": [ - "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3", - "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f" - ], - "markers": "python_version >= '3.6'", - "version": "==0.11.0" - }, - "et-xmlfile": { - "hashes": [ - "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c", - "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada" - ], - "markers": "python_version >= '3.6'", - "version": "==1.1.0" - }, - "fonttools": { - "hashes": [ - "sha256:9a1c52488045cd6c6491fd07711a380f932466e317cb8e016fc4e99dc7eac2f0", - "sha256:d73f25b283cd8033367451122aa868a23de0734757a01984e4b30b18b9050c72" - ], - "markers": "python_version >= '3.7'", - "version": "==4.34.4" - }, - "future": { - "hashes": [ - "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.18.2" - }, - "h11": { - "hashes": [ - "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", - "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" - ], - "markers": "python_version >= '3.6'", - "version": "==0.12.0" - }, - "httpcore": { - "hashes": [ - "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6", - "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b" - ], - "markers": "python_version >= '3.7'", - "version": "==0.15.0" - }, - "httpx": { - "hashes": [ - "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b", - "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef" - ], - "markers": "python_version >= '3.7'", - "version": "==0.23.0" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "kiwisolver": { - "hashes": [ - "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b", - "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166", - "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c", - "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c", - "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0", - "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c", - "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6", - "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004", - "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf", - "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac", - "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766", - "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6", - "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d", - "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191", - "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d", - "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f", - "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454", - "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8", - "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de", - "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a", - "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9", - "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3", - "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32", - "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938", - "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1", - "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d", - "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824", - "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b", - "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd", - "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3", - "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae", - "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597", - "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955", - "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a", - "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea", - "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede", - "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408", - "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871", - "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29", - "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897", - "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0", - "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09", - "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c" - ], - "markers": "python_version >= '3.7'", - "version": "==1.4.4" - }, - "matplotlib": { - "hashes": [ - "sha256:0bcdfcb0f976e1bac6721d7d457c17be23cf7501f977b6a38f9d38a3762841f7", - "sha256:1e64ac9be9da6bfff0a732e62116484b93b02a0b4d4b19934fb4f8e7ad26ad6a", - "sha256:22227c976ad4dc8c5a5057540421f0d8708c6560744ad2ad638d48e2984e1dbc", - "sha256:2886cc009f40e2984c083687251821f305d811d38e3df8ded414265e4583f0c5", - "sha256:2e6d184ebe291b9e8f7e78bbab7987d269c38ea3e062eace1fe7d898042ef804", - "sha256:3211ba82b9f1518d346f6309df137b50c3dc4421b4ed4815d1d7eadc617f45a1", - "sha256:339cac48b80ddbc8bfd05daae0a3a73414651a8596904c2a881cfd1edb65f26c", - "sha256:35a8ad4dddebd51f94c5d24bec689ec0ec66173bf614374a1244c6241c1595e0", - "sha256:3b4fa56159dc3c7f9250df88f653f085068bcd32dcd38e479bba58909254af7f", - "sha256:43e9d3fa077bf0cc95ded13d331d2156f9973dce17c6f0c8b49ccd57af94dbd9", - "sha256:57f1b4e69f438a99bb64d7f2c340db1b096b41ebaa515cf61ea72624279220ce", - "sha256:5c096363b206a3caf43773abebdbb5a23ea13faef71d701b21a9c27fdcef72f4", - "sha256:6bb93a0492d68461bd458eba878f52fdc8ac7bdb6c4acdfe43dba684787838c2", - "sha256:6ea6aef5c4338e58d8d376068e28f80a24f54e69f09479d1c90b7172bad9f25b", - "sha256:6fe807e8a22620b4cd95cfbc795ba310dc80151d43b037257250faf0bfcd82bc", - "sha256:73dd93dc35c85dece610cca8358003bf0760d7986f70b223e2306b4ea6d1406b", - "sha256:839d47b8ead7ad9669aaacdbc03f29656dc21f0d41a6fea2d473d856c39c8b1c", - "sha256:874df7505ba820e0400e7091199decf3ff1fde0583652120c50cd60d5820ca9a", - "sha256:879c7e5fce4939c6aa04581dfe08d57eb6102a71f2e202e3314d5fbc072fd5a0", - "sha256:94ff86af56a3869a4ae26a9637a849effd7643858a1a04dd5ee50e9ab75069a7", - "sha256:99482b83ebf4eb6d5fc6813d7aacdefdd480f0d9c0b52dcf9f1cc3b2c4b3361a", - "sha256:9ab29589cef03bc88acfa3a1490359000c18186fc30374d8aa77d33cc4a51a4a", - "sha256:9befa5954cdbc085e37d974ff6053da269474177921dd61facdad8023c4aeb51", - "sha256:a206a1b762b39398efea838f528b3a6d60cdb26fe9d58b48265787e29cd1d693", - "sha256:ab8d26f07fe64f6f6736d635cce7bfd7f625320490ed5bfc347f2cdb4fae0e56", - "sha256:b28de401d928890187c589036857a270a032961411934bdac4cf12dde3d43094", - "sha256:b428076a55fb1c084c76cb93e68006f27d247169f056412607c5c88828d08f88", - "sha256:bf618a825deb6205f015df6dfe6167a5d9b351203b03fab82043ae1d30f16511", - "sha256:c995f7d9568f18b5db131ab124c64e51b6820a92d10246d4f2b3f3a66698a15b", - "sha256:cd45a6f3e93a780185f70f05cf2a383daed13c3489233faad83e81720f7ede24", - "sha256:d2484b350bf3d32cae43f85dcfc89b3ed7bd2bcd781ef351f93eb6fb2cc483f9", - "sha256:d62880e1f60e5a30a2a8484432bcb3a5056969dc97258d7326ad465feb7ae069", - "sha256:dacddf5bfcec60e3f26ec5c0ae3d0274853a258b6c3fc5ef2f06a8eb23e042be", - "sha256:f3840c280ebc87a48488a46f760ea1c0c0c83fcf7abbe2e6baf99d033fd35fd8", - "sha256:f814504e459c68118bf2246a530ed953ebd18213dc20e3da524174d84ed010b2" - ], - "index": "pypi", - "version": "==3.5.3" - }, - "numpy": { - "hashes": [ - "sha256:17e5226674f6ea79e14e3b91bfbc153fdf3ac13f5cc54ee7bc8fdbe820a32da0", - "sha256:2bd879d3ca4b6f39b7770829f73278b7c5e248c91d538aab1e506c628353e47f", - "sha256:4f41f5bf20d9a521f8cab3a34557cd77b6f205ab2116651f12959714494268b0", - "sha256:5593f67e66dea4e237f5af998d31a43e447786b2154ba1ad833676c788f37cde", - "sha256:5e28cd64624dc2354a349152599e55308eb6ca95a13ce6a7d5679ebff2962913", - "sha256:633679a472934b1c20a12ed0c9a6c9eb167fbb4cb89031939bfd03dd9dbc62b8", - "sha256:806970e69106556d1dd200e26647e9bee5e2b3f1814f9da104a943e8d548ca38", - "sha256:806cc25d5c43e240db709875e947076b2826f47c2c340a5a2f36da5bb10c58d6", - "sha256:8247f01c4721479e482cc2f9f7d973f3f47810cbc8c65e38fd1bbd3141cc9842", - "sha256:8ebf7e194b89bc66b78475bd3624d92980fca4e5bb86dda08d677d786fefc414", - "sha256:8ecb818231afe5f0f568c81f12ce50f2b828ff2b27487520d85eb44c71313b9e", - "sha256:8f9d84a24889ebb4c641a9b99e54adb8cab50972f0166a3abc14c3b93163f074", - "sha256:909c56c4d4341ec8315291a105169d8aae732cfb4c250fbc375a1efb7a844f8f", - "sha256:9b83d48e464f393d46e8dd8171687394d39bc5abfe2978896b77dc2604e8635d", - "sha256:ac987b35df8c2a2eab495ee206658117e9ce867acf3ccb376a19e83070e69418", - "sha256:b78d00e48261fbbd04aa0d7427cf78d18401ee0abd89c7559bbf422e5b1c7d01", - "sha256:b8b97a8a87cadcd3f94659b4ef6ec056261fa1e1c3317f4193ac231d4df70215", - "sha256:bd5b7ccae24e3d8501ee5563e82febc1771e73bd268eef82a1e8d2b4d556ae66", - "sha256:bdc02c0235b261925102b1bd586579b7158e9d0d07ecb61148a1799214a4afd5", - "sha256:be6b350dfbc7f708d9d853663772a9310783ea58f6035eec649fb9c4371b5389", - "sha256:c403c81bb8ffb1c993d0165a11493fd4bf1353d258f6997b3ee288b0a48fce77", - "sha256:cf8c6aed12a935abf2e290860af8e77b26a042eb7f2582ff83dc7ed5f963340c", - "sha256:d98addfd3c8728ee8b2c49126f3c44c703e2b005d4a95998e2167af176a9e722", - "sha256:dc76bca1ca98f4b122114435f83f1fcf3c0fe48e4e6f660e07996abf2f53903c", - "sha256:dec198619b7dbd6db58603cd256e092bcadef22a796f778bf87f8592b468441d", - "sha256:df28dda02c9328e122661f399f7655cdcbcf22ea42daa3650a26bce08a187450", - "sha256:e603ca1fb47b913942f3e660a15e55a9ebca906857edfea476ae5f0fe9b457d5", - "sha256:ecfdd68d334a6b97472ed032b5b37a30d8217c097acfff15e8452c710e775524" - ], - "markers": "python_version >= '3.8'", - "version": "==1.23.2" - }, - "openpyxl": { - "hashes": [ - "sha256:0ab6d25d01799f97a9464630abacbb34aafecdcaa0ef3cba6d6b3499867d0355", - "sha256:e47805627aebcf860edb4edf7987b1309c1b3632f3750538ed962bbcc3bd7449" - ], - "index": "pypi", - "version": "==3.0.10" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "pandas": { - "hashes": [ - "sha256:07238a58d7cbc8a004855ade7b75bbd22c0db4b0ffccc721556bab8a095515f6", - "sha256:0daf876dba6c622154b2e6741f29e87161f844e64f84801554f879d27ba63c0d", - "sha256:16ad23db55efcc93fa878f7837267973b61ea85d244fc5ff0ccbcfa5638706c5", - "sha256:1d9382f72a4f0e93909feece6fef5500e838ce1c355a581b3d8f259839f2ea76", - "sha256:24ea75f47bbd5574675dae21d51779a4948715416413b30614c1e8b480909f81", - "sha256:2893e923472a5e090c2d5e8db83e8f907364ec048572084c7d10ef93546be6d1", - "sha256:2ff7788468e75917574f080cd4681b27e1a7bf36461fe968b49a87b5a54d007c", - "sha256:41fc406e374590a3d492325b889a2686b31e7a7780bec83db2512988550dadbf", - "sha256:48350592665ea3cbcd07efc8c12ff12d89be09cd47231c7925e3b8afada9d50d", - "sha256:605d572126eb4ab2eadf5c59d5d69f0608df2bf7bcad5c5880a47a20a0699e3e", - "sha256:6dfbf16b1ea4f4d0ee11084d9c026340514d1d30270eaa82a9f1297b6c8ecbf0", - "sha256:6f803320c9da732cc79210d7e8cc5c8019aad512589c910c66529eb1b1818230", - "sha256:721a3dd2f06ef942f83a819c0f3f6a648b2830b191a72bbe9451bcd49c3bd42e", - "sha256:755679c49460bd0d2f837ab99f0a26948e68fa0718b7e42afbabd074d945bf84", - "sha256:78b00429161ccb0da252229bcda8010b445c4bf924e721265bec5a6e96a92e92", - "sha256:958a0588149190c22cdebbc0797e01972950c927a11a900fe6c2296f207b1d6f", - "sha256:a3924692160e3d847e18702bb048dc38e0e13411d2b503fecb1adf0fcf950ba4", - "sha256:d51674ed8e2551ef7773820ef5dab9322be0828629f2cbf8d1fc31a0c4fed640", - "sha256:d5ebc990bd34f4ac3c73a2724c2dcc9ee7bf1ce6cf08e87bb25c6ad33507e318", - "sha256:d6c0106415ff1a10c326c49bc5dd9ea8b9897a6ca0c8688eb9c30ddec49535ef", - "sha256:e48fbb64165cda451c06a0f9e4c7a16b534fcabd32546d531b3c240ce2844112" - ], - "index": "pypi", - "version": "==1.4.3" - }, - "pillow": { - "hashes": [ - "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927", - "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14", - "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc", - "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58", - "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60", - "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76", - "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c", - "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac", - "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490", - "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1", - "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f", - "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d", - "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f", - "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069", - "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402", - "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885", - "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e", - "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be", - "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8", - "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff", - "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da", - "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004", - "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f", - "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20", - "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d", - "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c", - "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544", - "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9", - "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3", - "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04", - "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c", - "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5", - "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4", - "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb", - "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4", - "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c", - "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467", - "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e", - "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421", - "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b", - "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8", - "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb", - "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3", - "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf", - "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1", - "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a", - "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28", - "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0", - "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1", - "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8", - "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd", - "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4", - "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8", - "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f", - "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013", - "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59", - "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc", - "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4" - ], - "markers": "python_version >= '3.7'", - "version": "==9.2.0" - }, - "psycopg": { - "hashes": [ - "sha256:0f5e18920ed978f5063e48acc5ca4389225db7d06a03090d2bbb7a0ec7a640b2", - "sha256:44ca63373c33957ca852fefa1940f8cc5d4c11493b7f6710b0ab250ff5abc50c" - ], - "index": "pypi", - "version": "==3.0.16" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, - "python-dateutil": { - "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" - }, - "python-dotenv": { - "hashes": [ - "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f", - "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938" - ], - "index": "pypi", - "version": "==0.20.0" - }, - "python-telegram-bot": { - "hashes": [ - "sha256:009522b9ce05dced9b729f7efa072507eac6d08023ee4af84fc560aef4e41e95", - "sha256:ebdcf70cadcc4a0be1f53ff0feb7a0b62604ae76813f2ed439bdcb7ffb77e9f9" - ], - "index": "pypi", - "version": "==20.0a2" - }, - "pytz": { - "hashes": [ - "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197", - "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5" - ], - "index": "pypi", - "version": "==2022.2.1" - }, - "pytz-deprecation-shim": { - "hashes": [ - "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6", - "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==0.1.0.post0" - }, - "rfc3986": { - "extras": [ - "idna2008" - ], - "hashes": [ - "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", - "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" - ], - "version": "==1.5.0" - }, - "scipy": { - "hashes": [ - "sha256:01c2015e132774feefe059d5354055fec6b751d7a7d70ad2cf5ce314e7426e2a", - "sha256:0424d1bbbfa51d5ddaa16d067fd593863c9f2fb7c6840c32f8a08a8832f8e7a4", - "sha256:10417935486b320d98536d732a58362e3d37e84add98c251e070c59a6bfe0863", - "sha256:12005d30894e4fe7b247f7233ba0801a341f887b62e2eb99034dd6f2a8a33ad6", - "sha256:16207622570af10f9e6a2cdc7da7a9660678852477adbcd056b6d1057a036fef", - "sha256:45f0d6c0d6e55582d3b8f5c58ad4ca4259a02affb190f89f06c8cc02e21bba81", - "sha256:5d1b9cf3771fd921f7213b4b886ab2606010343bb36259b544a816044576d69e", - "sha256:693b3fe2e7736ce0dbc72b4d933798eb6ca8ce51b8b934e3f547cc06f48b2afb", - "sha256:73b704c5eea9be811919cae4caacf3180dd9212d9aed08477c1d2ba14900a9de", - "sha256:79dd7876614fc2869bf5d311ef33962d2066ea888bc66c80fd4fa80f8772e5a9", - "sha256:7bad16b91918bf3288089a78a4157e04892ea6475fb7a1d9bcdf32c30c8a3dba", - "sha256:8d541db2d441ef87afb60c4a2addb00c3af281633602a4967e733ef4b7050504", - "sha256:8f2232c9d9119ec356240255a715a289b3a33be828c3e4abac11fd052ce15b1e", - "sha256:97a1f1e51ea30782d7baa8d0c52f72c3f9f05cb609cf1b990664231c5102bccd", - "sha256:adb6c438c6ef550e2bb83968e772b9690cb421f2c6073f9c2cb6af15ee538bc9", - "sha256:bb687d245b6963673c639f318eea7e875d1ba147a67925586abed3d6f39bb7d8", - "sha256:bd490f77f35800d5620f4d9af669e372d9a88db1f76ef219e1609cc4ecdd1a24", - "sha256:c0dfd7d2429452e7e94904c6a3af63cbaa3cf51b348bd9d35b42db7e9ad42791", - "sha256:d3a326673ac5afa9ef5613a61626b9ec15c8f7222b4ecd1ce0fd8fcba7b83c59", - "sha256:e2004d2a3c397b26ca78e67c9d320153a1a9b71ae713ad33f4a3a3ab3d79cc65", - "sha256:e2ac088ea4aa61115b96b47f5f3d94b3fa29554340b6629cd2bfe6b0521ee33b", - "sha256:f7c3c578ff556333f3890c2df6c056955d53537bb176698359088108af73a58f", - "sha256:fc58c3fcb8a724b703ffbc126afdca5a8353d4d5945d5c92db85617e165299e7" - ], - "index": "pypi", - "version": "==1.9.0" - }, - "setuptools": { - "hashes": [ - "sha256:d73f8cd714a1a6691f5eb5abeeacbf313242b7aa2f5eba93776542c1aad90c6f", - "sha256:fe9a97f68b064a6ddd4bacfb0b4b93a4c65a556d97ce906255540439d0c35cef" - ], - "markers": "python_version >= '3.7'", - "version": "==65.0.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "sniffio": { - "hashes": [ - "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", - "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" - ], - "markers": "python_version >= '3.5'", - "version": "==1.2.0" - }, - "tornado": { - "hashes": [ - "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca", - "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72", - "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23", - "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8", - "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b", - "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9", - "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13", - "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75", - "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac", - "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e", - "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b" - ], - "markers": "python_version >= '3.7'", - "version": "==6.2" - }, - "tzdata": { - "hashes": [ - "sha256:21f4f0d7241572efa7f7a4fdabb052e61b55dc48274e6842697ccdf5253e5451", - "sha256:c3119520447d68ef3eb8187a55a4f44fa455f30eb1b4238fa5691ba094f2b05b" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.2" - }, - "tzlocal": { - "hashes": [ - "sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745", - "sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7" - ], - "markers": "python_version >= '3.6'", - "version": "==4.2" - }, - "uncertainties": { - "hashes": [ - "sha256:4040ec64d298215531922a68fa1506dc6b1cb86cd7cca8eca848fcfe0f987151", - "sha256:80111e0839f239c5b233cb4772017b483a0b7a1573a581b92ab7746a35e6faab" - ], - "index": "pypi", - "version": "==3.1.7" - } - }, - "develop": { - "anyio": { - "hashes": [ - "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", - "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.6.1" - }, - "astroid": { - "hashes": [ - "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b", - "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==2.11.7" - }, - "attrs": { - "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" - ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" - }, - "autopep8": { - "hashes": [ - "sha256:6f09e90a2be784317e84dc1add17ebfc7abe3924239957a37e5040e27d812087", - "sha256:ca9b1a83e53a7fad65d731dc7a2a2d50aa48f43850407c59f6a1a306c4201142" - ], - "index": "pypi", - "version": "==1.7.0" - }, - "dill": { - "hashes": [ - "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302", - "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==0.3.5.1" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "importlab": { - "hashes": [ - "sha256:744bd75d4410744962d203bd1eb71a950b19e8fb8eb5f0b805461dc0a2da329b" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "isort": { - "hashes": [ - "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", - "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" - ], - "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", - "version": "==5.10.1" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7", - "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a", - "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c", - "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc", - "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f", - "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09", - "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442", - "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e", - "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029", - "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61", - "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb", - "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0", - "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35", - "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42", - "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1", - "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad", - "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443", - "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd", - "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9", - "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148", - "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38", - "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55", - "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36", - "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a", - "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b", - "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44", - "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6", - "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69", - "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4", - "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84", - "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de", - "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28", - "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c", - "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1", - "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8", - "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b", - "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb" - ], - "markers": "python_version >= '3.6'", - "version": "==1.7.1" - }, - "libcst": { - "hashes": [ - "sha256:0ca2771ff3cfdf1f148349f89fcae64afa365213ed5c2703a69a89319325d0c8", - "sha256:1e541ccfeebda1ae5f005fc120a5bf3e8ac9ccfda405ec3efd3df54fc4688ac3", - "sha256:214a9c4f4f90cd5b4bfa18e17877da4dd9a896821d9af9be86fa3effdc289b9b", - "sha256:234293aa8681a3d47fef1716c5622797a81cbe85a9381fe023815468cfe20eed", - "sha256:26f86535271eaefe84a99736875566a038449f92e1a2a61ea0b588d8359fbefd", - "sha256:27a37f2b459a8b51a41e260bd89c24ae41ab1d658f610c91650c79b1bbf27138", - "sha256:2f6766391d90472f036b88a95251c87d498ab068c377724f212ab0cc20509a68", - "sha256:31da97bc986dc3f7a97f7d431fa911932aaf716d2f8bcda947fc964afd3b57cd", - "sha256:3569d9901c18940632414fb7a0943bffd326db9f726a9c041664926820857815", - "sha256:3a2b7253cd2e3f0f8a3e23b5c2acb492811d865ef36e0816091c925f32b713d2", - "sha256:617f7fa2610a8c86cf22d8d03416f25391383d05bd0ad1ca8ef68023ddd6b4f6", - "sha256:71b2e2c5e33e53669c20de0853cecfac1ffb8657ee727ab8527140f39049b820", - "sha256:76fae68bd6b7ce069e267b3322c806b4305341cea78d161ae40e0ed641c8c660", - "sha256:8c6bd66a8be2ffad7b968d90dae86c62fd4739c0e011d71f3e76544a891ae743", - "sha256:95c52c2130531f6e726a3b077442cfd486975435fecf3db8224d43fba7b85099", - "sha256:a8f47d809df59fcd83058b777b86a300154ee3a1f1b0523a398a67b5f8affd4c", - "sha256:aa438131b7befc7e5a3cbadb5a7b1506305de5d62262ea0556add0152f40925e", - "sha256:bac76d69980bb3254f503f52128c256ef4d1bcbaabe4a17c3a9ebcd1fc0472c0", - "sha256:beb5347e46b419f782589da060e9300957e71d561aa5574309883b71f93c1dfe", - "sha256:c0d19de56aa733b4ef024527e3ce4896d4b0e9806889797f409ec24caa651a44", - "sha256:c3637fffe476c5b4ee2225c6474b83382518f2c1b2fe4771039e06bdd7835a4a", - "sha256:dc6f8965b6ca68d47e11321772887d81fa6fd8ea86e6ef87434ca2147de10747", - "sha256:f56565124c2541adee0634e411b2126b3f335306d19e91ed2bfe52efa698b219", - "sha256:fa618dc359663a0a097c633452b104c1ca93365da7a811e655c6944f6b323239" - ], - "markers": "python_version >= '3.7'", - "version": "==0.4.7" - }, - "mccabe": { - "hashes": [ - "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", - "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "networkx": { - "hashes": [ - "sha256:67fab04a955a73eb660fe7bf281b6fa71a003bc6e23a92d2f6227654c5223dbe", - "sha256:f151edac6f9b0cf11fecce93e236ac22b499bb9ff8d6f8393b9fef5ad09506cc" - ], - "markers": "python_version >= '3.8'", - "version": "==2.8.3" - }, - "ninja": { - "hashes": [ - "sha256:0560eea57199e41e86ac2c1af0108b63ae77c3ca4d05a9425a750e908135935a", - "sha256:21a1d84d4c7df5881bfd86c25cce4cf7af44ba2b8b255c57bc1c434ec30a2dfc", - "sha256:279836285975e3519392c93c26e75755e8a8a7fafec9f4ecbb0293119ee0f9c6", - "sha256:29570a18d697fc84d361e7e6330f0021f34603ae0fcb0ef67ae781e9814aae8d", - "sha256:5ea785bf6a15727040835256577239fa3cf5da0d60e618c307aa5efc31a1f0ce", - "sha256:688167841b088b6802e006f911d911ffa925e078c73e8ef2f88286107d3204f8", - "sha256:6bd76a025f26b9ae507cf8b2b01bb25bb0031df54ed685d85fc559c411c86cf4", - "sha256:740d61fefb4ca13573704ee8fe89b973d40b8dc2a51aaa4e9e68367233743bb6", - "sha256:840a0b042d43a8552c4004966e18271ec726e5996578f28345d9ce78e225b67e", - "sha256:84be6f9ec49f635dc40d4b871319a49fa49b8d55f1d9eae7cd50d8e57ddf7a85", - "sha256:9ca8dbece144366d5f575ffc657af03eb11c58251268405bc8519d11cf42f113", - "sha256:cc8b31b5509a2129e4d12a35fc21238c157038022560aaf22e49ef0a77039086", - "sha256:d5e0275d28997a750a4f445c00bdd357b35cc334c13cdff13edf30e544704fbd", - "sha256:e1b86ad50d4e681a7dbdff05fc23bb52cb773edb90bc428efba33fa027738408" - ], - "version": "==1.10.2.3" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "platformdirs": { - "hashes": [ - "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", - "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" - ], - "markers": "python_version >= '3.7'", - "version": "==2.5.2" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pycodestyle": { - "hashes": [ - "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", - "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b" - ], - "markers": "python_version >= '3.6'", - "version": "==2.9.1" - }, - "pydot": { - "hashes": [ - "sha256:248081a39bcb56784deb018977e428605c1c758f10897a339fce1dd728ff007d", - "sha256:66c98190c65b8d2e2382a441b4c0edfdb4f4c025ef9cb9874de478fb0793a451" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.2" - }, - "pylint": { - "hashes": [ - "sha256:487ce2192eee48211269a0e976421f334cf94de1806ca9d0a99449adcdf0285e", - "sha256:fabe30000de7d07636d2e82c9a518ad5ad7908590fe135ace169b44839c15f90" - ], - "index": "pypi", - "version": "==2.14.5" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, - "pytest": { - "hashes": [ - "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", - "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" - ], - "index": "pypi", - "version": "==7.1.2" - }, - "pytype": { - "hashes": [ - "sha256:02fde02909ddcfe96ca1779bde10a126eb96eabb4fa8f2a428454bb1d9e359e9", - "sha256:22c57bf362e2fa4aa7d443f3e0b723ed0fba07d984c90a3304e867a0c2372cc4", - "sha256:34b98bb1b4fd876d40c4eab99881780b9d88b1057766031a3bcc8dc8517d26be", - "sha256:4746f39aa25063990d207a159e5143adffd3be112ee057116180b9dc4ad7f4fc", - "sha256:5a863dff6280b86f1a5f7f58acef9d77d86adff52866a25b893bbd8be3e9d103", - "sha256:602ad4fde8f6ac5031787f82caa6f7dffa04c66a00ef5543e3657b98803a47a1", - "sha256:6b45e4bb290904702c1b23ffd9345696432d6e2a8abe330e2077442e745d2359", - "sha256:6d5c72c3110a2642d42c2324f78c7200fe791b52079152e85eea266361403025", - "sha256:779805511943f17cb84a4516b08057943188e4934820fecdbe9e25eb1d3e6af1", - "sha256:7b1c8d5080e5e2990699d39cbab8abf99694f61a634fed8bfeaa7d2f78942db6", - "sha256:9930a837920fcb947be551cb0534678f8ada3817ca8680d18f71c8620687b662", - "sha256:ab9e4186e4060c0e2dc3e8495592c5af0ef2ef4a4a760004fe286d26da233405", - "sha256:de3e18e1274d8751734ead631a2cb0ade869470f58d394498f01293ccd7f576b" - ], - "index": "pypi", - "version": "==2022.8.3" - }, - "pyyaml": { - "hashes": [ - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0" - }, - "setuptools": { - "hashes": [ - "sha256:d73f8cd714a1a6691f5eb5abeeacbf313242b7aa2f5eba93776542c1aad90c6f", - "sha256:fe9a97f68b064a6ddd4bacfb0b4b93a4c65a556d97ce906255540439d0c35cef" - ], - "markers": "python_version >= '3.7'", - "version": "==65.0.0" - }, - "sniffio": { - "hashes": [ - "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", - "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" - ], - "markers": "python_version >= '3.5'", - "version": "==1.2.0" - }, - "tabulate": { - "hashes": [ - "sha256:0ba055423dbaa164b9e456abe7920c5e8ed33fcc16f6d1b2f2d152c8e1e8b4fc", - "sha256:436f1c768b424654fce8597290d2764def1eea6a77cfa5c33be00b1bc0f4f63d", - "sha256:6c57f3f3dd7ac2782770155f3adb2db0b1a269637e42f27599925e64b114f519" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.8.10" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.1" - }, - "tomlkit": { - "hashes": [ - "sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c", - "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83" - ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==0.11.4" - }, - "typing-extensions": { - "hashes": [ - "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", - "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" - ], - "markers": "python_version < '3.10'", - "version": "==4.3.0" - }, - "typing-inspect": { - "hashes": [ - "sha256:047d4097d9b17f46531bf6f014356111a1b6fb821a24fe7ac909853ca2a782aa", - "sha256:3cd7d4563e997719a710a3bfe7ffb544c6b72069b6812a02e9b414a8fa3aaa6b", - "sha256:b1f56c0783ef0f25fb064a01be6e5407e54cf4a4bf4f3ba3fe51e0bd6dcea9e5" - ], - "version": "==0.7.1" - }, - "watchfiles": { - "hashes": [ - "sha256:1e41c8b4bf3e07c18aa51775b36b718830fa727929529a7d6e5b38cf845a06b4", - "sha256:22af3b915f928ef59d427d7228668f87ac8054ed8200808c73fbcaa4f82d5572", - "sha256:2a3debb19912072799d7ca53e99fc5f090f77948f5601392623b2a416b4c86be", - "sha256:35f3e411822e14a35f2ef656535aad4e6e79670d6b6ef8e53db958e28916b1fe", - "sha256:44c6aff58b8a70a26431737e483a54e8e224279b21873388571ed184fe7c91a7", - "sha256:4a6a1ac96edf5bc3f8e36f4462fc1daad0ec3769ff2adb920571e120e37c91c5", - "sha256:5741246ae399a03395aa5ee35480083a4f29d58ffd41dd3395594f8805f8cdbc", - "sha256:70159e759f52b65a50c498182dece80364bfd721e839c254c328cbc7a1716616", - "sha256:75a4b9cec1b1c337ea77d4428b29861553d6bf8179923b1bc7e825e217460e2c", - "sha256:91d1b2d0cf060e5222a930a3e2f40f6577da1d18c085c32741b98a128dc1e72c", - "sha256:a8a1809bf910672aa0b7ed6e6045d4fc2cf1e0718b99bc443ef17faa5697b68a", - "sha256:aed7575e24434c8fec2f2bbb0cecb1521ea1240234d9108db7915a3424d92394", - "sha256:b2c7ad91a867dd688b9a12097dd6a4f89397b43fccee871152aa67197cc94398", - "sha256:baa6d0c1c5140e1dcf6ff802dd7b09fcd95b358e50d42fabc83d83f719451c54", - "sha256:c9a7a6dc63684ff5ba11f0be0e64f744112c3c7a0baf4ec8f6794f9a6257d21e", - "sha256:cd7d2fd9a8f28066edc8db5278f3632eb94d10596af760fa0601631f32b1a41e", - "sha256:e939a2693404ac11e055f9d1237db8ad7635e2185a6143bde00116e691ea2983", - "sha256:f91035a273001390093a09e52274a34695b0d15ee8736183b640bbc3b8a432ab" - ], - "index": "pypi", - "version": "==0.16.1" - }, - "wrapt": { - "hashes": [ - "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3", - "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b", - "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4", - "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2", - "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656", - "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3", - "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff", - "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310", - "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a", - "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57", - "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069", - "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383", - "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe", - "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87", - "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d", - "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b", - "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907", - "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f", - "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0", - "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28", - "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1", - "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853", - "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc", - "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3", - "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3", - "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164", - "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1", - "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c", - "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1", - "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7", - "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1", - "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320", - "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed", - "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1", - "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248", - "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c", - "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456", - "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77", - "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef", - "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1", - "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7", - "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86", - "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4", - "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d", - "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d", - "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8", - "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5", - "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471", - "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00", - "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68", - "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3", - "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d", - "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735", - "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d", - "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569", - "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7", - "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59", - "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5", - "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb", - "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b", - "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f", - "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462", - "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015", - "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.14.1" - } - } -} diff --git a/README.md b/README.md index 9c0554d..6dbf3fa 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,14 @@ Add the bot on [Telegram](https://t.me/NoPainNoGraphbot) into your group and enj ### Docker (Recommended) -1. Set `PYTHONPATH=${PYTHONPATH}:kipubot` in .env -2. Set `BOT_TOKEN` in .env -3. `docker-compose -f docker-compose.dev.yml up` -### Pipenv +1. Set `BOT_TOKEN` in .env +2. `docker compose -f docker-compose.dev.yml up` + +### Poetry -Using [pipenv](https://pipenv.pypa.io/en/latest/): +Using [poetry](https://python-poetry.org/docs/): -1. Set `PYTHONPATH=${PYTHONPATH}:kipubot` in .env 2. Set `BOT_TOKEN` in .env 3. Set `DATABASE_URL` in .env -4. pipenv install --dev -5. pipenv run dev +4. poetry install +5. poetry run poe dev diff --git a/dev.Dockerfile b/dev.Dockerfile index 5dad4f8..724ad9b 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -1,20 +1,23 @@ # syntax=docker/dockerfile:1 -FROM python:3.8 as base +FROM python:3.10 as base # Setup ENV variables here (if needed in the future) FROM base as python-deps -# Install pipenv -RUN pip3 install pipenv +# Install pipx and poetry +RUN pip3 install --user pipx +ENV PATH=/root/.local/bin:$PATH +RUN pipx install poetry==1.4.2 +ENV PATH=/root/.local/pipx/venvs/poetry/bin:$PATH # Install python dependencies in /.venv WORKDIR /bot -COPY Pipfile . -COPY Pipfile.lock . -RUN pipenv install --dev +COPY pyproject.toml . +COPY poetry.lock . +RUN poetry install COPY . . # Run the app -CMD [ "pipenv", "run", "dev" ] +CMD [ "poetry", "run", "poe", "dev" ] diff --git a/kipubot/__init__.py b/kipubot/__init__.py index 57019de..1b8cdb7 100644 --- a/kipubot/__init__.py +++ b/kipubot/__init__.py @@ -8,38 +8,33 @@ # LOGGING CONFIG logging.basicConfig( - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - level=logging.INFO + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # ENV VARIABLES -BOT_TOKEN = os.getenv('BOT_TOKEN', default=None) -DATABASE_URL = os.getenv('DATABASE_URL', default=None) -DEVELOPER_CHAT_ID = os.getenv('DEVELOPER_CHAT_ID', default=None) -MODE = os.getenv('MODE', default=None) +BOT_TOKEN = os.getenv("BOT_TOKEN", default=None) +DATABASE_URL = os.getenv("DATABASE_URL", default=None) +DEVELOPER_CHAT_ID = os.getenv("DEVELOPER_CHAT_ID", default=None) +MODE = os.getenv("MODE", default=None) if BOT_TOKEN is None and MODE != "TEST": - logging.error('Bot token is not set!') + logging.error("Bot token is not set!") sys.exit(1) if DATABASE_URL is None: - logging.error('Database URL is not set!') + logging.error("Database URL is not set!") sys.exit(1) if DEVELOPER_CHAT_ID is None: - logging.warning('Developer chat ID is not set!') + logging.warning("Developer chat ID is not set!") # INITIALIZE DB AND CREATE TABLES IF THEY DON'T EXIST kipubot.db._init_db(DATABASE_URL) # pylint: disable=protected-access # CHECK/CREATE DATA DIRECTORY -if not os.path.exists('data'): - logging.info('Creating ./data/ directory...') - os.mkdir('data') - -__all__ = ( - 'BOT_TOKEN', - 'DATABASE_URL', - 'DEVELOPER_CHAT_ID' -) +if not os.path.exists("data"): + logging.info("Creating ./data/ directory...") + os.mkdir("data") + +__all__ = ("BOT_TOKEN", "DATABASE_URL", "DEVELOPER_CHAT_ID") diff --git a/kipubot/bot.py b/kipubot/bot.py index 48f8270..541cc7f 100644 --- a/kipubot/bot.py +++ b/kipubot/bot.py @@ -3,20 +3,23 @@ from telegram.ext import ApplicationBuilder, PicklePersistence from kipubot import BOT_TOKEN -from kipubot.handlers import (start_handler, moro_handler, excel_file_handler, - bot_added_handler, winner_handler, graph_handler, - expected_value_handler, raffle_setup_handler, no_dm_handler, - error_handler) +from kipubot.handlers import ( + start_handler, + moro_handler, + excel_file_handler, + bot_added_handler, + winner_handler, + graph_handler, + expected_value_handler, + raffle_setup_handler, + no_dm_handler, + error_handler, +) def main() -> None: - persistence = PicklePersistence(filepath='data/.pkl') - app = ( - ApplicationBuilder() - .token(BOT_TOKEN) - .persistence(persistence) - .build() - ) + persistence = PicklePersistence(filepath="data/.pkl") + app = ApplicationBuilder().token(BOT_TOKEN).persistence(persistence).build() app.add_handler(start_handler) diff --git a/kipubot/constants.py b/kipubot/constants.py index b64709d..2025616 100644 --- a/kipubot/constants.py +++ b/kipubot/constants.py @@ -1,51 +1,59 @@ # CONSTANTS -EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' +EXCEL_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" STRINGS = { - 'moro_prompt': 'Join the raffles in %(chat_title)s by typing /moro or /hello!', - 'not_winner': 'You are not the winner in any chat! ❌', - 'choose_channel': 'Choose channel:', - 'chat_button': '💬 %(chat_title)s', - 'cancel_button': '❌ Cancel', - 'no_raffle': 'No raffle data found in %(chat_title)s!', - 'raffle_db_error': ('Error getting raffle data from database!\n\n' + - 'Perhaps one is not setup yet for this chat? 🤔'), - 'no_entries': 'No raffle entries yet in %(chat_title)s!', - 'no_data': 'No data found for %(chat_title)s!', - 'moro': 'Registered %(username)s in %(chat_title)s!', - 'double_moro': 'You are already registered in %(chat_title)s!', - 'no_dm_warn': 'This command is not usable in private messages!', - 'cancelled': 'Cancelled! ❌', - 'unknown_error': 'Unknown error, please try again later! ❌', - 'server_error': ('Server error, please try again later! ❌\n\n' + - 'The administration has been contacted.'), - 'invalid_file': 'Invalid Excel file! ❌\n\n/start for instructions', - 'new_raffle_button': '🆕 Create a new raffle!', - 'update_raffle_button': '🔄 Update existing raffle!', - 'raffle_setup_base': ('📝 Raffle setup for %(chat_title)s\n' + - '=============================\n\n'), - 'raffle_setup_update_or_new': ('Found existing raffle.\n' + - 'Do you want update it or create a new one?'), - 'raffle_setup_new': ('No existing raffle found.\n' + - 'Do you want to create a new one?'), - 'updated_raffle': 'Updated raffle data in %(chat_title)s! 🔄', - 'raffle_setup_start_date': 'Start date set to %(start_date)s!\n', - 'raffle_setup_end_date': 'End date set to %(end_date)s!\n\n', - 'raffle_setup_fee': 'Fee set to %(fee)s €!\n\n', - 'end_date_before_start': 'End date cannot be before start date! ❌', - 'negative_fee': 'Fee cannot be negative! ❌', - 'confirm_button': '✔️ Confirm', - 'finish_raffle_button': '✔️ Finish raffle', - 'raffle_confirmation': 'Succesfully setup new raffle in %(chat_title)s! ✔️', - 'raffle_created_chat': 'New raffle created by @%(username)s! ✔️', - 'raffle_updated_chat': 'Raffle updated by @%(username)s! ✔️', - 'timed_out': 'Timed out! 🕐', - 'start_prompt': ('Use the given commands or send me an Excel-file ' + - 'from MobilePay if you\'re the host of a raffle!'), - 'invalid_winner_usage': 'Please use the format /winner @username', - 'forbidden_command': 'You are not allowed to use this command! ❌', - 'user_not_found': ('Error getting user!\n' + - 'Perhaps they haven\'t /moro ed? 🤔'), - 'already_winner': 'You are already the winner!', - 'winner_confirmation': '%(username)s is the new winner!' + "moro_prompt": "Join the raffles in %(chat_title)s by typing /moro or /hello!", + "not_winner": "You are not the winner in any chat! ❌", + "choose_channel": "Choose channel:", + "chat_button": "💬 %(chat_title)s", + "cancel_button": "❌ Cancel", + "no_raffle": "No raffle data found in %(chat_title)s!", + "raffle_db_error": ( + "Error getting raffle data from database!\n\n" + + "Perhaps one is not setup yet for this chat? 🤔" + ), + "no_entries": "No raffle entries yet in %(chat_title)s!", + "no_data": "No data found for %(chat_title)s!", + "moro": "Registered %(username)s in %(chat_title)s!", + "double_moro": "You are already registered in %(chat_title)s!", + "no_dm_warn": "This command is not usable in private messages!", + "cancelled": "Cancelled! ❌", + "unknown_error": "Unknown error, please try again later! ❌", + "server_error": ( + "Server error, please try again later! ❌\n\n" + + "The administration has been contacted." + ), + "invalid_file": "Invalid Excel file! ❌\n\n/start for instructions", + "new_raffle_button": "🆕 Create a new raffle!", + "update_raffle_button": "🔄 Update existing raffle!", + "raffle_setup_base": ( + "📝 Raffle setup for %(chat_title)s\n" + "=============================\n\n" + ), + "raffle_setup_update_or_new": ( + "Found existing raffle.\n" + "Do you want update it or create a new one?" + ), + "raffle_setup_new": ( + "No existing raffle found.\n" + "Do you want to create a new one?" + ), + "updated_raffle": "Updated raffle data in %(chat_title)s! 🔄", + "raffle_setup_start_date": "Start date set to %(start_date)s!\n", + "raffle_setup_end_date": "End date set to %(end_date)s!\n\n", + "raffle_setup_fee": "Fee set to %(fee)s €!\n\n", + "end_date_before_start": "End date cannot be before start date! ❌", + "negative_fee": "Fee cannot be negative! ❌", + "confirm_button": "✔️ Confirm", + "finish_raffle_button": "✔️ Finish raffle", + "raffle_confirmation": "Succesfully setup new raffle in %(chat_title)s! ✔️", + "raffle_created_chat": "New raffle created by @%(username)s! ✔️", + "raffle_updated_chat": "Raffle updated by @%(username)s! ✔️", + "timed_out": "Timed out! 🕐", + "start_prompt": ( + "Use the given commands or send me an Excel-file " + + "from MobilePay if you're the host of a raffle!" + ), + "invalid_winner_usage": "Please use the format /winner @username", + "forbidden_command": "You are not allowed to use this command! ❌", + "user_not_found": ("Error getting user!\n" + "Perhaps they haven't /moro ed? 🤔"), + "already_winner": "You are already the winner!", + "winner_confirmation": "%(username)s is the new winner!", } diff --git a/kipubot/db.py b/kipubot/db.py index a970bb2..9c83daa 100644 --- a/kipubot/db.py +++ b/kipubot/db.py @@ -16,31 +16,38 @@ def _init_db(url: str) -> None: global _CON # pylint: disable=global-statement if not _CON: - _logger.info('Connecting to DB...') + _logger.info("Connecting to DB...") _CON = psycopg.connect(url) - _logger.info('Connected!') + _logger.info("Connected!") - _logger.info('Initializing database...') + _logger.info("Initializing database...") try: - _CON.execute('''CREATE TABLE IF NOT EXISTS chat ( + _CON.execute( + """CREATE TABLE IF NOT EXISTS chat ( chat_id BIGINT PRIMARY KEY, title VARCHAR(128), admins BIGINT[], prev_winners BIGINT[], cur_winner BIGINT - )''') + )""" + ) - _CON.execute('''CREATE TABLE IF NOT EXISTS chat_user ( + _CON.execute( + """CREATE TABLE IF NOT EXISTS chat_user ( user_id BIGINT PRIMARY KEY - )''') + )""" + ) - _CON.execute('''CREATE TABLE IF NOT EXISTS in_chat ( + _CON.execute( + """CREATE TABLE IF NOT EXISTS in_chat ( user_id BIGINT REFERENCES chat_user(user_id), chat_id BIGINT REFERENCES chat(chat_id), PRIMARY KEY (user_id, chat_id) - )''') + )""" + ) - _CON.execute('''CREATE TABLE IF NOT EXISTS raffle ( + _CON.execute( + """CREATE TABLE IF NOT EXISTS raffle ( chat_id BIGINT PRIMARY KEY REFERENCES chat(chat_id), start_date TIMESTAMP, end_date TIMESTAMP, @@ -48,69 +55,77 @@ def _init_db(url: str) -> None: dates TIMESTAMP[], entries VARCHAR(128)[], amounts INTEGER[] - )''') + )""" + ) except PSErrors.Error as e: - _logger.error('Unknown error during database initialization:') + _logger.error("Unknown error during database initialization:") _logger.error(e) _CON.rollback() else: - _logger.info('Database succesfully initialized!') + _logger.info("Database succesfully initialized!") _CON.commit() def get_registered_member_ids(chat_id: int) -> List[int]: - return [row[0] for row in _CON.execute( - '''SELECT chat_user.user_id + return [ + row[0] + for row in _CON.execute( + """SELECT chat_user.user_id FROM chat_user, in_chat - WHERE chat_id = %s AND chat_user.user_id = in_chat.user_id''', (chat_id,)).fetchall()] + WHERE chat_id = %s AND chat_user.user_id = in_chat.user_id""", + (chat_id,), + ).fetchall() + ] def get_admin_ids(chat_id: int) -> List[int]: - return (_CON.execute( - 'SELECT admins FROM chat WHERE chat_id = %s', (chat_id,)) - .fetchone()[0]) + return _CON.execute( + "SELECT admins FROM chat WHERE chat_id = %s", (chat_id,) + ).fetchone()[0] def get_prev_winner_ids(chat_id: int) -> List[int]: - return (_CON.execute( - 'SELECT prev_winners FROM chat WHERE chat_id = %s', (chat_id,)) - .fetchone()[0]) + return _CON.execute( + "SELECT prev_winners FROM chat WHERE chat_id = %s", (chat_id,) + ).fetchone()[0] def get_winner_id(chat_id: int) -> int: - return (_CON.execute( - 'SELECT cur_winner FROM chat WHERE chat_id = %s', (chat_id,)) - .fetchone()[0]) + return _CON.execute( + "SELECT cur_winner FROM chat WHERE chat_id = %s", (chat_id,) + ).fetchone()[0] def get_chats_where_winner(user_id: int) -> List[Tuple[int, str]]: return _CON.execute( - '''SELECT c.chat_id, c.title + """SELECT c.chat_id, c.title FROM chat AS c, in_chat as i WHERE i.user_id = %(id)s AND c.chat_id = i.chat_id - AND (c.cur_winner = %(id)s)''', - {'id': user_id}).fetchall() - - -def get_raffle_data(chat_id: int) -> Tuple[ - int, Timestamp, Timestamp, int, - List[Timestamp], List[str], List[int]]: - return _CON.execute( - 'SELECT * FROM raffle WHERE chat_id = %s', [chat_id]).fetchone() - - -def save_raffle_data(chat_id: int, - start_date: Timestamp, - end_date: Timestamp, - entry_fee: int, - df: DataFrame) -> None: - - dates = df['date'].tolist() - entries = df['name'].tolist() - amounts = df['amount'].tolist() - - _CON.execute('''INSERT INTO raffle + AND (c.cur_winner = %(id)s)""", + {"id": user_id}, + ).fetchall() + + +def get_raffle_data( + chat_id: int, +) -> Tuple[int, Timestamp, Timestamp, int, List[Timestamp], List[str], List[int]]: + return _CON.execute("SELECT * FROM raffle WHERE chat_id = %s", [chat_id]).fetchone() + + +def save_raffle_data( + chat_id: int, + start_date: Timestamp, + end_date: Timestamp, + entry_fee: int, + df: DataFrame, +) -> None: + dates = df["date"].tolist() + entries = df["name"].tolist() + amounts = df["amount"].tolist() + + _CON.execute( + """INSERT INTO raffle VALUES (%s, %s, %s, %s, %s, %s, %s) ON CONFLICT (chat_id) DO UPDATE SET @@ -119,50 +134,55 @@ def save_raffle_data(chat_id: int, entry_fee = EXCLUDED.entry_fee, dates = EXCLUDED.dates, entries = EXCLUDED.entries, - amounts = EXCLUDED.amounts''', - (chat_id, start_date, end_date, entry_fee, dates, entries, amounts)) + amounts = EXCLUDED.amounts""", + (chat_id, start_date, end_date, entry_fee, dates, entries, amounts), + ) _CON.commit() def delete_raffle_data(chat_id: int): - _CON.execute('''DELETE FROM raffle where chat_id=%s''', (chat_id,)) + _CON.execute("""DELETE FROM raffle where chat_id=%s""", (chat_id,)) _CON.commit() def save_user_or_ignore(user_id: int) -> None: - _CON.execute('''INSERT INTO chat_user + _CON.execute( + """INSERT INTO chat_user VALUES (%s) ON CONFLICT (user_id) - DO NOTHING''', - (user_id,)) + DO NOTHING""", + (user_id,), + ) _CON.commit() def save_chat_or_ignore(chat_id: int, title: str, admin_ids: List[int]) -> None: - _CON.execute('''INSERT INTO chat (chat_id, title, admins) + _CON.execute( + """INSERT INTO chat (chat_id, title, admins) VALUES (%s, %s, %s) ON CONFLICT (chat_id) - DO NOTHING''', - (chat_id, title, admin_ids)) + DO NOTHING""", + (chat_id, title, admin_ids), + ) _CON.commit() def delete_chat(chat_id: int): - _CON.execute('''DELETE FROM chat where chat_id=%s''', (chat_id,)) + _CON.execute("""DELETE FROM chat where chat_id=%s""", (chat_id,)) _CON.commit() - def register_user(chat_id: int, user_id: int) -> None: - save_user_or_ignore(user_id) try: - _CON.execute('''INSERT INTO in_chat(user_id, chat_id) - VALUES (%s, %s)''', - (user_id, chat_id)) + _CON.execute( + """INSERT INTO in_chat(user_id, chat_id) + VALUES (%s, %s)""", + (user_id, chat_id), + ) except PSErrors.UniqueViolation as e: _CON.rollback() raise AlreadyRegisteredError from e @@ -178,26 +198,32 @@ def register_user_or_ignore(chat_id: int, user_id: int) -> None: def admin_cycle_winners(winner_id: int, chat_id: int) -> None: - _CON.execute('''UPDATE chat + _CON.execute( + """UPDATE chat SET prev_winners = array_append(prev_winners, cur_winner), cur_winner=%s - WHERE chat_id=%s''', - (winner_id, chat_id)) + WHERE chat_id=%s""", + (winner_id, chat_id), + ) _CON.commit() def replace_cur_winner(winner_id: int, chat_id: int) -> None: - _CON.execute('''UPDATE chat + _CON.execute( + """UPDATE chat SET cur_winner=%s - WHERE chat_id=%s''', - (winner_id, chat_id)) + WHERE chat_id=%s""", + (winner_id, chat_id), + ) _CON.commit() def cycle_winners(user_id: int, winner_id: int, chat_id: int) -> None: - _CON.execute('''UPDATE chat + _CON.execute( + """UPDATE chat SET prev_winners=array_append(prev_winners, %s), cur_winner=%s' - WHERE chat_id=%s''', - (user_id, winner_id, chat_id)) + WHERE chat_id=%s""", + (user_id, winner_id, chat_id), + ) _CON.commit() diff --git a/kipubot/handlers/__init__.py b/kipubot/handlers/__init__.py index 34a2cc8..a6aeb26 100644 --- a/kipubot/handlers/__init__.py +++ b/kipubot/handlers/__init__.py @@ -8,7 +8,7 @@ "expected_value_handler", "raffle_setup_handler", "no_dm_handler", - "error_handler" + "error_handler", ) from ._raffle_setup_handler import raffle_setup_handler diff --git a/kipubot/handlers/_bot_added_handler.py b/kipubot/handlers/_bot_added_handler.py index 40f08d2..0789acb 100644 --- a/kipubot/handlers/_bot_added_handler.py +++ b/kipubot/handlers/_bot_added_handler.py @@ -3,8 +3,7 @@ from telegram.constants import ChatMemberStatus import psycopg.errors as PSErrors from kipubot.constants import STRINGS -from kipubot.db import (save_chat_or_ignore, - save_user_or_ignore, register_user_or_ignore) +from kipubot.db import save_chat_or_ignore, save_user_or_ignore, register_user_or_ignore async def bot_added(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: @@ -26,18 +25,19 @@ async def bot_added(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: register_user_or_ignore(chat_id, user_id) except PSErrors.IntegrityError as e: - print('SQLite Error: ' + str(e)) + print("SQLite Error: " + str(e)) await context.bot.send_message( - chat_id=chat_id, - text=STRINGS['unknown_error'] + chat_id=chat_id, text=STRINGS["unknown_error"] ) else: # Kiitos pääsystä! -stigu await context.bot.send_sticker( chat_id=chat_id, - sticker='CAACAgQAAxkBAAIBPmLicTHP2Xv8IcFzxHYocjLRFBvQAAI5AAMcLHsXd9jLHwYNcSEpBA') + sticker="CAACAgQAAxkBAAIBPmLicTHP2Xv8IcFzxHYocjLRFBvQAAI5AAMcLHsXd9jLHwYNcSEpBA", + ) await context.bot.send_message( - chat_id=chat_id, - text=STRINGS['moro_prompt'] % {'chat_title': title}) + chat_id=chat_id, text=STRINGS["moro_prompt"] % {"chat_title": title} + ) + bot_added_handler = ChatMemberHandler(bot_added, -1) diff --git a/kipubot/handlers/_error_handler.py b/kipubot/handlers/_error_handler.py index 139b485..3022dc0 100644 --- a/kipubot/handlers/_error_handler.py +++ b/kipubot/handlers/_error_handler.py @@ -13,22 +13,27 @@ async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Log the error and send a telegram message to notify the developer.""" - # Log the error before we do anything else, so we can see it even if something breaks. - _logger.error(msg="Exception while handling an update:", - exc_info=context.error) + # Log the error before we do anything else, + # so we can see it even if something breaks. + _logger.error(msg="Exception while handling an update:", exc_info=context.error) - # traceback.format_exception returns the usual python message about an exception, but as a - # list of strings rather than a single string, so we have to join them together. + # traceback.format_exception returns the usual python message about an exception, + # but as a list of strings rather than a single string, + # so we have to join them together. tb_list = traceback.format_exception( - None, context.error, context.error.__traceback__) + None, context.error, context.error.__traceback__ + ) tb_string = "".join(tb_list) # Build the message with some markup and additional information about what happened. - # You might need to add some logic to deal with messages longer than the 4096 character limit. + # You might need to add some logic to deal with + # messages longer than the 4096 character limit. update_str = update.to_dict() if isinstance(update, Update) else str(update) message = ( f"An exception was raised while handling an update\n" - f"
update = {html.escape(json.dumps(update_str, indent=2, ensure_ascii=False))}"
+        f"""
update = {html.escape(
+            json.dumps(update_str, indent=2, ensure_ascii=False)
+        )}"""
         "
\n\n" f"
context.chat_data = {html.escape(str(context.chat_data))}
\n\n" f"
context.user_data = {html.escape(str(context.user_data))}
\n\n" @@ -41,5 +46,5 @@ async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N ) # Also send a message to the user who triggered the error. await context.bot.send_message( - chat_id=update.effective_chat.id, text=STRINGS['server_error'] + chat_id=update.effective_chat.id, text=STRINGS["server_error"] ) diff --git a/kipubot/handlers/_excel_file_handler.py b/kipubot/handlers/_excel_file_handler.py index f51daff..11205cf 100644 --- a/kipubot/handlers/_excel_file_handler.py +++ b/kipubot/handlers/_excel_file_handler.py @@ -8,49 +8,56 @@ from kipubot.db import get_chats_where_winner -async def excel_file(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Optional[str]: +async def excel_file( + update: Update, context: ContextTypes.DEFAULT_TYPE +) -> Optional[str]: user_id = update.effective_user.id dm_id = update.effective_chat.id chats = get_chats_where_winner(user_id) if len(chats) == 0: - await update.message.reply_text(STRINGS['not_winner']) + await update.message.reply_text(STRINGS["not_winner"]) return ConversationHandler.END doc = update.message.document file = await context.bot.get_file(doc) - base_path = f'data/{dm_id}' - excel_path = base_path + '/data.xlsx' + base_path = f"data/{dm_id}" + excel_path = base_path + "/data.xlsx" if not os.path.exists(base_path): os.mkdir(base_path) - with open(excel_path, 'wb') as f: + with open(excel_path, "wb") as f: await file.download(out=f) if not validate_excel(excel_path): - await update.message.reply_text(STRINGS['invalid_file']) + await update.message.reply_text(STRINGS["invalid_file"]) os.remove(excel_path) return chat_buttons = [] for chat_id, chat_title in chats: - chat_buttons.append(InlineKeyboardButton( - STRINGS['chat_button'] % {'chat_title': chat_title}, - callback_data=f'raffle:chat_selected:{chat_id}:{chat_title}')) + chat_buttons.append( + InlineKeyboardButton( + STRINGS["chat_button"] % {"chat_title": chat_title}, + callback_data=f"raffle:chat_selected:{chat_id}:{chat_title}", + ) + ) keyboard = [ chat_buttons, - [InlineKeyboardButton(STRINGS['cancel_button'], - callback_data='raffle:cancel')] + [InlineKeyboardButton(STRINGS["cancel_button"], callback_data="raffle:cancel")], ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text( - STRINGS['choose_channel'], reply_markup=reply_markup) + STRINGS["choose_channel"], reply_markup=reply_markup + ) -excel_file_handler = MessageHandler(Filters.Document.MimeType(EXCEL_MIME) & - Filters.ChatType.PRIVATE, excel_file) + +excel_file_handler = MessageHandler( + Filters.Document.MimeType(EXCEL_MIME) & Filters.ChatType.PRIVATE, excel_file +) diff --git a/kipubot/handlers/_graph_handlers.py b/kipubot/handlers/_graph_handlers.py index 1343d8b..bac0ce8 100644 --- a/kipubot/handlers/_graph_handlers.py +++ b/kipubot/handlers/_graph_handlers.py @@ -9,22 +9,25 @@ class GraphType(Enum): - EXPECTED = 'expected' - GRAPH = 'graph' + EXPECTED = "expected" + GRAPH = "graph" def get_graph_img(graph_type: GraphType) -> str: if graph_type == GraphType.EXPECTED: - return 'expected.png' + return "expected.png" - return 'graph.png' + return "graph.png" -async def graph(update: Update, _context: ContextTypes.DEFAULT_TYPE, - graph_type: GraphType = GraphType.GRAPH) -> None: +async def graph( + update: Update, + _context: ContextTypes.DEFAULT_TYPE, + graph_type: GraphType = GraphType.GRAPH, +) -> None: chat_id = update.effective_chat.id chat_title = update.effective_chat.title - graph_path = f'data/{chat_id}/{get_graph_img(graph_type)}' + graph_path = f"data/{chat_id}/{get_graph_img(graph_type)}" try: if graph_type == GraphType.EXPECTED: @@ -32,23 +35,28 @@ async def graph(update: Update, _context: ContextTypes.DEFAULT_TYPE, else: generate_graph(graph_path, chat_id, chat_title) - with open(graph_path, 'rb') as f: + with open(graph_path, "rb") as f: await update.message.reply_photo(photo=f) except NoRaffleError: - await update.message.reply_text(STRINGS['no_raffle'] % {'chat_title': chat_title}) + await update.message.reply_text( + STRINGS["no_raffle"] % {"chat_title": chat_title} + ) except NoEntriesError: - await update.message.reply_text(STRINGS['no_entries'] % {'chat_title': chat_title}) + await update.message.reply_text( + STRINGS["no_entries"] % {"chat_title": chat_title} + ) except PSErrors.Error as e: print(e) - await update.message.reply_text(STRINGS['raffle_db_error']) + await update.message.reply_text(STRINGS["raffle_db_error"]) except FileNotFoundError: - await update.message.reply_text(STRINGS['no_data'] % {'chat_title': chat_title}) + await update.message.reply_text(STRINGS["no_data"] % {"chat_title": chat_title}) -graph_handler = CommandHandler( - ['kuvaaja', 'graph'], graph, ~Filters.ChatType.PRIVATE) + +graph_handler = CommandHandler(["kuvaaja", "graph"], graph, ~Filters.ChatType.PRIVATE) expected_value_handler = CommandHandler( - ['odotusarvo', 'expected'], + ["odotusarvo", "expected"], lambda u, c: graph(u, c, graph_type=GraphType.EXPECTED), - ~Filters.ChatType.PRIVATE) + ~Filters.ChatType.PRIVATE, +) diff --git a/kipubot/handlers/_moro_handler.py b/kipubot/handlers/_moro_handler.py index 0495665..f81b648 100644 --- a/kipubot/handlers/_moro_handler.py +++ b/kipubot/handlers/_moro_handler.py @@ -16,11 +16,13 @@ async def hello(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: register_user(chat_id, user_id) except AlreadyRegisteredError: - await update.message.reply_text(STRINGS['double_moro'] % - {'username': username, 'chat_title': chat}) + await update.message.reply_text( + STRINGS["double_moro"] % {"username": username, "chat_title": chat} + ) else: - await update.message.reply_text(STRINGS['moro'] % - {'username': username, 'chat_title': chat}) + await update.message.reply_text( + STRINGS["moro"] % {"username": username, "chat_title": chat} + ) -moro_handler = CommandHandler( - ['moro', 'hello'], hello, ~Filters.ChatType.PRIVATE) + +moro_handler = CommandHandler(["moro", "hello"], hello, ~Filters.ChatType.PRIVATE) diff --git a/kipubot/handlers/_no_dm_handler.py b/kipubot/handlers/_no_dm_handler.py index 6916bf8..6696920 100644 --- a/kipubot/handlers/_no_dm_handler.py +++ b/kipubot/handlers/_no_dm_handler.py @@ -5,8 +5,11 @@ async def chat_only(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: - await update.message.reply_text(STRINGS['no_dm_warn']) + await update.message.reply_text(STRINGS["no_dm_warn"]) + no_dm_handler = CommandHandler( - ['moro', 'hello', 'kuvaaja', 'graph', 'voittaja', 'winner'], - chat_only, Filters.ChatType.PRIVATE) + ["moro", "hello", "kuvaaja", "graph", "voittaja", "winner"], + chat_only, + Filters.ChatType.PRIVATE, +) diff --git a/kipubot/handlers/_raffle_setup_handler.py b/kipubot/handlers/_raffle_setup_handler.py index 8a21a4c..2274040 100644 --- a/kipubot/handlers/_raffle_setup_handler.py +++ b/kipubot/handlers/_raffle_setup_handler.py @@ -4,8 +4,15 @@ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import ConversationHandler, CallbackQueryHandler, CallbackContext from kipubot.constants import STRINGS -from kipubot.utils import (get_raffle, save_raffle, read_excel_to_df, is_int, - is_float, get_cur_time_hel, int_price_to_str) +from kipubot.utils import ( + get_raffle, + save_raffle, + read_excel_to_df, + is_int, + is_float, + get_cur_time_hel, + int_price_to_str, +) from kipubot.errors import NoRaffleError # ================== @@ -18,24 +25,25 @@ async def cancel_convo(update: Update, context: CallbackContext) -> int: query = update.callback_query - await query.message.edit_text(STRINGS['cancelled'], reply_markup=None) + await query.message.edit_text(STRINGS["cancelled"], reply_markup=None) context.user_data.clear() return ConversationHandler.END async def convo_error(update: Update, context: CallbackContext) -> int: query = update.callback_query - await query.message.edit_text(STRINGS['unknown_error'], reply_markup=None) + await query.message.edit_text(STRINGS["unknown_error"], reply_markup=None) context.user_data.clear() return ConversationHandler.END async def convo_timeout(update: Update, context: CallbackContext) -> int: query = update.callback_query - await query.message.edit_text(STRINGS['timed_out'], reply_markup=None) + await query.message.edit_text(STRINGS["timed_out"], reply_markup=None) context.user_data.clear() return ConversationHandler.END + # KEYBOARD COMPONENTS # -------------------- @@ -43,66 +51,71 @@ async def convo_timeout(update: Update, context: CallbackContext) -> int: def raffle_keyboard(has_existing: bool = False) -> InlineKeyboardMarkup: if has_existing: keyboard = [ - [InlineKeyboardButton( - STRINGS['new_raffle_button'], callback_data='raffle:setup:new')], - [InlineKeyboardButton( - STRINGS['update_raffle_button'], callback_data='raffle:setup:old')], - [InlineKeyboardButton( - STRINGS['cancel_button'], - callback_data='raffle:cancel')] + [ + InlineKeyboardButton( + STRINGS["new_raffle_button"], callback_data="raffle:setup:new" + ) + ], + [ + InlineKeyboardButton( + STRINGS["update_raffle_button"], callback_data="raffle:setup:old" + ) + ], + [ + InlineKeyboardButton( + STRINGS["cancel_button"], callback_data="raffle:cancel" + ) + ], ] return InlineKeyboardMarkup(keyboard) keyboard = [ - [InlineKeyboardButton( - STRINGS['new_raffle_button'], callback_data='raffle:setup:new')], - [InlineKeyboardButton( - STRINGS['cancel_button'], callback_data='raffle:cancel')], + [ + InlineKeyboardButton( + STRINGS["new_raffle_button"], callback_data="raffle:setup:new" + ) + ], + [InlineKeyboardButton(STRINGS["cancel_button"], callback_data="raffle:cancel")], ] return InlineKeyboardMarkup(keyboard) def date_keyboard(which: str) -> InlineKeyboardMarkup: - if which not in ['start', 'end']: - raise Exception('Invalid date type, should be start or end!') + if which not in ["start", "end"]: + raise Exception("Invalid date type, should be start or end!") rough_controls = [ - InlineKeyboardButton( - '-1 d', callback_data=f'raffle:date:{which}:update:-24'), - InlineKeyboardButton( - '-12 h', callback_data=f'raffle:date:{which}:update:-12'), - InlineKeyboardButton( - '-6 h', callback_data=f'raffle:date:{which}:update:-6'), - InlineKeyboardButton('+6 h', - callback_data=f'raffle:date:{which}:update:+6'), - InlineKeyboardButton('+12 h', - callback_data=f'raffle:date:{which}:update:+12'), - InlineKeyboardButton('+1 d', - callback_data=f'raffle:date:{which}:update:+24'), + InlineKeyboardButton("-1 d", callback_data=f"raffle:date:{which}:update:-24"), + InlineKeyboardButton("-12 h", callback_data=f"raffle:date:{which}:update:-12"), + InlineKeyboardButton("-6 h", callback_data=f"raffle:date:{which}:update:-6"), + InlineKeyboardButton("+6 h", callback_data=f"raffle:date:{which}:update:+6"), + InlineKeyboardButton("+12 h", callback_data=f"raffle:date:{which}:update:+12"), + InlineKeyboardButton("+1 d", callback_data=f"raffle:date:{which}:update:+24"), ] smooth_controls = [ + InlineKeyboardButton("-1 h", callback_data=f"raffle:date:{which}:update:-1"), + InlineKeyboardButton("-30 m", callback_data=f"raffle:date:{which}:update:-0.5"), InlineKeyboardButton( - '-1 h', callback_data=f'raffle:date:{which}:update:-1'), - InlineKeyboardButton('-30 m', - callback_data=f'raffle:date:{which}:update:-0.5'), + "-15 m", callback_data=f"raffle:date:{which}:update:-0.25" + ), InlineKeyboardButton( - '-15 m', callback_data=f'raffle:date:{which}:update:-0.25'), - InlineKeyboardButton('+15 m', - callback_data=f'raffle:date:{which}:update:+0.25'), - InlineKeyboardButton('+30 m', - callback_data=f'raffle:date:{which}:update:+0.5'), - InlineKeyboardButton('+1 h', - callback_data=f'raffle:date:{which}:update:+1'), + "+15 m", callback_data=f"raffle:date:{which}:update:+0.25" + ), + InlineKeyboardButton("+30 m", callback_data=f"raffle:date:{which}:update:+0.5"), + InlineKeyboardButton("+1 h", callback_data=f"raffle:date:{which}:update:+1"), ] keyboard = [ rough_controls, smooth_controls, - [InlineKeyboardButton(STRINGS['confirm_button'], - callback_data=f'raffle:date:{which}:confirmed')], - [InlineKeyboardButton( - STRINGS['cancel_button'], callback_data='raffle:cancel')] + [ + InlineKeyboardButton( + STRINGS["confirm_button"], + callback_data=f"raffle:date:{which}:confirmed", + ) + ], + [InlineKeyboardButton(STRINGS["cancel_button"], callback_data="raffle:cancel")], ] return InlineKeyboardMarkup(keyboard) @@ -110,20 +123,18 @@ def date_keyboard(which: str) -> InlineKeyboardMarkup: def fee_keyboard() -> InlineKeyboardMarkup: keyboard = [ + [ + InlineKeyboardButton("-1", callback_data="raffle:fee:update:-100"), + InlineKeyboardButton("-0.5", callback_data="raffle:fee:update:-50"), + InlineKeyboardButton("+0.5", callback_data="raffle:fee:update:+50"), + InlineKeyboardButton("+1", callback_data="raffle:fee:update:+100"), + ], [ InlineKeyboardButton( - '-1', callback_data='raffle:fee:update:-100'), - InlineKeyboardButton('-0.5', - callback_data='raffle:fee:update:-50'), - InlineKeyboardButton('+0.5', - callback_data='raffle:fee:update:+50'), - InlineKeyboardButton('+1', - callback_data='raffle:fee:update:+100'), + STRINGS["finish_raffle_button"], callback_data="raffle:fee:confirmed" + ) ], - [InlineKeyboardButton(STRINGS['finish_raffle_button'], - callback_data='raffle:fee:confirmed')], - [InlineKeyboardButton( - STRINGS['cancel_button'], callback_data='raffle:cancel')], + [InlineKeyboardButton(STRINGS["cancel_button"], callback_data="raffle:cancel")], ] return InlineKeyboardMarkup(keyboard) @@ -137,36 +148,42 @@ def fee_keyboard() -> InlineKeyboardMarkup: async def setup_raffle(update: Update, context: CallbackContext) -> Union[str, int]: query = update.callback_query - if query.data == 'raffle:cancel': + if query.data == "raffle:cancel": return await cancel_convo(update, context) - if (query.data.startswith('raffle:chat_selected') and - len(query.data.split(':')) == 4 and - is_int(query.data.split(':')[2])): - args = query.data.split(':') + if ( + query.data.startswith("raffle:chat_selected") + and len(query.data.split(":")) == 4 + and is_int(query.data.split(":")[2]) + ): + args = query.data.split(":") chat_id = int(args[2]) chat_title = args[3] # store selected chat in user_data - context.user_data['raffle_chat_id'] = chat_id - context.user_data['raffle_chat_title'] = chat_title + context.user_data["raffle_chat_id"] = chat_id + context.user_data["raffle_chat_title"] = chat_title try: get_raffle(chat_id) - msg = (STRINGS['raffle_setup_base'] + STRINGS['raffle_setup_update_or_new']) % { - 'chat_title': chat_title} + msg = ( + STRINGS["raffle_setup_base"] + STRINGS["raffle_setup_update_or_new"] + ) % {"chat_title": chat_title} - await query.message.edit_text(msg, reply_markup=raffle_keyboard(has_existing=True)) + await query.message.edit_text( + msg, reply_markup=raffle_keyboard(has_existing=True) + ) except NoRaffleError: - msg = (STRINGS['raffle_setup_base'] + STRINGS['raffle_setup_new']) % { - 'chat_title': chat_title} + msg = (STRINGS["raffle_setup_base"] + STRINGS["raffle_setup_new"]) % { + "chat_title": chat_title + } await query.message.edit_text(msg, reply_markup=raffle_keyboard()) - return 'raffle_setup_state:update_or_new' + return "raffle_setup_state:update_or_new" # If nothing matches, return error return await convo_error(update, context) @@ -175,31 +192,34 @@ async def setup_raffle(update: Update, context: CallbackContext) -> Union[str, i async def setup_start_date(update: Update, context: CallbackContext) -> Optional[str]: query = update.callback_query - if query.data == 'raffle:setup:new': - context.user_data['raffle_start_date'] = get_cur_time_hel().floor( - freq="15T") - - if (query.data.startswith('raffle:date:start:update') and - len(query.data.split(':')) == 5 and - is_float(query.data.split(':')[4])): + if query.data == "raffle:setup:new": + context.user_data["raffle_start_date"] = get_cur_time_hel().floor(freq="15T") - diff = float(query.data.split(':')[4]) - old_date = context.user_data['raffle_start_date'] - new_date = old_date + pd.Timedelta(diff, unit='h') + if ( + query.data.startswith("raffle:date:start:update") + and len(query.data.split(":")) == 5 + and is_float(query.data.split(":")[4]) + ): + diff = float(query.data.split(":")[4]) + old_date = context.user_data["raffle_start_date"] + new_date = old_date + pd.Timedelta(diff, unit="h") - context.user_data['raffle_start_date'] = new_date + context.user_data["raffle_start_date"] = new_date - if (query.data == 'raffle:setup:new' or - query.data.startswith('raffle:date:start:update')): - chat_title = context.user_data['raffle_chat_title'] - start_date = context.user_data['raffle_start_date'] + if query.data == "raffle:setup:new" or query.data.startswith( + "raffle:date:start:update" + ): + chat_title = context.user_data["raffle_chat_title"] + start_date = context.user_data["raffle_start_date"] - msg = (STRINGS['raffle_setup_base'] + STRINGS['raffle_setup_start_date']) % { - 'chat_title': chat_title, 'start_date': start_date} + msg = (STRINGS["raffle_setup_base"] + STRINGS["raffle_setup_start_date"]) % { + "chat_title": chat_title, + "start_date": start_date, + } - await query.message.edit_text(msg, reply_markup=date_keyboard('start')) + await query.message.edit_text(msg, reply_markup=date_keyboard("start")) - return 'raffle_setup_state:start_date' + return "raffle_setup_state:start_date" return None @@ -207,40 +227,48 @@ async def setup_start_date(update: Update, context: CallbackContext) -> Optional async def setup_end_date(update: Update, context: CallbackContext) -> Optional[str]: query = update.callback_query - if query.data == 'raffle:date:start:confirmed': - context.user_data['raffle_end_date'] = context.user_data['raffle_start_date'] - - if (query.data.startswith('raffle:date:end:update') and - len(query.data.split(':')) == 5 and - is_float(query.data.split(':')[4])): - - diff = float(query.data.split(':')[4]) - old_date = context.user_data['raffle_end_date'] - new_date = old_date + pd.Timedelta(diff, unit='h') - - context.user_data['raffle_end_date'] = new_date - - if (query.data == 'raffle:date:start:confirmed' or - query.data.startswith('raffle:date:end:update')): - chat_title = context.user_data['raffle_chat_title'] - start_date = context.user_data['raffle_start_date'] - end_date = context.user_data['raffle_end_date'] - - msg = (STRINGS['raffle_setup_base'] + STRINGS['raffle_setup_start_date'] + - STRINGS['raffle_setup_end_date']) + if query.data == "raffle:date:start:confirmed": + context.user_data["raffle_end_date"] = context.user_data["raffle_start_date"] + + if ( + query.data.startswith("raffle:date:end:update") + and len(query.data.split(":")) == 5 + and is_float(query.data.split(":")[4]) + ): + diff = float(query.data.split(":")[4]) + old_date = context.user_data["raffle_end_date"] + new_date = old_date + pd.Timedelta(diff, unit="h") + + context.user_data["raffle_end_date"] = new_date + + if query.data == "raffle:date:start:confirmed" or query.data.startswith( + "raffle:date:end:update" + ): + chat_title = context.user_data["raffle_chat_title"] + start_date = context.user_data["raffle_start_date"] + end_date = context.user_data["raffle_end_date"] + + msg = ( + STRINGS["raffle_setup_base"] + + STRINGS["raffle_setup_start_date"] + + STRINGS["raffle_setup_end_date"] + ) if end_date < start_date: - context.user_data['raffle_end_date'] = start_date + context.user_data["raffle_end_date"] = start_date end_date = start_date - msg += STRINGS['end_date_before_start'] + msg += STRINGS["end_date_before_start"] - msg = msg % {'chat_title': chat_title, - 'start_date': start_date, 'end_date': end_date} + msg = msg % { + "chat_title": chat_title, + "start_date": start_date, + "end_date": end_date, + } if query.message.text != msg: - await query.message.edit_text(msg, reply_markup=date_keyboard('end')) + await query.message.edit_text(msg, reply_markup=date_keyboard("end")) - return 'raffle_setup_state:end_date' + return "raffle_setup_state:end_date" return None @@ -248,39 +276,50 @@ async def setup_end_date(update: Update, context: CallbackContext) -> Optional[s async def setup_fee(update: Update, context: CallbackContext) -> Optional[str]: query = update.callback_query - if query.data == 'raffle:date:end:confirmed': - context.user_data['raffle_fee'] = 100 + if query.data == "raffle:date:end:confirmed": + context.user_data["raffle_fee"] = 100 - if (query.data.startswith('raffle:fee:update') and - len(query.data.split(':')) == 4 and - is_int(query.data.split(':')[3])): - - diff = int(query.data.split(':')[3]) - old_fee = context.user_data['raffle_fee'] + if ( + query.data.startswith("raffle:fee:update") + and len(query.data.split(":")) == 4 + and is_int(query.data.split(":")[3]) + ): + diff = int(query.data.split(":")[3]) + old_fee = context.user_data["raffle_fee"] new_fee = old_fee + diff - context.user_data['raffle_fee'] = new_fee + context.user_data["raffle_fee"] = new_fee - if (query.data == 'raffle:date:end:confirmed' or query.data.startswith('raffle:fee:update')): - chat_title = context.user_data['raffle_chat_title'] - start_date = context.user_data['raffle_start_date'] - end_date = context.user_data['raffle_end_date'] - fee = context.user_data['raffle_fee'] + if query.data == "raffle:date:end:confirmed" or query.data.startswith( + "raffle:fee:update" + ): + chat_title = context.user_data["raffle_chat_title"] + start_date = context.user_data["raffle_start_date"] + end_date = context.user_data["raffle_end_date"] + fee = context.user_data["raffle_fee"] - msg = (STRINGS['raffle_setup_base'] + STRINGS['raffle_setup_start_date'] + - STRINGS['raffle_setup_end_date'] + STRINGS['raffle_setup_fee']) + msg = ( + STRINGS["raffle_setup_base"] + + STRINGS["raffle_setup_start_date"] + + STRINGS["raffle_setup_end_date"] + + STRINGS["raffle_setup_fee"] + ) if fee < 0: - context.user_data['raffle_fee'] = 0 - msg += STRINGS['negative_fee'] + context.user_data["raffle_fee"] = 0 + msg += STRINGS["negative_fee"] - msg = msg % {'chat_title': chat_title, 'start_date': start_date, - 'end_date': end_date, 'fee': int_price_to_str(fee)} + msg = msg % { + "chat_title": chat_title, + "start_date": start_date, + "end_date": end_date, + "fee": int_price_to_str(fee), + } if query.message.text != msg: await query.message.edit_text(msg, reply_markup=fee_keyboard()) - return 'raffle_setup_state:fee' + return "raffle_setup_state:fee" return None @@ -289,48 +328,60 @@ async def finish_setup(update: Update, context: CallbackContext) -> Optional[int query = update.callback_query dm_id = update.effective_chat.id - if query.data == 'raffle:setup:old': - chat_title = context.user_data['raffle_chat_title'] - chat_id = context.user_data['raffle_chat_id'] + if query.data == "raffle:setup:old": + chat_title = context.user_data["raffle_chat_title"] + chat_id = context.user_data["raffle_chat_id"] dm_id = update.effective_chat.id start_date, end_date, entry_fee, _ = get_raffle(chat_id) - excel_path = f'data/{dm_id}/data.xlsx' + excel_path = f"data/{dm_id}/data.xlsx" df = read_excel_to_df(excel_path, start_date, end_date) save_raffle(chat_id, start_date, end_date, entry_fee, df) - await query.message.edit_text(STRINGS['updated_raffle'] % {'chat_title': chat_title}) - await context.bot.send_message(chat_id, - STRINGS['raffle_updated_chat'] - % {'username': update.effective_user.username}) + await query.message.edit_text( + STRINGS["updated_raffle"] % {"chat_title": chat_title} + ) + await context.bot.send_message( + chat_id, + STRINGS["raffle_updated_chat"] + % {"username": update.effective_user.username}, + ) # perform cleanup context.user_data.clear() os.remove(excel_path) return ConversationHandler.END - if query.data == 'raffle:fee:confirmed': - chat_title = context.user_data['raffle_chat_title'] - chat_id = context.user_data['raffle_chat_id'] - start_date = context.user_data['raffle_start_date'] - end_date = context.user_data['raffle_end_date'] - fee = context.user_data['raffle_fee'] + if query.data == "raffle:fee:confirmed": + chat_title = context.user_data["raffle_chat_title"] + chat_id = context.user_data["raffle_chat_id"] + start_date = context.user_data["raffle_start_date"] + end_date = context.user_data["raffle_end_date"] + fee = context.user_data["raffle_fee"] - excel_path = f'data/{dm_id}/data.xlsx' + excel_path = f"data/{dm_id}/data.xlsx" df = read_excel_to_df(excel_path, start_date, end_date) save_raffle(chat_id, start_date, end_date, fee, df) - msg = (STRINGS['raffle_setup_base'] + STRINGS['raffle_setup_start_date'] + - STRINGS['raffle_setup_end_date'] + STRINGS['raffle_setup_fee'] + - STRINGS['raffle_confirmation']) % { - 'chat_title': chat_title, - 'start_date': start_date, - 'end_date': end_date, - 'fee': int_price_to_str(fee)} + msg = ( + STRINGS["raffle_setup_base"] + + STRINGS["raffle_setup_start_date"] + + STRINGS["raffle_setup_end_date"] + + STRINGS["raffle_setup_fee"] + + STRINGS["raffle_confirmation"] + ) % { + "chat_title": chat_title, + "start_date": start_date, + "end_date": end_date, + "fee": int_price_to_str(fee), + } await query.message.edit_text(msg, reply_markup=None) - await context.bot.send_message(chat_id, STRINGS['raffle_created_chat'] - % {'username': update.effective_user.username}) + await context.bot.send_message( + chat_id, + STRINGS["raffle_created_chat"] + % {"username": update.effective_user.username}, + ) # perform cleanup context.user_data.clear() @@ -340,47 +391,35 @@ async def finish_setup(update: Update, context: CallbackContext) -> Optional[int return None + raffle_setup_handler = ConversationHandler( - entry_points=[CallbackQueryHandler(setup_raffle, pattern='^raffle:.*$')], + entry_points=[CallbackQueryHandler(setup_raffle, pattern="^raffle:.*$")], states={ - 'raffle_setup_state:update_or_new': [ - CallbackQueryHandler( - setup_start_date, - pattern='^raffle:setup:new$'), - CallbackQueryHandler( - finish_setup, - pattern='^raffle:setup:old$')], - 'raffle_setup_state:start_date': [ - CallbackQueryHandler( - setup_start_date, - pattern='^raffle:date:start:update.*$'), - CallbackQueryHandler( - setup_end_date, - pattern='^raffle:date:start:confirmed$') + "raffle_setup_state:update_or_new": [ + CallbackQueryHandler(setup_start_date, pattern="^raffle:setup:new$"), + CallbackQueryHandler(finish_setup, pattern="^raffle:setup:old$"), ], - 'raffle_setup_state:end_date': [ + "raffle_setup_state:start_date": [ CallbackQueryHandler( - setup_end_date, - pattern='^raffle:date:end:update.*$'), + setup_start_date, pattern="^raffle:date:start:update.*$" + ), CallbackQueryHandler( - setup_fee, - pattern='^raffle:date:end:confirmed$') + setup_end_date, pattern="^raffle:date:start:confirmed$" + ), ], - 'raffle_setup_state:fee': [ - CallbackQueryHandler( - setup_fee, - pattern='^raffle:fee:update.*$'), - CallbackQueryHandler( - finish_setup, - pattern='^raffle:fee:confirmed$') + "raffle_setup_state:end_date": [ + CallbackQueryHandler(setup_end_date, pattern="^raffle:date:end:update.*$"), + CallbackQueryHandler(setup_fee, pattern="^raffle:date:end:confirmed$"), ], - ConversationHandler.TIMEOUT: [CallbackQueryHandler(convo_timeout)] + "raffle_setup_state:fee": [ + CallbackQueryHandler(setup_fee, pattern="^raffle:fee:update.*$"), + CallbackQueryHandler(finish_setup, pattern="^raffle:fee:confirmed$"), + ], + ConversationHandler.TIMEOUT: [CallbackQueryHandler(convo_timeout)], }, fallbacks=[CallbackQueryHandler(setup_raffle)], conversation_timeout=120, - name='raffle_setup', + name="raffle_setup", persistent=True, - per_message=True - - + per_message=True, ) diff --git a/kipubot/handlers/_start_handler.py b/kipubot/handlers/_start_handler.py index d37d0c9..fcb1c80 100644 --- a/kipubot/handlers/_start_handler.py +++ b/kipubot/handlers/_start_handler.py @@ -4,8 +4,8 @@ async def start(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: - with open('kipubot/resources/info.png', 'rb') as f: - await update.message.reply_photo( - caption=STRINGS['start_prompt'], photo=f) + with open("kipubot/resources/info.png", "rb") as f: + await update.message.reply_photo(caption=STRINGS["start_prompt"], photo=f) -start_handler = CommandHandler('start', start) + +start_handler = CommandHandler("start", start) diff --git a/kipubot/handlers/_winner_handler.py b/kipubot/handlers/_winner_handler.py index cc98437..a292945 100644 --- a/kipubot/handlers/_winner_handler.py +++ b/kipubot/handlers/_winner_handler.py @@ -4,10 +4,15 @@ import telegram.ext.filters as Filters import psycopg.errors as PSErrors from kipubot.constants import STRINGS -from kipubot.db import (admin_cycle_winners, cycle_winners, - get_registered_member_ids, - get_admin_ids, get_prev_winner_ids, - get_winner_id, replace_cur_winner) +from kipubot.db import ( + admin_cycle_winners, + cycle_winners, + get_registered_member_ids, + get_admin_ids, + get_prev_winner_ids, + get_winner_id, + replace_cur_winner, +) from kipubot.utils import get_chat_member_opt @@ -21,7 +26,7 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: user_id = update.effective_user.id if len(ent) != 2 or ent[1].type != MessageEntityType.MENTION: - await update.message.reply_text(STRINGS['invalid_winner_usage']) + await update.message.reply_text(STRINGS["invalid_winner_usage"]) return username = update.message.text.split(" ")[1][1:] @@ -34,25 +39,28 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: is_prev_winner = prev_winner_ids and user_id == prev_winner_ids[-1] if not is_admin and not is_cur_winner and not is_prev_winner: - await update.message.reply_text(STRINGS['forbidden_command']) + await update.message.reply_text(STRINGS["forbidden_command"]) return registered_member_ids = get_registered_member_ids(chat_id) - registered_members = [await get_chat_member_opt(update.effective_chat, id) - for id in registered_member_ids] + registered_members = [ + await get_chat_member_opt(update.effective_chat, id) + for id in registered_member_ids + ] # drop None values registered_members = [m for m in registered_members if m] supposed_winner = [ - member for member in registered_members if member.user.username == username] + member for member in registered_members if member.user.username == username + ] if not supposed_winner: - await update.message.reply_text(STRINGS['user_not_found']) + await update.message.reply_text(STRINGS["user_not_found"]) return winner_id = supposed_winner[0].user.id if winner_id == user_id and not is_admin: - await update.message.reply_text(STRINGS['already_winner']) + await update.message.reply_text(STRINGS["already_winner"]) return # admin: moves current to prev and makes new current @@ -66,10 +74,14 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: cycle_winners(user_id, winner_id, chat_id) except PSErrors.Error as e: print(e) - await update.message.reply_text(STRINGS['user_not_found']) + await update.message.reply_text(STRINGS["user_not_found"]) return - await update.message.reply_text(STRINGS['winner_confirmation'] % {'username': username}) + await update.message.reply_text( + STRINGS["winner_confirmation"] % {"username": username} + ) + winner_handler = CommandHandler( - ['voittaja', 'winner'], winner, ~Filters.ChatType.PRIVATE) + ["voittaja", "winner"], winner, ~Filters.ChatType.PRIVATE +) diff --git a/kipubot/utils.py b/kipubot/utils.py index 4fb22fe..7ceb2af 100644 --- a/kipubot/utils.py +++ b/kipubot/utils.py @@ -43,16 +43,15 @@ def is_float(x: str) -> bool: def int_price_to_str(num: int) -> str: num = num / 100.0 - str_num: str = format(num, '.2f') if num >= 0 else \ - '-' + format(-num, '.2f') + str_num: str = format(num, ".2f") if num >= 0 else "-" + format(-num, ".2f") - euros, cents = str_num.split('.') + euros, cents = str_num.split(".") - if cents == '00': + if cents == "00": return euros - if cents[1] == '0': - return euros + '.' + cents[0] + if cents[1] == "0": + return euros + "." + cents[0] return str_num @@ -61,7 +60,7 @@ async def get_chat_member_opt(chat: Chat, member_id: int) -> Optional[ChatMember try: return await chat.get_member(member_id) except BadRequest as e: - if e.message == 'User not found': + if e.message == "User not found": return None raise e @@ -70,7 +69,7 @@ def preband(x, xd, yd, p, func): conf = 0.95 alpha = 1.0 - conf quantile = stats.t.ppf(1.0 - alpha / 2.0, xd.size - len(p)) - stdev = np.sqrt(1. / (xd.size - len(p)) * np.sum((yd - func(xd, *p))**2)) + stdev = np.sqrt(1.0 / (xd.size - len(p)) * np.sum((yd - func(xd, *p)) ** 2)) # auxiliary definitions sx = (x - xd.mean()) ** 2 sxd = np.sum((xd - xd.mean()) ** 2) @@ -83,7 +82,9 @@ def preband(x, xd, yd, p, func): return lpb, upb -def fit_timedata(x_series: "pd.Series[np.int64]", y_series: "pd.Series[np.int64]"): # pylint: disable=too-many-locals +def fit_timedata( + x_series: "pd.Series[np.int64]", y_series: "pd.Series[np.int64]" +): # pylint: disable=too-many-locals # ignore the end date in curve fitting x = x_series.values[:-1] y = y_series.values[:-1] @@ -102,48 +103,62 @@ def f(x, slope, intercept): now = get_cur_time_hel().value end = x_series[-2] if now >= x_series[-1] else x_series[-1] - px = np.linspace(x_series[0], end, - y_series.size - 1, dtype=np.int64) - py = a*px+b + px = np.linspace(x_series[0], end, y_series.size - 1, dtype=np.int64) + py = a * px + b nom = unp.nominal_values(py) std = unp.std_devs(py) lpb, upb = preband(px, x, y, popt, f) # convert back to dates - px = [pd.to_datetime(x, unit='ns') for x in px] + px = [pd.to_datetime(x, unit="ns") for x in px] return (px, nom, std, lpb, upb) def remove_emojis(text: str) -> str: - emojis = re.compile(pattern="[" - u"\U0001F600-\U0001F64F" # emoticons - u"\U0001F300-\U0001F5FF" # symbols & pictographs - u"\U0001F680-\U0001F6FF" # transport & map symbols - u"\U0001F1E0-\U0001F1FF" # flags (iOS) - "]+", flags=re.UNICODE) - return emojis.sub(r' ', text) + emojis = re.compile( + pattern="[" + "\U0001F600-\U0001F64F" # emoticons + "\U0001F300-\U0001F5FF" # symbols & pictographs + "\U0001F680-\U0001F6FF" # transport & map symbols + "\U0001F1E0-\U0001F1FF" # flags (iOS) + "]+", + flags=re.UNICODE, + ) + return emojis.sub(r" ", text) def validate_excel(excel_path: str) -> bool: - df = pd.read_excel(excel_path, usecols='A,B,D', header=None, names=[ - 'date', 'name', 'amount'], parse_dates=True) - return (df.size > 0 and - df['date'].dtype == 'datetime64[ns]' and - df['name'].dtype == 'object' and - df['amount'].dtype in ('int64', 'float64')) - - -def read_excel_to_df(excel_path: str, - start_date: pd.Timestamp, - end_date: pd.Timestamp) -> pd.DataFrame: - df = pd.read_excel(excel_path, usecols='A,B,D', header=None, names=[ - 'date', 'name', 'amount'], parse_dates=True) - df.drop(df[df['amount'] <= 0].index, inplace=True) - df.drop(df[df['date'] > end_date].index, inplace=True) - df.drop(df[df['date'] < start_date].index, inplace=True) - df['amount'] = df['amount'] * 100 + df = pd.read_excel( + excel_path, + usecols="A,B,D", + header=None, + names=["date", "name", "amount"], + parse_dates=True, + ) + return ( + df.size > 0 + and df["date"].dtype == "datetime64[ns]" + and df["name"].dtype == "object" + and df["amount"].dtype in ("int64", "float64") + ) + + +def read_excel_to_df( + excel_path: str, start_date: pd.Timestamp, end_date: pd.Timestamp +) -> pd.DataFrame: + df = pd.read_excel( + excel_path, + usecols="A,B,D", + header=None, + names=["date", "name", "amount"], + parse_dates=True, + ) + df.drop(df[df["amount"] <= 0].index, inplace=True) + df.drop(df[df["date"] > end_date].index, inplace=True) + df.drop(df[df["date"] < start_date].index, inplace=True) + df["amount"] = df["amount"] * 100 return df @@ -151,14 +166,13 @@ def get_raffle(chat_id: int, include_df: bool = False) -> RaffleData: query_result = db.get_raffle_data(chat_id) if query_result is None: - raise NoRaffleError(f'No raffle found for chat {chat_id}') + raise NoRaffleError(f"No raffle found for chat {chat_id}") _, start_date, end_date, entry_fee, dates, entries, amounts = query_result if include_df: - df = pd.DataFrame( - data={'date': dates, 'name': entries, 'amount': amounts}) - df.set_index('date', inplace=True) + df = pd.DataFrame(data={"date": dates, "name": entries, "amount": amounts}) + df.set_index("date", inplace=True) return RaffleData(start_date, end_date, entry_fee, df) return RaffleData(start_date, end_date, entry_fee, None) @@ -167,29 +181,31 @@ def get_raffle(chat_id: int, include_df: bool = False) -> RaffleData: def get_cur_time_hel() -> pd.Timestamp: # take current time in helsinki and convert it to naive time, # as mobilepay times are naive (naive = no timezone specified). - helsinki_tz = pytz.timezone('Europe/Helsinki') + helsinki_tz = pytz.timezone("Europe/Helsinki") cur_time_hel = pd.Timestamp.utcnow().astimezone(helsinki_tz).replace(tzinfo=None) return cur_time_hel -def save_raffle(chat_id: int, - start_date: pd.Timestamp, - end_date: pd.Timestamp, - entry_fee: int, - df: pd.DataFrame) -> None: +def save_raffle( + chat_id: int, + start_date: pd.Timestamp, + end_date: pd.Timestamp, + entry_fee: int, + df: pd.DataFrame, +) -> None: db.save_raffle_data(chat_id, start_date, end_date, entry_fee, df) def parse_df_essentials(raffle_data: RaffleData) -> RaffleData: start_date, end_date, fee, df = raffle_data - df.at[start_date, 'amount'] = 0 + df.at[start_date, "amount"] = 0 - df['datenum'] = pd.to_numeric(df.index.values) - df = df.sort_values('datenum') - df['amount'] = df['amount'].cumsum().astype(int) - df['unique'] = (~df['name'].duplicated()).cumsum() - 1 + df["datenum"] = pd.to_numeric(df.index.values) + df = df.sort_values("datenum") + df["amount"] = df["amount"].cumsum().astype(int) + df["unique"] = (~df["name"].duplicated()).cumsum() - 1 return RaffleData(start_date, end_date, fee, df) @@ -197,10 +213,18 @@ def parse_df_essentials(raffle_data: RaffleData) -> RaffleData: def parse_expected(raffle_data: RaffleData) -> RaffleData: start_date, end_date, entry_fee, df = parse_df_essentials(raffle_data) - df['win_odds'] = 1.0 / df['unique'] - df['next_expected'] = ((- entry_fee * (1 - df['win_odds']) - + (df['amount'] - entry_fee) * df['win_odds']) - ).fillna(0).round().astype(int) + df["win_odds"] = 1.0 / df["unique"] + df["next_expected"] = ( + ( + ( + -entry_fee * (1 - df["win_odds"]) + + (df["amount"] - entry_fee) * df["win_odds"] + ) + ) + .fillna(0) + .round() + .astype(int) + ) return RaffleData(start_date, end_date, entry_fee, df) @@ -208,11 +232,10 @@ def parse_expected(raffle_data: RaffleData) -> RaffleData: def parse_graph(raffle_data: RaffleData) -> RaffleData: df = raffle_data.df - df.at[get_cur_time_hel(), 'amount'] = 0 - df.at[raffle_data.end_date, 'amount'] = 0 + df.at[get_cur_time_hel(), "amount"] = 0 + df.at[raffle_data.end_date, "amount"] = 0 - parsed_raffle_data = parse_df_essentials( - raffle_data._replace(df=df)) + parsed_raffle_data = parse_df_essentials(raffle_data._replace(df=df)) return parsed_raffle_data @@ -224,14 +247,13 @@ def configure_and_save_plot(out_img_path: str) -> None: ax.legend() # format axis - ax.xaxis.set_major_formatter(mdates.DateFormatter('%d.%m. %H:%M')) + ax.xaxis.set_major_formatter(mdates.DateFormatter("%d.%m. %H:%M")) ax.yaxis.set_major_formatter(lambda x, _: int_price_to_str(x)) # set grid ax.xaxis.set_minor_locator(AutoMinorLocator(2)) ax.yaxis.set_minor_locator(AutoMinorLocator(2)) - plt.grid(visible=True, which='major', - axis='both', linestyle='--', linewidth=0.5) + plt.grid(visible=True, which="major", axis="both", linestyle="--", linewidth=0.5) if not os.path.exists(os.path.dirname(out_img_path)): os.makedirs(os.path.dirname(out_img_path)) @@ -240,50 +262,47 @@ def configure_and_save_plot(out_img_path: str) -> None: plt.clf() -def generate_graph(out_img_path: str, - chat_id: int, - chat_title: str) -> None: - +def generate_graph(out_img_path: str, chat_id: int, chat_title: str) -> None: # -- get raffle data -- raffle_data = get_raffle(chat_id, include_df=True) # -- parse and fit data -- start_date, end_date, _, df = parse_graph(raffle_data) - px, nom, std, lpb, upb = fit_timedata(df['datenum'], df['amount']) + px, nom, std, lpb, upb = fit_timedata(df["datenum"], df["amount"]) # -- plot -- # clear previous plot in case of leftovers plt.clf() ax = plt.gca() # plot data - df['amount'][:-1].plot(ax=ax, marker='o', style='r', label='Pool') + df["amount"][:-1].plot(ax=ax, marker="o", style="r", label="Pool") # plot regression - ax.plot(px, nom, '-', color='black', label='y=ax+b') + ax.plot(px, nom, "-", color="black", label="y=ax+b") # uncertainty lines (95% conf) - ax.plot(px, nom-1.96*std, c='orange', label='95% confidence region') - ax.plot(px, nom+1.96*std, c='orange') + ax.plot(px, nom - 1.96 * std, c="orange", label="95% confidence region") + ax.plot(px, nom + 1.96 * std, c="orange") # prediction band (95% conf) - ax.plot(px, lpb, 'k--', label='95% prediction band') - ax.plot(px, upb, 'k--') + ax.plot(px, lpb, "k--", label="95% prediction band") + ax.plot(px, upb, "k--") # -- style graph -- - pred_max_pool = (nom+1.96*std)[-1] - pool_total = df['amount'].max() + pred_max_pool = (nom + 1.96 * std)[-1] + pool_total = df["amount"].max() plt.ylim(0, max(pred_max_pool, pool_total)) plt.xlim((pd.to_datetime(start_date), pd.to_datetime(end_date))) - plt.title(str(remove_emojis(chat_title).strip()) + "\n" + - f"Entries {df['unique'].max()} | Pool {int_price_to_str(pool_total)} €") + plt.title( + str(remove_emojis(chat_title).strip()) + + "\n" + + f"Entries {df['unique'].max()} | Pool {int_price_to_str(pool_total)} €" + ) plt.xlabel(None) - plt.ylabel('Pool (€)') + plt.ylabel("Pool (€)") configure_and_save_plot(out_img_path) -def generate_expected(out_img_path: str, - chat_id: int, - chat_title: str) -> None: - +def generate_expected(out_img_path: str, chat_id: int, chat_title: str) -> None: # -- get raffle data -- raffle_data = get_raffle(chat_id, include_df=True) @@ -295,18 +314,21 @@ def generate_expected(out_img_path: str, plt.clf() ax = plt.gca() - df['next_expected'].plot( - ax=ax, marker='o', style='r', label='Expected Value') + df["next_expected"].plot(ax=ax, marker="o", style="r", label="Expected Value") # -- style graph -- - plt.ylim(float(int_price_to_str((df['next_expected'].min() - 100) * 110)), - float(int_price_to_str((df['next_expected'].max() + 100) * 110))) + plt.ylim( + float(int_price_to_str((df["next_expected"].min() - 100) * 110)), + float(int_price_to_str((df["next_expected"].max() + 100) * 110)), + ) plt.xlim((pd.to_datetime(start_date), pd.to_datetime(get_cur_time_hel()))) - plt.title(str(remove_emojis(chat_title).strip()) + - f' | Fee {int_price_to_str(entry_fee)} €\n' + - f"Expected Value { int_price_to_str(df['next_expected'].iloc[-1])} €") + plt.title( + str(remove_emojis(chat_title).strip()) + + f" | Fee {int_price_to_str(entry_fee)} €\n" + + f"Expected Value { int_price_to_str(df['next_expected'].iloc[-1])} €" + ) plt.xlabel(None) - plt.ylabel('Expected Value (€)') + plt.ylabel("Expected Value (€)") configure_and_save_plot(out_img_path) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..b0acdf8 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1466 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "3.6.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16,<0.22)"] + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "black" +version = "23.3.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, + {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, + {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, + {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, + {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, + {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, + {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, + {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, + {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, + {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, + {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "contourpy" +version = "1.0.7" +description = "Python library for calculating contours of 2D quadrilateral grids" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d"}, + {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab"}, + {file = "contourpy-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6"}, + {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803"}, + {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8"}, + {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0"}, + {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6"}, + {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd"}, + {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69"}, + {file = "contourpy-1.0.7-cp310-cp310-win32.whl", hash = "sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3"}, + {file = "contourpy-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80"}, + {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71"}, + {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5"}, + {file = "contourpy-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414"}, + {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2"}, + {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02"}, + {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae"}, + {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc"}, + {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac"}, + {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566"}, + {file = "contourpy-1.0.7-cp311-cp311-win32.whl", hash = "sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0"}, + {file = "contourpy-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350"}, + {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad"}, + {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772"}, + {file = "contourpy-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f"}, + {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98"}, + {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc"}, + {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd"}, + {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810"}, + {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c"}, + {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc"}, + {file = "contourpy-1.0.7-cp38-cp38-win32.whl", hash = "sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66"}, + {file = "contourpy-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556"}, + {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4"}, + {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1"}, + {file = "contourpy-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5"}, + {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3"}, + {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50"}, + {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2"}, + {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a"}, + {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2"}, + {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967"}, + {file = "contourpy-1.0.7-cp39-cp39-win32.whl", hash = "sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693"}, + {file = "contourpy-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4"}, + {file = "contourpy-1.0.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161"}, + {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566"}, + {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436"}, + {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2"}, + {file = "contourpy-1.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9"}, + {file = "contourpy-1.0.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f"}, + {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d"}, + {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051"}, + {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa"}, + {file = "contourpy-1.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd"}, + {file = "contourpy-1.0.7.tar.gz", hash = "sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e"}, +] + +[package.dependencies] +numpy = ">=1.16" + +[package.extras] +bokeh = ["bokeh", "chromedriver", "selenium"] +docs = ["furo", "sphinx-copybutton"] +mypy = ["contourpy[bokeh]", "docutils-stubs", "mypy (==0.991)", "types-Pillow"] +test = ["Pillow", "matplotlib", "pytest"] +test-no-images = ["pytest"] + +[[package]] +name = "cycler" +version = "0.11.0" +description = "Composable style cycles" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, + {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, +] + +[[package]] +name = "et-xmlfile" +version = "1.1.0" +description = "An implementation of lxml.xmlfile for the standard library" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"}, + {file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.1" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, + {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fonttools" +version = "4.39.3" +description = "Tools to manipulate font files" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.39.3-py3-none-any.whl", hash = "sha256:64c0c05c337f826183637570ac5ab49ee220eec66cf50248e8df527edfa95aeb"}, + {file = "fonttools-4.39.3.zip", hash = "sha256:9234b9f57b74e31b192c3fc32ef1a40750a8fbc1cd9837a7b7bfc4ca4a5c51d7"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "scipy"] +lxml = ["lxml (>=4.0,<5)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.0.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "future" +version = "0.18.3" +description = "Clean single-source support for Python 3 and 2" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "0.16.3" +description = "A minimal low-level HTTP client." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, + {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, +] + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.23.3" +description = "The next generation HTTP client." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, + {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, +] + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.17.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "importlab" +version = "0.8" +description = "A library to calculate python dependency graphs." +category = "dev" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "importlab-0.8-py2.py3-none-any.whl", hash = "sha256:a009ccde7b549b16f3e6b034fea748febc8d45ded9e8a09370a8f994acfda25b"}, + {file = "importlab-0.8.tar.gz", hash = "sha256:b24b3aac3b073966ae42fb2d3a7764f3377b30bb72c0d411fe29134cc9276e86"}, +] + +[package.dependencies] +networkx = ">=2" + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "kiwisolver" +version = "1.4.4" +description = "A fast implementation of the Cassowary constraint solver" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"}, + {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"}, + {file = "kiwisolver-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32"}, + {file = "kiwisolver-1.4.4-cp310-cp310-win32.whl", hash = "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408"}, + {file = "kiwisolver-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004"}, + {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6"}, + {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2"}, + {file = "kiwisolver-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5"}, + {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750"}, + {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4"}, + {file = "kiwisolver-1.4.4-cp311-cp311-win32.whl", hash = "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e"}, + {file = "kiwisolver-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-win32.whl", hash = "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166"}, + {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454"}, + {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0"}, + {file = "kiwisolver-1.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c"}, + {file = "kiwisolver-1.4.4-cp38-cp38-win32.whl", hash = "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191"}, + {file = "kiwisolver-1.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766"}, + {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8"}, + {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897"}, + {file = "kiwisolver-1.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9"}, + {file = "kiwisolver-1.4.4-cp39-cp39-win32.whl", hash = "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea"}, + {file = "kiwisolver-1.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb"}, + {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2"}, + {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b"}, + {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, +] + +[[package]] +name = "libcst" +version = "0.4.9" +description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 programs." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "libcst-0.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f9e42085c403e22201e5c41e707ef73e4ea910ad9fc67983ceee2368097f54e"}, + {file = "libcst-0.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1266530bf840cc40633a04feb578bb4cac1aa3aea058cc3729e24eab09a8e996"}, + {file = "libcst-0.4.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9679177391ccb9b0cdde3185c22bf366cb672457c4b7f4031fcb3b5e739fbd6"}, + {file = "libcst-0.4.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d67bc87e0d8db9434f2ea063734938a320f541f4c6da1074001e372f840f385d"}, + {file = "libcst-0.4.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e316da5a126f2a9e1d7680f95f907b575f082a35e2f8bd5620c59b2aaaebfe0a"}, + {file = "libcst-0.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:7415569ab998a85b0fc9af3a204611ea7fadb2d719a12532c448f8fc98f5aca4"}, + {file = "libcst-0.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:15ded11ff7f4572f91635e02b519ae959f782689fdb4445bbebb7a3cc5c71d75"}, + {file = "libcst-0.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b266867b712a120fad93983de432ddb2ccb062eb5fd2bea748c9a94cb200c36"}, + {file = "libcst-0.4.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045b3b0b06413cdae6e9751b5f417f789ffa410f2cb2815e3e0e0ea6bef10ec0"}, + {file = "libcst-0.4.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e799add8fba4976628b9c1a6768d73178bf898f0ed1bd1322930c2d3db9063ba"}, + {file = "libcst-0.4.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10479371d04ee8dc978c889c1774bbf6a83df88fa055fcb0159a606f6679c565"}, + {file = "libcst-0.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:7a98286cbbfa90a42d376900c875161ad02a5a2a6b7c94c0f7afd9075e329ce4"}, + {file = "libcst-0.4.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:400166fc4efb9aa06ce44498d443aa78519082695b1894202dd73cd507d2d712"}, + {file = "libcst-0.4.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46123863fba35cc84f7b54dd68826419cabfd9504d8a101c7fe3313ea03776f9"}, + {file = "libcst-0.4.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27be8db54c0e5fe440021a771a38b81a7dbc23cd630eb8b0e9828b7717f9b702"}, + {file = "libcst-0.4.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:132bec627b064bd567e7e4cd6c89524d02842151eb0d8f5f3f7ffd2579ec1b09"}, + {file = "libcst-0.4.9-cp37-cp37m-win_amd64.whl", hash = "sha256:596860090aeed3ee6ad1e59c35c6c4110a57e4e896abf51b91cae003ec720a11"}, + {file = "libcst-0.4.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4487608258109f774300466d4ca97353df29ae6ac23d1502e13e5509423c9d5"}, + {file = "libcst-0.4.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa53993e9a2853efb3ed3605da39f2e7125df6430f613eb67ef886c1ce4f94b5"}, + {file = "libcst-0.4.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6ce794483d4c605ef0f5b199a49fb6996f9586ca938b7bfef213bd13858d7ab"}, + {file = "libcst-0.4.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:786e562b54bbcd17a060d1244deeef466b7ee07fe544074c252c4a169e38f1ee"}, + {file = "libcst-0.4.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794250d2359edd518fb698e5d21c38a5bdfc5e4a75d0407b4c19818271ce6742"}, + {file = "libcst-0.4.9-cp38-cp38-win_amd64.whl", hash = "sha256:76491f67431318c3145442e97dddcead7075b074c59eac51be7cc9e3fffec6ee"}, + {file = "libcst-0.4.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3cf48d7aec6dc54b02aec0b1bb413c5bb3b02d852fd6facf1f05c7213e61a176"}, + {file = "libcst-0.4.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b3348c6b7711a5235b133bd8e11d22e903c388db42485b8ceb5f2aa0fae9b9f"}, + {file = "libcst-0.4.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e33b66762efaa014c38819efae5d8f726dd823e32d5d691035484411d2a2a69"}, + {file = "libcst-0.4.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1350d375d3fb9b20a6cf10c09b2964baca9be753a033dde7c1aced49d8e58387"}, + {file = "libcst-0.4.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3822056dc13326082362db35b3f649e0f4a97e36ddb4e487441da8e0fb9db7b3"}, + {file = "libcst-0.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:183636141b839aa35b639e100883813744523bc7c12528906621121731b28443"}, + {file = "libcst-0.4.9.tar.gz", hash = "sha256:01786c403348f76f274dbaf3888ae237ffb73e6ed6973e65eba5c1fc389861dd"}, +] + +[package.dependencies] +pyyaml = ">=5.2" +typing-extensions = ">=3.7.4.2" +typing-inspect = ">=0.4.0" + +[package.extras] +dev = ["Sphinx (>=5.1.1)", "black (==22.10.0)", "coverage (>=4.5.4)", "fixit (==0.1.1)", "flake8 (>=3.7.8,<5)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.2)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<0.14)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.9)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.0.1)", "usort (==1.0.5)"] + +[[package]] +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] + +[[package]] +name = "matplotlib" +version = "3.7.1" +description = "Python plotting package" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:95cbc13c1fc6844ab8812a525bbc237fa1470863ff3dace7352e910519e194b1"}, + {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:08308bae9e91aca1ec6fd6dda66237eef9f6294ddb17f0d0b3c863169bf82353"}, + {file = "matplotlib-3.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:544764ba51900da4639c0f983b323d288f94f65f4024dc40ecb1542d74dc0500"}, + {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d94989191de3fcc4e002f93f7f1be5da476385dde410ddafbb70686acf00ea"}, + {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99bc9e65901bb9a7ce5e7bb24af03675cbd7c70b30ac670aa263240635999a4"}, + {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb7d248c34a341cd4c31a06fd34d64306624c8cd8d0def7abb08792a5abfd556"}, + {file = "matplotlib-3.7.1-cp310-cp310-win32.whl", hash = "sha256:ce463ce590f3825b52e9fe5c19a3c6a69fd7675a39d589e8b5fbe772272b3a24"}, + {file = "matplotlib-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d7bc90727351fb841e4d8ae620d2d86d8ed92b50473cd2b42ce9186104ecbba"}, + {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:770a205966d641627fd5cf9d3cb4b6280a716522cd36b8b284a8eb1581310f61"}, + {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f67bfdb83a8232cb7a92b869f9355d677bce24485c460b19d01970b64b2ed476"}, + {file = "matplotlib-3.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2bf092f9210e105f414a043b92af583c98f50050559616930d884387d0772aba"}, + {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89768d84187f31717349c6bfadc0e0d8c321e8eb34522acec8a67b1236a66332"}, + {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83111e6388dec67822e2534e13b243cc644c7494a4bb60584edbff91585a83c6"}, + {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a867bf73a7eb808ef2afbca03bcdb785dae09595fbe550e1bab0cd023eba3de0"}, + {file = "matplotlib-3.7.1-cp311-cp311-win32.whl", hash = "sha256:fbdeeb58c0cf0595efe89c05c224e0a502d1aa6a8696e68a73c3efc6bc354304"}, + {file = "matplotlib-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:c0bd19c72ae53e6ab979f0ac6a3fafceb02d2ecafa023c5cca47acd934d10be7"}, + {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:6eb88d87cb2c49af00d3bbc33a003f89fd9f78d318848da029383bfc08ecfbfb"}, + {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:cf0e4f727534b7b1457898c4f4ae838af1ef87c359b76dcd5330fa31893a3ac7"}, + {file = "matplotlib-3.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:46a561d23b91f30bccfd25429c3c706afe7d73a5cc64ef2dfaf2b2ac47c1a5dc"}, + {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8704726d33e9aa8a6d5215044b8d00804561971163563e6e6591f9dcf64340cc"}, + {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4cf327e98ecf08fcbb82685acaf1939d3338548620ab8dfa02828706402c34de"}, + {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:617f14ae9d53292ece33f45cba8503494ee199a75b44de7717964f70637a36aa"}, + {file = "matplotlib-3.7.1-cp38-cp38-win32.whl", hash = "sha256:7c9a4b2da6fac77bcc41b1ea95fadb314e92508bf5493ceff058e727e7ecf5b0"}, + {file = "matplotlib-3.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:14645aad967684e92fc349493fa10c08a6da514b3d03a5931a1bac26e6792bd1"}, + {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:81a6b377ea444336538638d31fdb39af6be1a043ca5e343fe18d0f17e098770b"}, + {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:28506a03bd7f3fe59cd3cd4ceb2a8d8a2b1db41afede01f66c42561b9be7b4b7"}, + {file = "matplotlib-3.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8c587963b85ce41e0a8af53b9b2de8dddbf5ece4c34553f7bd9d066148dc719c"}, + {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bf26ade3ff0f27668989d98c8435ce9327d24cffb7f07d24ef609e33d582439"}, + {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:def58098f96a05f90af7e92fd127d21a287068202aa43b2a93476170ebd99e87"}, + {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f883a22a56a84dba3b588696a2b8a1ab0d2c3d41be53264115c71b0a942d8fdb"}, + {file = "matplotlib-3.7.1-cp39-cp39-win32.whl", hash = "sha256:4f99e1b234c30c1e9714610eb0c6d2f11809c9c78c984a613ae539ea2ad2eb4b"}, + {file = "matplotlib-3.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:3ba2af245e36990facf67fde840a760128ddd71210b2ab6406e640188d69d136"}, + {file = "matplotlib-3.7.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3032884084f541163f295db8a6536e0abb0db464008fadca6c98aaf84ccf4717"}, + {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a2cb34336110e0ed8bb4f650e817eed61fa064acbefeb3591f1b33e3a84fd96"}, + {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b867e2f952ed592237a1828f027d332d8ee219ad722345b79a001f49df0936eb"}, + {file = "matplotlib-3.7.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:57bfb8c8ea253be947ccb2bc2d1bb3862c2bccc662ad1b4626e1f5e004557042"}, + {file = "matplotlib-3.7.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:438196cdf5dc8d39b50a45cb6e3f6274edbcf2254f85fa9b895bf85851c3a613"}, + {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21e9cff1a58d42e74d01153360de92b326708fb205250150018a52c70f43c290"}, + {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d4725d70b7c03e082bbb8a34639ede17f333d7247f56caceb3801cb6ff703d"}, + {file = "matplotlib-3.7.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:97cc368a7268141afb5690760921765ed34867ffb9655dd325ed207af85c7529"}, + {file = "matplotlib-3.7.1.tar.gz", hash = "sha256:7b73305f25eab4541bd7ee0b96d87e53ae9c9f1823be5659b806cd85786fe882"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.0.1" +numpy = ">=1.20" +packaging = ">=20.0" +pillow = ">=6.2.0" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "networkx" +version = "2.8.3" +description = "Python package for creating and manipulating graphs and networks" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "networkx-2.8.3-py3-none-any.whl", hash = "sha256:f151edac6f9b0cf11fecce93e236ac22b499bb9ff8d6f8393b9fef5ad09506cc"}, + {file = "networkx-2.8.3.tar.gz", hash = "sha256:67fab04a955a73eb660fe7bf281b6fa71a003bc6e23a92d2f6227654c5223dbe"}, +] + +[package.extras] +default = ["matplotlib (>=3.4)", "numpy (>=1.19)", "pandas (>=1.3)", "scipy (>=1.8)"] +developer = ["mypy (>=0.960)", "pre-commit (>=2.19)"] +doc = ["nb2plots (>=0.6)", "numpydoc (>=1.3)", "pillow (>=9.1)", "pydata-sphinx-theme (>=0.8.1)", "sphinx (>=4.5)", "sphinx-gallery (>=0.10)", "texext (>=0.6.6)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.9)", "sympy (>=1.10)"] +test = ["codecov (>=2.1)", "pytest (>=7.1)", "pytest-cov (>=3.0)"] + +[[package]] +name = "ninja" +version = "1.11.1" +description = "Ninja is a small build system with a focus on speed" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "ninja-1.11.1-py2.py3-none-macosx_10_9_universal2.macosx_10_9_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:f48c3c6eea204062f6bbf089dfc63e1ad41a08640e1da46ef2b30fa426f7ce23"}, + {file = "ninja-1.11.1-py2.py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:edec1053e141253076b2df7ec03a246ff581e9270aa1ca9759397b21e2760e57"}, + {file = "ninja-1.11.1-py2.py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:817e2aee2a4d28a708a67bcfba1817ae502c32c6d8ef80e50d63b0f23adf3a08"}, + {file = "ninja-1.11.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df11b8afea0501883e33faeb1c43d2ef67f466d5f4bd85f9c376e9a93a43a277"}, + {file = "ninja-1.11.1-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a564fe755ddfbdbccb07b0b758e3f8460e5f8ba1adaab40a5eaa2f8c01ce68"}, + {file = "ninja-1.11.1-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c474326e11fba3f8c2582715d79216292e327d3335367c0e87e9647a002cc4a"}, + {file = "ninja-1.11.1-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f6465a7efe6473a2a34edab83633594de19d59406a727316e1367ebcc528908"}, + {file = "ninja-1.11.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:642cb64d859276998f14972724850e0c5b7febbc1bce3d2065b7e0cb7d3a0b79"}, + {file = "ninja-1.11.1-py2.py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:60179bb4f22c88279c53a5402bb5fe81c97c627a28d93c737d1fa067d892115d"}, + {file = "ninja-1.11.1-py2.py3-none-musllinux_1_1_i686.whl", hash = "sha256:34753459493543782d87267e4cad63dd4639b07f8394ffe6d4417e9eda05c8a8"}, + {file = "ninja-1.11.1-py2.py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:779f228e407c54a8b6e4cbf8f835489998dd250f67bf1b9bd7b8a8ab6bdcdc7b"}, + {file = "ninja-1.11.1-py2.py3-none-musllinux_1_1_s390x.whl", hash = "sha256:ba50a32424912e5f3ee40d791b506a160dc0eeda7de5ad8faebe7aa8006244dc"}, + {file = "ninja-1.11.1-py2.py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3b28b595ed580752240ade7821b6cb7a5a4c6a604c865dc474bd38f06e2eb7f5"}, + {file = "ninja-1.11.1-py2.py3-none-win32.whl", hash = "sha256:3329b4b7c1694730772522a3ba0ba40fd15c012476ed3e1c9f0fd9e76190394e"}, + {file = "ninja-1.11.1-py2.py3-none-win_amd64.whl", hash = "sha256:4e547bc759c570773d83d110c41fd5ca9a94c0a9a8388f5a3ea37bdf97d002b0"}, + {file = "ninja-1.11.1-py2.py3-none-win_arm64.whl", hash = "sha256:8cf96f92ccc851c600cb3e1251c34db06f1dd682de79188ad490c33cddc66981"}, + {file = "ninja-1.11.1.tar.gz", hash = "sha256:c833a47d39b2d1eee3f9ca886fa1581efd5be6068b82734ac229961ee8748f90"}, +] + +[package.extras] +test = ["codecov (>=2.0.5)", "coverage (>=4.2)", "flake8 (>=3.0.4)", "pytest (>=4.5.0)", "pytest-cov (>=2.7.1)", "pytest-runner (>=5.1)", "pytest-virtualenv (>=1.7.0)", "virtualenv (>=15.0.3)"] + +[[package]] +name = "numpy" +version = "1.24.2" +description = "Fundamental package for array computing in Python" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "numpy-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d"}, + {file = "numpy-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5"}, + {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253"}, + {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978"}, + {file = "numpy-1.24.2-cp310-cp310-win32.whl", hash = "sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9"}, + {file = "numpy-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0"}, + {file = "numpy-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a"}, + {file = "numpy-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0"}, + {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281"}, + {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910"}, + {file = "numpy-1.24.2-cp311-cp311-win32.whl", hash = "sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95"}, + {file = "numpy-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04"}, + {file = "numpy-1.24.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2"}, + {file = "numpy-1.24.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5"}, + {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a"}, + {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96"}, + {file = "numpy-1.24.2-cp38-cp38-win32.whl", hash = "sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d"}, + {file = "numpy-1.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756"}, + {file = "numpy-1.24.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a"}, + {file = "numpy-1.24.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f"}, + {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb"}, + {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780"}, + {file = "numpy-1.24.2-cp39-cp39-win32.whl", hash = "sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468"}, + {file = "numpy-1.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5"}, + {file = "numpy-1.24.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d"}, + {file = "numpy-1.24.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa"}, + {file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"}, + {file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"}, +] + +[[package]] +name = "openpyxl" +version = "3.1.2" +description = "A Python library to read/write Excel 2010 xlsx/xlsm files" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "openpyxl-3.1.2-py2.py3-none-any.whl", hash = "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5"}, + {file = "openpyxl-3.1.2.tar.gz", hash = "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184"}, +] + +[package.dependencies] +et-xmlfile = "*" + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pandas" +version = "2.0.0" +description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pandas-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bbb2c5e94d6aa4e632646a3bacd05c2a871c3aa3e85c9bec9be99cb1267279f2"}, + {file = "pandas-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b5337c87c4e963f97becb1217965b6b75c6fe5f54c4cf09b9a5ac52fc0bd03d3"}, + {file = "pandas-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ded51f7e3dd9b4f8b87f2ceb7bd1a8df2491f7ee72f7074c6927a512607199e"}, + {file = "pandas-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c858de9e9fc422d25e67e1592a6e6135d7bcf9a19fcaf4d0831a0be496bf21"}, + {file = "pandas-2.0.0-cp310-cp310-win32.whl", hash = "sha256:2d1d138848dd71b37e3cbe7cd952ff84e2ab04d8988972166e18567dcc811245"}, + {file = "pandas-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:d08e41d96bc4de6f500afe80936c68fce6099d5a434e2af7c7fd8e7c72a3265d"}, + {file = "pandas-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:24472cfc7ced511ac90608728b88312be56edc8f19b9ed885a7d2e47ffaf69c0"}, + {file = "pandas-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ffb14f50c74ee541610668137830bb93e9dfa319b1bef2cedf2814cd5ac9c70"}, + {file = "pandas-2.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c24c7d12d033a372a9daf9ff2c80f8b0af6f98d14664dbb0a4f6a029094928a7"}, + {file = "pandas-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8318de0f886e4dcb8f9f36e45a3d6a6c3d1cfdc508354da85e739090f0222991"}, + {file = "pandas-2.0.0-cp311-cp311-win32.whl", hash = "sha256:57c34b79c13249505e850d0377b722961b99140f81dafbe6f19ef10239f6284a"}, + {file = "pandas-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f987ec26e96a8490909bc5d98c514147236e49830cba7df8690f6087c12bbae"}, + {file = "pandas-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b3ba8f5dd470d8bfbc4259829589f4a32881151c49e36384d9eb982b35a12020"}, + {file = "pandas-2.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcd471c9d9f60926ab2f15c6c29164112f458acb42280365fbefa542d0c2fc74"}, + {file = "pandas-2.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9253edfd015520ce77a9343eb7097429479c039cd3ebe81d7810ea11b4b24695"}, + {file = "pandas-2.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977326039bd1ded620001a1889e2ed4798460a6bc5a24fbaebb5f07a41c32a55"}, + {file = "pandas-2.0.0-cp38-cp38-win32.whl", hash = "sha256:78425ca12314b23356c28b16765639db10ebb7d8983f705d6759ff7fe41357fa"}, + {file = "pandas-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:d93b7fcfd9f3328072b250d6d001dcfeec5d3bb66c1b9c8941e109a46c0c01a8"}, + {file = "pandas-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:425705cee8be54db2504e8dd2a730684790b15e5904b750c367611ede49098ab"}, + {file = "pandas-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f789b7c012a608c08cda4ff0872fd979cb18907a37982abe884e6f529b8793"}, + {file = "pandas-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bb9d840bf15656805f6a3d87eea9dcb7efdf1314a82adcf7f00b820427c5570"}, + {file = "pandas-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0778ab54c8f399d83d98ffb674d11ec716449956bc6f6821891ab835848687f2"}, + {file = "pandas-2.0.0-cp39-cp39-win32.whl", hash = "sha256:70db5c278bbec0306d32bf78751ff56b9594c05a5098386f6c8a563659124f91"}, + {file = "pandas-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f3320bb55f34af4193020158ef8118ee0fb9aec7cc47d2084dbfdd868a0a24f"}, + {file = "pandas-2.0.0.tar.gz", hash = "sha256:cda9789e61b44463c1c4fe17ef755de77bcd13b09ba31c940d20f193d63a5dc8"}, +] + +[package.dependencies] +numpy = {version = ">=1.21.0", markers = "python_version >= \"3.10\""} +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.1" + +[package.extras] +all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"] +aws = ["s3fs (>=2021.08.0)"] +clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"] +compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"] +computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"] +feather = ["pyarrow (>=7.0.0)"] +fss = ["fsspec (>=2021.07.0)"] +gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"] +hdf5 = ["tables (>=3.6.1)"] +html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"] +mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"] +parquet = ["pyarrow (>=7.0.0)"] +performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"] +plot = ["matplotlib (>=3.6.1)"] +postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"] +spss = ["pyreadstat (>=1.1.2)"] +sql-other = ["SQLAlchemy (>=1.4.16)"] +test = ["hypothesis (>=6.34.2)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.6.3)"] + +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] + +[[package]] +name = "pathspec" +version = "0.11.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, + {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, +] + +[[package]] +name = "pillow" +version = "9.5.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, + {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, + {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, + {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, + {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, + {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, + {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "platformdirs" +version = "3.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, + {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "poethepoet" +version = "0.19.0" +description = "A task runner that works well with poetry." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "poethepoet-0.19.0-py3-none-any.whl", hash = "sha256:87038be589077e4b407050a9da644d9cd9e4076ccfc8abc7f855cf6870d5c6c2"}, + {file = "poethepoet-0.19.0.tar.gz", hash = "sha256:897eb85ec15876d79befc7d19d4c80ce7c8b214d1bb0dcfec640abd81616bfed"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +tomli = ">=1.2.2" + +[package.extras] +poetry-plugin = ["poetry (>=1.0,<2.0)"] + +[[package]] +name = "psycopg" +version = "3.1.8" +description = "PostgreSQL database adapter for Python" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "psycopg-3.1.8-py3-none-any.whl", hash = "sha256:b1500c42063abaa01d30b056f0b300826b8dd8d586900586029a294ce74af327"}, + {file = "psycopg-3.1.8.tar.gz", hash = "sha256:59b4a71536b146925513c0234dfd1dc42b81e65d56ce5335dff4813434dbc113"}, +] + +[package.dependencies] +typing-extensions = ">=4.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (>=3.1.6,<=3.1.8)"] +c = ["psycopg-c (>=3.1.6,<=3.1.8)"] +dev = ["black (>=22.3.0)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=0.990)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["mypy (>=0.990)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-asyncio (>=0.17)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "pydot" +version = "1.4.2" +description = "Python interface to Graphviz's Dot" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pydot-1.4.2-py2.py3-none-any.whl", hash = "sha256:66c98190c65b8d2e2382a441b4c0edfdb4f4c025ef9cb9874de478fb0793a451"}, + {file = "pydot-1.4.2.tar.gz", hash = "sha256:248081a39bcb56784deb018977e428605c1c758f10897a339fce1dd728ff007d"}, +] + +[package.dependencies] +pyparsing = ">=2.1.4" + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "7.3.1" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"}, + {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.0.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, + {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-telegram-bot" +version = "20.2" +description = "We have made you a wrapper you can't refuse" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "python-telegram-bot-20.2.tar.gz", hash = "sha256:d0aa53e1f06d7cb7919cc0e2d6c81a02d968fc29921aeaa962edd1efb816a9bd"}, + {file = "python_telegram_bot-20.2-py3-none-any.whl", hash = "sha256:4d1d4b643ce158aa17a0987b84005eaf25fe0ce8b38fd234099594985611c198"}, +] + +[package.dependencies] +httpx = ">=0.23.3,<0.24.0" + +[package.extras] +all = ["APScheduler (>=3.10.1,<3.11.0)", "aiolimiter (>=1.0.0,<1.1.0)", "cachetools (>=5.3.0,<5.4.0)", "cryptography (>=39.0.1)", "httpx[http2]", "httpx[socks]", "pytz (>=2018.6)", "tornado (>=6.2,<7.0)"] +callback-data = ["cachetools (>=5.3.0,<5.4.0)"] +ext = ["APScheduler (>=3.10.1,<3.11.0)", "aiolimiter (>=1.0.0,<1.1.0)", "cachetools (>=5.3.0,<5.4.0)", "pytz (>=2018.6)", "tornado (>=6.2,<7.0)"] +http2 = ["httpx[http2]"] +job-queue = ["APScheduler (>=3.10.1,<3.11.0)", "pytz (>=2018.6)"] +passport = ["cryptography (>=39.0.1)"] +rate-limiter = ["aiolimiter (>=1.0.0,<1.1.0)"] +socks = ["httpx[socks]"] +webhooks = ["tornado (>=6.2,<7.0)"] + +[[package]] +name = "pytype" +version = "2023.4.11" +description = "Python type inferencer" +category = "dev" +optional = false +python-versions = "<3.11,>=3.7" +files = [ + {file = "pytype-2023.4.11-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:2079e3d1618f8b790ad0533445dfc5389c06b39378603b29fb1abbf2ac2d532b"}, + {file = "pytype-2023.4.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb37bfb8fddf108359f89a6ae7560343561aa3e8cbc5d4445f6b766cd8971e2c"}, + {file = "pytype-2023.4.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cdb329e1bc9819cca1bf1e00b608c417ef1cec0bd2b1e62ea3834c13c3bd14a"}, + {file = "pytype-2023.4.11-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:0698c1663d103298f6112d1da1d2c4532a22f2c69f9dc4c0021ddc4f4b12401d"}, + {file = "pytype-2023.4.11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4afd2f4bcdd1118948d7696d9fea57d853797bf31898ee58c399680138ec38d3"}, + {file = "pytype-2023.4.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83aed054788d66d3a0cd05a0f34364a102f64eefcdd573d4119bee5122c245d6"}, + {file = "pytype-2023.4.11-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:7f053490736d2ac0c3d3f3e46e7b54354b4277387103d783467416d275a8ae9d"}, + {file = "pytype-2023.4.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44eeb17a8288c20ed04ffa9d09daf2e009997518e60da66f2b50a82459434185"}, + {file = "pytype-2023.4.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9209c187bf109f46dd57201656dcd7bd1a9cd6378e113658c0ac9b34f0ce5ed8"}, + {file = "pytype-2023.4.11-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:485f1dbbc579cf14028c666022fd00dfd0029ff153bec20c4cb72b2bf344335e"}, + {file = "pytype-2023.4.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22389ac370a594645757b69213a464a48defa6bd5d8469420c2960bae023f605"}, + {file = "pytype-2023.4.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cfe61030a7257a168bcde462f30c04ea5e858b17007a6d0497c6a4f8e75afe"}, + {file = "pytype-2023.4.11.tar.gz", hash = "sha256:90c9f5b83709b9f87253dae519eb1c0bfda72ca01461fc7023ab0f27d7e1296e"}, +] + +[package.dependencies] +attrs = ">=21.4.0" +importlab = ">=0.8" +jinja2 = ">=3.1.2" +libcst = ">=0.4.9" +networkx = "<2.8.4" +ninja = ">=1.10.0.post2" +pydot = ">=1.4.2" +tabulate = ">=0.8.10" +toml = ">=0.10.2" +typing-extensions = ">=4.3.0" + +[[package]] +name = "pytz" +version = "2023.3" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "ruff" +version = "0.0.261" +description = "An extremely fast Python linter, written in Rust." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.0.261-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:6624a966c4a21110cee6780333e2216522a831364896f3d98f13120936eff40a"}, + {file = "ruff-0.0.261-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:2dba68a9e558ab33e6dd5d280af798a2d9d3c80c913ad9c8b8e97d7b287f1cc9"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd0cee5a81b0785dc0feeb2640c1e31abe93f0d77c5233507ac59731a626f1"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:581e64fa1518df495ca890a605ee65065101a86db56b6858f848bade69fc6489"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc970f6ece0b4950e419f0252895ee42e9e8e5689c6494d18f5dc2c6ebb7f798"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8fa98e747e0fe185d65a40b0ea13f55c492f3b5f9a032a1097e82edaddb9e52e"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f268d52a71bf410aa45c232870c17049df322a7d20e871cfe622c9fc784aab7b"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1293acc64eba16a11109678dc4743df08c207ed2edbeaf38b3e10eb2597321b"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d95596e2f4cafead19a6d1ec0b86f8fda45ba66fe934de3956d71146a87959b3"}, + {file = "ruff-0.0.261-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4bcec45abdf65c1328a269cf6cc193f7ff85b777fa2865c64cf2c96b80148a2c"}, + {file = "ruff-0.0.261-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6c5f397ec0af42a434ad4b6f86565027406c5d0d0ebeea0d5b3f90c4bf55bc82"}, + {file = "ruff-0.0.261-py3-none-musllinux_1_2_i686.whl", hash = "sha256:39abd02342cec0c131b2ddcaace08b2eae9700cab3ca7dba64ae5fd4f4881bd0"}, + {file = "ruff-0.0.261-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:aaa4f52a6e513f8daa450dac4859e80390d947052f592f0d8e796baab24df2fc"}, + {file = "ruff-0.0.261-py3-none-win32.whl", hash = "sha256:daff64b4e86e42ce69e6367d63aab9562fc213cd4db0e146859df8abc283dba0"}, + {file = "ruff-0.0.261-py3-none-win_amd64.whl", hash = "sha256:0fbc689c23609edda36169c8708bb91bab111d8f44cb4a88330541757770ab30"}, + {file = "ruff-0.0.261-py3-none-win_arm64.whl", hash = "sha256:d2eddc60ae75fc87f8bb8fd6e8d5339cf884cd6de81e82a50287424309c187ba"}, + {file = "ruff-0.0.261.tar.gz", hash = "sha256:c1c715b0d1e18f9c509d7c411ca61da3543a4aa459325b1b1e52b8301d65c6d2"}, +] + +[[package]] +name = "scipy" +version = "1.10.1" +description = "Fundamental algorithms for scientific computing in Python" +category = "main" +optional = false +python-versions = "<3.12,>=3.8" +files = [ + {file = "scipy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7354fd7527a4b0377ce55f286805b34e8c54b91be865bac273f527e1b839019"}, + {file = "scipy-1.10.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:4b3f429188c66603a1a5c549fb414e4d3bdc2a24792e061ffbd607d3d75fd84e"}, + {file = "scipy-1.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1553b5dcddd64ba9a0d95355e63fe6c3fc303a8fd77c7bc91e77d61363f7433f"}, + {file = "scipy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c0ff64b06b10e35215abce517252b375e580a6125fd5fdf6421b98efbefb2d2"}, + {file = "scipy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:fae8a7b898c42dffe3f7361c40d5952b6bf32d10c4569098d276b4c547905ee1"}, + {file = "scipy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f1564ea217e82c1bbe75ddf7285ba0709ecd503f048cb1236ae9995f64217bd"}, + {file = "scipy-1.10.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d925fa1c81b772882aa55bcc10bf88324dadb66ff85d548c71515f6689c6dac5"}, + {file = "scipy-1.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaea0a6be54462ec027de54fca511540980d1e9eea68b2d5c1dbfe084797be35"}, + {file = "scipy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15a35c4242ec5f292c3dd364a7c71a61be87a3d4ddcc693372813c0b73c9af1d"}, + {file = "scipy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:43b8e0bcb877faf0abfb613d51026cd5cc78918e9530e375727bf0625c82788f"}, + {file = "scipy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5678f88c68ea866ed9ebe3a989091088553ba12c6090244fdae3e467b1139c35"}, + {file = "scipy-1.10.1-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:39becb03541f9e58243f4197584286e339029e8908c46f7221abeea4b749fa88"}, + {file = "scipy-1.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bce5869c8d68cf383ce240e44c1d9ae7c06078a9396df68ce88a1230f93a30c1"}, + {file = "scipy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07c3457ce0b3ad5124f98a86533106b643dd811dd61b548e78cf4c8786652f6f"}, + {file = "scipy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:049a8bbf0ad95277ffba9b3b7d23e5369cc39e66406d60422c8cfef40ccc8415"}, + {file = "scipy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cd9f1027ff30d90618914a64ca9b1a77a431159df0e2a195d8a9e8a04c78abf9"}, + {file = "scipy-1.10.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:79c8e5a6c6ffaf3a2262ef1be1e108a035cf4f05c14df56057b64acc5bebffb6"}, + {file = "scipy-1.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51af417a000d2dbe1ec6c372dfe688e041a7084da4fdd350aeb139bd3fb55353"}, + {file = "scipy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b4735d6c28aad3cdcf52117e0e91d6b39acd4272f3f5cd9907c24ee931ad601"}, + {file = "scipy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ff7f37b1bf4417baca958d254e8e2875d0cc23aaadbe65b3d5b3077b0eb23ea"}, + {file = "scipy-1.10.1.tar.gz", hash = "sha256:2cf9dfb80a7b4589ba4c40ce7588986d6d5cebc5457cad2c2880f6bc2d42f3a5"}, +] + +[package.dependencies] +numpy = ">=1.19.5,<1.27.0" + +[package.extras] +dev = ["click", "doit (>=0.36.0)", "flake8", "mypy", "pycodestyle", "pydevtool", "rich-click", "typing_extensions"] +doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] +test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] + +[[package]] +name = "typing-inspect" +version = "0.8.0" +description = "Runtime inspection utilities for typing module." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "typing_inspect-0.8.0-py3-none-any.whl", hash = "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188"}, + {file = "typing_inspect-0.8.0.tar.gz", hash = "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d"}, +] + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "uncertainties" +version = "3.1.7" +description = "Transparent calculations with uncertainties on the quantities involved (aka error propagation); fast calculation of derivatives" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "uncertainties-3.1.7-py2.py3-none-any.whl", hash = "sha256:4040ec64d298215531922a68fa1506dc6b1cb86cd7cca8eca848fcfe0f987151"}, + {file = "uncertainties-3.1.7.tar.gz", hash = "sha256:80111e0839f239c5b233cb4772017b483a0b7a1573a581b92ab7746a35e6faab"}, +] + +[package.dependencies] +future = "*" + +[package.extras] +all = ["nose", "numpy", "sphinx"] +docs = ["sphinx"] +optional = ["numpy"] +tests = ["nose", "numpy"] + +[[package]] +name = "watchfiles" +version = "0.19.0" +description = "Simple, modern and high performance file watching and code reload in python." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "watchfiles-0.19.0-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:91633e64712df3051ca454ca7d1b976baf842d7a3640b87622b323c55f3345e7"}, + {file = "watchfiles-0.19.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b6577b8c6c8701ba8642ea9335a129836347894b666dd1ec2226830e263909d3"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:18b28f6ad871b82df9542ff958d0c86bb0d8310bb09eb8e87d97318a3b5273af"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac19dc9cbc34052394dbe81e149411a62e71999c0a19e1e09ce537867f95ae0"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ea3397aecbc81c19ed7f025e051a7387feefdb789cf768ff994c1228182fda"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c0376deac92377817e4fb8f347bf559b7d44ff556d9bc6f6208dd3f79f104aaf"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c75eff897786ee262c9f17a48886f4e98e6cfd335e011c591c305e5d083c056"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb5d45c4143c1dd60f98a16187fd123eda7248f84ef22244818c18d531a249d1"}, + {file = "watchfiles-0.19.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:79c533ff593db861ae23436541f481ec896ee3da4e5db8962429b441bbaae16e"}, + {file = "watchfiles-0.19.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3d7d267d27aceeeaa3de0dd161a0d64f0a282264d592e335fff7958cc0cbae7c"}, + {file = "watchfiles-0.19.0-cp37-abi3-win32.whl", hash = "sha256:176a9a7641ec2c97b24455135d58012a5be5c6217fc4d5fef0b2b9f75dbf5154"}, + {file = "watchfiles-0.19.0-cp37-abi3-win_amd64.whl", hash = "sha256:945be0baa3e2440151eb3718fd8846751e8b51d8de7b884c90b17d271d34cae8"}, + {file = "watchfiles-0.19.0-cp37-abi3-win_arm64.whl", hash = "sha256:0089c6dc24d436b373c3c57657bf4f9a453b13767150d17284fc6162b2791911"}, + {file = "watchfiles-0.19.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cae3dde0b4b2078f31527acff6f486e23abed307ba4d3932466ba7cdd5ecec79"}, + {file = "watchfiles-0.19.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f3920b1285a7d3ce898e303d84791b7bf40d57b7695ad549dc04e6a44c9f120"}, + {file = "watchfiles-0.19.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9afd0d69429172c796164fd7fe8e821ade9be983f51c659a38da3faaaaac44dc"}, + {file = "watchfiles-0.19.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68dce92b29575dda0f8d30c11742a8e2b9b8ec768ae414b54f7453f27bdf9545"}, + {file = "watchfiles-0.19.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:5569fc7f967429d4bc87e355cdfdcee6aabe4b620801e2cf5805ea245c06097c"}, + {file = "watchfiles-0.19.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5471582658ea56fca122c0f0d0116a36807c63fefd6fdc92c71ca9a4491b6b48"}, + {file = "watchfiles-0.19.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b538014a87f94d92f98f34d3e6d2635478e6be6423a9ea53e4dd96210065e193"}, + {file = "watchfiles-0.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20b44221764955b1e703f012c74015306fb7e79a00c15370785f309b1ed9aa8d"}, + {file = "watchfiles-0.19.0.tar.gz", hash = "sha256:d9b073073e048081e502b6c6b0b88714c026a1a4c890569238d04aca5f9ca74b"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[metadata] +lock-version = "2.0" +python-versions = ">=3.10,<3.11" +content-hash = "24472122b86803712adcc56dc8e2cee99ddf221affbddb501f9a7c16c025a1d8" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..917191a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,43 @@ +[tool.poetry] +name = "kipubot" +version = "0.2.0" +description = "Graphs for Friday bottle raffles." +authors = [ + "Mikael Siidorow ", + "Kalle Ahlstrom <71292737+kqlski@users.noreply.github.com>", +] +license = "GPL-3.0" +readme = "README.md" + + +[tool.poe.tasks] +start = "poetry run python -m kipubot" +dev = "watchfiles 'poetry run poe start' kipubot" +test = "poetry run pytest" +test_hot = "watchfiles 'pytest' kipubot tests" +lint = "poetry run ruff ." +type = "poetry run pytype ." + +[tool.poetry.dependencies] +python = ">=3.10,<3.11" +pandas = "^2.0.0" +matplotlib = "^3.7.1" +scipy = "^1.10.1" +python-dotenv = "^1.0.0" +python-telegram-bot = "^20.2" +psycopg = "^3.1.8" +openpyxl = "^3.1.2" +pytz = "^2023.3" +uncertainties = "^3.1.7" + +[tool.poetry.group.dev.dependencies] +watchfiles = "^0.19.0" +ruff = "^0.0.261" +pytype = "^2023.4.11" +pytest = "^7.3.1" +black = "^23.3.0" +poethepoet = "^0.19.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/test.Dockerfile b/test.Dockerfile index ee58b6e..2bb0df4 100644 --- a/test.Dockerfile +++ b/test.Dockerfile @@ -1,20 +1,23 @@ # syntax=docker/dockerfile:1 -FROM python:3.8 as base +FROM python:3.10 as base # Setup ENV variables here (if needed in the future) FROM base as python-deps -# Install pipenv -RUN pip3 install pipenv +# Install pipx and poetry +RUN pip3 install --user pipx +ENV PATH=/root/.local/bin:$PATH +RUN pipx install poetry==1.4.2 +ENV PATH=/root/.local/pipx/venvs/poetry/bin:$PATH # Install python dependencies in /.venv WORKDIR /bot -COPY Pipfile . -COPY Pipfile.lock . -RUN pipenv install --dev +COPY pyproject.toml . +COPY poetry.lock . +RUN poetry install COPY . . # Run the app -CMD [ "pipenv", "run", "test_hot" ] +CMD [ "poetry", "run", "poe", "test_hot" ] diff --git a/tests/test_utils.py b/tests/test_utils.py index 302036f..c73b4cb 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,22 +2,61 @@ import pytest from datetime import datetime -from db import delete_chat, delete_raffle_data, save_chat_or_ignore, _init_db +from kipubot.db import delete_chat, delete_raffle_data, save_chat_or_ignore, _init_db from kipubot import DATABASE_URL from pandas.testing import assert_frame_equal -from kipubot.utils import get_raffle, int_price_to_str, remove_emojis, read_excel_to_df, save_raffle +from kipubot.utils import ( + get_raffle, + int_price_to_str, + remove_emojis, + read_excel_to_df, + save_raffle, +) class TestUtils: - def test_int_price_to_str(self): - test_cases = [-10000, -1001, -101, -100, -99, -50, -10, -1, - 0, - 1, 10, 50, 100, 101, 150, 1001, 10001, 100001] + test_cases = [ + -10000, + -1001, + -101, + -100, + -99, + -50, + -10, + -1, + 0, + 1, + 10, + 50, + 100, + 101, + 150, + 1001, + 10001, + 100001, + ] - expected_results = ['-100', '-10.01', '-1.01', '-1', '-0.99', '-0.5', '-0.1', '-0.01', - '0', - '0.01', '0.1', '0.5', '1', '1.01', '1.5', '10.01', '100.01', '1000.01'] + expected_results = [ + "-100", + "-10.01", + "-1.01", + "-1", + "-0.99", + "-0.5", + "-0.1", + "-0.01", + "0", + "0.01", + "0.1", + "0.5", + "1", + "1.01", + "1.5", + "10.01", + "100.01", + "1000.01", + ] results = [int_price_to_str(price) for price in test_cases] @@ -27,18 +66,26 @@ def test_int_price_to_str(self): # assert int_price_to_str(case) == ex_res def test_remove_emojis(self): - assert remove_emojis('💩') == ' ' - assert remove_emojis( - 'text💩with💩emoji💩in💩the💩middle') == 'text with emoji in the middle' - assert remove_emojis( - '💩text with emoji at the start') == ' text with emoji at the start' - assert remove_emojis( - 'text with emoji at the end💩') == 'text with emoji at the end ' - assert remove_emojis( - '💩text with emoji💩at the start, middle and end💩') == ' text with emoji at the start, middle and end ' + assert remove_emojis("💩") == " " + assert ( + remove_emojis("text💩with💩emoji💩in💩the💩middle") + == "text with emoji in the middle" + ) + assert ( + remove_emojis("💩text with emoji at the start") + == " text with emoji at the start" + ) + assert ( + remove_emojis("text with emoji at the end💩") + == "text with emoji at the end " + ) + assert ( + remove_emojis("💩text with emoji💩at the start, middle and end💩") + == " text with emoji at the start, middle and end " + ) + class TestGraphSave: - @pytest.fixture(autouse=True) def create_chat(self): _init_db(DATABASE_URL) @@ -46,7 +93,6 @@ def create_chat(self): yield 1 delete_chat(1) - def test_graph_save(self): file_path = "tests/example_data/example_1.xlsx" start_date = datetime.fromisoformat("2022-08-01 03:15:00") @@ -56,10 +102,11 @@ def test_graph_save(self): save_raffle(1, start_date, end_date, entry_fee, df) raffle_from_db = get_raffle(1, True) delete_raffle_data(1) - assert (start_date == raffle_from_db.start_date) - assert (end_date == raffle_from_db.end_date) - assert (entry_fee == raffle_from_db.entry_fee) + assert start_date == raffle_from_db.start_date + assert end_date == raffle_from_db.end_date + assert entry_fee == raffle_from_db.entry_fee - # behavior that get_raffle returns without index and read returns with probably should be changed. - df.set_index('date',inplace=True) - assert_frame_equal(df,raffle_from_db.df) + # behavior that get_raffle returns without index and + # read returns with probably should be changed. + df.set_index("date", inplace=True) + assert_frame_equal(df, raffle_from_db.df) From 3d92e39c8bb87c895466756043a7fb0f6f590b1e Mon Sep 17 00:00:00 2001 From: Mikael Siidorow Date: Mon, 17 Apr 2023 11:09:36 +0300 Subject: [PATCH 2/8] style: format with new ruff rules --- kipubot/__init__.py | 4 +- kipubot/bot.py | 16 +-- kipubot/constants.py | 16 ++- kipubot/db.py | 37 +++---- kipubot/handlers/__init__.py | 12 +- kipubot/handlers/_bot_added_handler.py | 9 +- kipubot/handlers/_error_handler.py | 10 +- kipubot/handlers/_excel_file_handler.py | 18 ++- kipubot/handlers/_graph_handlers.py | 17 +-- kipubot/handlers/_moro_handler.py | 6 +- kipubot/handlers/_no_dm_handler.py | 6 +- kipubot/handlers/_raffle_setup_handler.py | 48 ++++---- kipubot/handlers/_start_handler.py | 3 +- kipubot/handlers/_winner_handler.py | 27 +++-- kipubot/utils.py | 43 ++++---- pyproject.toml | 127 +++++++++++++++++++++- tests/test_utils.py | 12 +- 17 files changed, 272 insertions(+), 139 deletions(-) diff --git a/kipubot/__init__.py b/kipubot/__init__.py index 1b8cdb7..12d9cab 100644 --- a/kipubot/__init__.py +++ b/kipubot/__init__.py @@ -1,7 +1,9 @@ +import logging import os import sys -import logging + from dotenv import load_dotenv + import kipubot.db load_dotenv() diff --git a/kipubot/bot.py b/kipubot/bot.py index 541cc7f..43ac146 100644 --- a/kipubot/bot.py +++ b/kipubot/bot.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- from telegram.ext import ApplicationBuilder, PicklePersistence + from kipubot import BOT_TOKEN from kipubot.handlers import ( - start_handler, - moro_handler, - excel_file_handler, bot_added_handler, - winner_handler, - graph_handler, + error_handler, + excel_file_handler, expected_value_handler, - raffle_setup_handler, + graph_handler, + moro_handler, no_dm_handler, - error_handler, + raffle_setup_handler, + start_handler, + winner_handler, ) diff --git a/kipubot/constants.py b/kipubot/constants.py index 2025616..12ae209 100644 --- a/kipubot/constants.py +++ b/kipubot/constants.py @@ -10,7 +10,7 @@ "no_raffle": "No raffle data found in %(chat_title)s!", "raffle_db_error": ( "Error getting raffle data from database!\n\n" - + "Perhaps one is not setup yet for this chat? 🤔" + "Perhaps one is not setup yet for this chat? 🤔" ), "no_entries": "No raffle entries yet in %(chat_title)s!", "no_data": "No data found for %(chat_title)s!", @@ -21,20 +21,18 @@ "unknown_error": "Unknown error, please try again later! ❌", "server_error": ( "Server error, please try again later! ❌\n\n" - + "The administration has been contacted." + "The administration has been contacted." ), "invalid_file": "Invalid Excel file! ❌\n\n/start for instructions", "new_raffle_button": "🆕 Create a new raffle!", "update_raffle_button": "🔄 Update existing raffle!", "raffle_setup_base": ( - "📝 Raffle setup for %(chat_title)s\n" + "=============================\n\n" + "📝 Raffle setup for %(chat_title)s\n=============================\n\n" ), "raffle_setup_update_or_new": ( - "Found existing raffle.\n" + "Do you want update it or create a new one?" - ), - "raffle_setup_new": ( - "No existing raffle found.\n" + "Do you want to create a new one?" + "Found existing raffle.\nDo you want update it or create a new one?" ), + "raffle_setup_new": "No existing raffle found.\nDo you want to create a new one?", "updated_raffle": "Updated raffle data in %(chat_title)s! 🔄", "raffle_setup_start_date": "Start date set to %(start_date)s!\n", "raffle_setup_end_date": "End date set to %(end_date)s!\n\n", @@ -49,11 +47,11 @@ "timed_out": "Timed out! 🕐", "start_prompt": ( "Use the given commands or send me an Excel-file " - + "from MobilePay if you're the host of a raffle!" + "from MobilePay if you're the host of a raffle!" ), "invalid_winner_usage": "Please use the format /winner @username", "forbidden_command": "You are not allowed to use this command! ❌", - "user_not_found": ("Error getting user!\n" + "Perhaps they haven't /moro ed? 🤔"), + "user_not_found": ("Error getting user!\nPerhaps they haven't /moro ed? 🤔"), "already_winner": "You are already the winner!", "winner_confirmation": "%(username)s is the new winner!", } diff --git a/kipubot/db.py b/kipubot/db.py index 9c83daa..2bc923f 100644 --- a/kipubot/db.py +++ b/kipubot/db.py @@ -1,19 +1,21 @@ import logging -from typing import Tuple, List, Optional -from pandas import Timestamp, DataFrame +from contextlib import suppress + import psycopg -import psycopg.errors as PSErrors +import psycopg.errors as pserrors +from pandas import DataFrame, Timestamp + from kipubot.errors import AlreadyRegisteredError # STORE DB CONNECTION -_CON: Optional[psycopg.Connection] = None +_CON: psycopg.Connection | None = None # LOGGER _logger = logging.getLogger(__name__) def _init_db(url: str) -> None: - global _CON # pylint: disable=global-statement + global _CON # noqa: PLW0603 if not _CON: _logger.info("Connecting to DB...") @@ -57,16 +59,15 @@ def _init_db(url: str) -> None: amounts INTEGER[] )""" ) - except PSErrors.Error as e: - _logger.error("Unknown error during database initialization:") - _logger.error(e) + except pserrors.Error: + _logger.exception("Unknown error during database initialization!") _CON.rollback() else: _logger.info("Database succesfully initialized!") _CON.commit() -def get_registered_member_ids(chat_id: int) -> List[int]: +def get_registered_member_ids(chat_id: int) -> list[int]: return [ row[0] for row in _CON.execute( @@ -78,13 +79,13 @@ def get_registered_member_ids(chat_id: int) -> List[int]: ] -def get_admin_ids(chat_id: int) -> List[int]: +def get_admin_ids(chat_id: int) -> list[int]: return _CON.execute( "SELECT admins FROM chat WHERE chat_id = %s", (chat_id,) ).fetchone()[0] -def get_prev_winner_ids(chat_id: int) -> List[int]: +def get_prev_winner_ids(chat_id: int) -> list[int]: return _CON.execute( "SELECT prev_winners FROM chat WHERE chat_id = %s", (chat_id,) ).fetchone()[0] @@ -96,7 +97,7 @@ def get_winner_id(chat_id: int) -> int: ).fetchone()[0] -def get_chats_where_winner(user_id: int) -> List[Tuple[int, str]]: +def get_chats_where_winner(user_id: int) -> list[tuple[int, str]]: return _CON.execute( """SELECT c.chat_id, c.title FROM chat AS c, in_chat as i @@ -109,7 +110,7 @@ def get_chats_where_winner(user_id: int) -> List[Tuple[int, str]]: def get_raffle_data( chat_id: int, -) -> Tuple[int, Timestamp, Timestamp, int, List[Timestamp], List[str], List[int]]: +) -> tuple[int, Timestamp, Timestamp, int, list[Timestamp], list[str], list[int]]: return _CON.execute("SELECT * FROM raffle WHERE chat_id = %s", [chat_id]).fetchone() @@ -128,7 +129,7 @@ def save_raffle_data( """INSERT INTO raffle VALUES (%s, %s, %s, %s, %s, %s, %s) ON CONFLICT (chat_id) - DO UPDATE SET + DO UPDATE SET start_date = EXCLUDED.start_date, end_date = EXCLUDED.end_date, entry_fee = EXCLUDED.entry_fee, @@ -158,7 +159,7 @@ def save_user_or_ignore(user_id: int) -> None: _CON.commit() -def save_chat_or_ignore(chat_id: int, title: str, admin_ids: List[int]) -> None: +def save_chat_or_ignore(chat_id: int, title: str, admin_ids: list[int]) -> None: _CON.execute( """INSERT INTO chat (chat_id, title, admins) VALUES (%s, %s, %s) @@ -183,7 +184,7 @@ def register_user(chat_id: int, user_id: int) -> None: VALUES (%s, %s)""", (user_id, chat_id), ) - except PSErrors.UniqueViolation as e: + except pserrors.UniqueViolation as e: _CON.rollback() raise AlreadyRegisteredError from e else: @@ -191,10 +192,8 @@ def register_user(chat_id: int, user_id: int) -> None: def register_user_or_ignore(chat_id: int, user_id: int) -> None: - try: + with suppress(AlreadyRegisteredError): register_user(chat_id, user_id) - except AlreadyRegisteredError: - pass def admin_cycle_winners(winner_id: int, chat_id: int) -> None: diff --git a/kipubot/handlers/__init__.py b/kipubot/handlers/__init__.py index a6aeb26..c509acc 100644 --- a/kipubot/handlers/__init__.py +++ b/kipubot/handlers/__init__.py @@ -11,12 +11,12 @@ "error_handler", ) +from ._bot_added_handler import bot_added_handler +from ._error_handler import error_handler +from ._excel_file_handler import excel_file_handler +from ._graph_handlers import expected_value_handler, graph_handler +from ._moro_handler import moro_handler +from ._no_dm_handler import no_dm_handler from ._raffle_setup_handler import raffle_setup_handler from ._start_handler import start_handler -from ._moro_handler import moro_handler -from ._excel_file_handler import excel_file_handler -from ._bot_added_handler import bot_added_handler from ._winner_handler import winner_handler -from ._graph_handlers import graph_handler, expected_value_handler -from ._no_dm_handler import no_dm_handler -from ._error_handler import error_handler diff --git a/kipubot/handlers/_bot_added_handler.py b/kipubot/handlers/_bot_added_handler.py index 0789acb..69216d1 100644 --- a/kipubot/handlers/_bot_added_handler.py +++ b/kipubot/handlers/_bot_added_handler.py @@ -1,9 +1,10 @@ +import psycopg.errors as pserrors from telegram import Update -from telegram.ext import ContextTypes, ChatMemberHandler from telegram.constants import ChatMemberStatus -import psycopg.errors as PSErrors +from telegram.ext import ChatMemberHandler, ContextTypes + from kipubot.constants import STRINGS -from kipubot.db import save_chat_or_ignore, save_user_or_ignore, register_user_or_ignore +from kipubot.db import register_user_or_ignore, save_chat_or_ignore, save_user_or_ignore async def bot_added(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: @@ -24,7 +25,7 @@ async def bot_added(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: save_user_or_ignore(user_id) register_user_or_ignore(chat_id, user_id) - except PSErrors.IntegrityError as e: + except pserrors.IntegrityError as e: print("SQLite Error: " + str(e)) await context.bot.send_message( chat_id=chat_id, text=STRINGS["unknown_error"] diff --git a/kipubot/handlers/_error_handler.py b/kipubot/handlers/_error_handler.py index 3022dc0..694c1c2 100644 --- a/kipubot/handlers/_error_handler.py +++ b/kipubot/handlers/_error_handler.py @@ -1,10 +1,12 @@ -import logging import html -import traceback import json +import logging +import traceback + from telegram import Update -from telegram.ext import ContextTypes from telegram.constants import ParseMode +from telegram.ext import ContextTypes + from kipubot import DEVELOPER_CHAT_ID from kipubot.constants import STRINGS @@ -15,7 +17,7 @@ async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N """Log the error and send a telegram message to notify the developer.""" # Log the error before we do anything else, # so we can see it even if something breaks. - _logger.error(msg="Exception while handling an update:", exc_info=context.error) + _logger.error("Exception while handling an update:", exc_info=context.error) # traceback.format_exception returns the usual python message about an exception, # but as a list of strings rather than a single string, diff --git a/kipubot/handlers/_excel_file_handler.py b/kipubot/handlers/_excel_file_handler.py index 11205cf..4ebfc56 100644 --- a/kipubot/handlers/_excel_file_handler.py +++ b/kipubot/handlers/_excel_file_handler.py @@ -1,16 +1,14 @@ import os -from typing import Optional -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup + +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ContextTypes, ConversationHandler, MessageHandler -import telegram.ext.filters as Filters -from kipubot.constants import EXCEL_MIME, STRINGS -from kipubot.utils import validate_excel + +from kipubot.constants import STRINGS from kipubot.db import get_chats_where_winner +from kipubot.utils import validate_excel -async def excel_file( - update: Update, context: ContextTypes.DEFAULT_TYPE -) -> Optional[str]: +async def excel_file(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str | None: user_id = update.effective_user.id dm_id = update.effective_chat.id @@ -58,6 +56,4 @@ async def excel_file( ) -excel_file_handler = MessageHandler( - Filters.Document.MimeType(EXCEL_MIME) & Filters.ChatType.PRIVATE, excel_file -) +excel_file_handler = MessageHandler() diff --git a/kipubot/handlers/_graph_handlers.py b/kipubot/handlers/_graph_handlers.py index bac0ce8..01c0f8b 100644 --- a/kipubot/handlers/_graph_handlers.py +++ b/kipubot/handlers/_graph_handlers.py @@ -1,11 +1,12 @@ from enum import Enum + +import psycopg.errors as pserrors from telegram import Update -from telegram.ext import ContextTypes, CommandHandler -import telegram.ext.filters as Filters -import psycopg.errors as PSErrors -from kipubot.errors import NoEntriesError, NoRaffleError -from kipubot.utils import generate_graph, generate_expected +from telegram.ext import CommandHandler, ContextTypes, filters + from kipubot.constants import STRINGS +from kipubot.errors import NoEntriesError, NoRaffleError +from kipubot.utils import generate_expected, generate_graph class GraphType(Enum): @@ -46,17 +47,17 @@ async def graph( await update.message.reply_text( STRINGS["no_entries"] % {"chat_title": chat_title} ) - except PSErrors.Error as e: + except pserrors.Error as e: print(e) await update.message.reply_text(STRINGS["raffle_db_error"]) except FileNotFoundError: await update.message.reply_text(STRINGS["no_data"] % {"chat_title": chat_title}) -graph_handler = CommandHandler(["kuvaaja", "graph"], graph, ~Filters.ChatType.PRIVATE) +graph_handler = CommandHandler(["kuvaaja", "graph"], graph, ~filters.ChatType.PRIVATE) expected_value_handler = CommandHandler( ["odotusarvo", "expected"], lambda u, c: graph(u, c, graph_type=GraphType.EXPECTED), - ~Filters.ChatType.PRIVATE, + ~filters.ChatType.PRIVATE, ) diff --git a/kipubot/handlers/_moro_handler.py b/kipubot/handlers/_moro_handler.py index f81b648..2eb87ea 100644 --- a/kipubot/handlers/_moro_handler.py +++ b/kipubot/handlers/_moro_handler.py @@ -1,6 +1,6 @@ from telegram import Update -from telegram.ext import ContextTypes, CommandHandler -import telegram.ext.filters as Filters +from telegram.ext import CommandHandler, ContextTypes, filters + from kipubot.constants import STRINGS from kipubot.db import register_user from kipubot.errors import AlreadyRegisteredError @@ -25,4 +25,4 @@ async def hello(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: ) -moro_handler = CommandHandler(["moro", "hello"], hello, ~Filters.ChatType.PRIVATE) +moro_handler = CommandHandler(["moro", "hello"], hello, ~filters.ChatType.PRIVATE) diff --git a/kipubot/handlers/_no_dm_handler.py b/kipubot/handlers/_no_dm_handler.py index 6696920..cf02ed1 100644 --- a/kipubot/handlers/_no_dm_handler.py +++ b/kipubot/handlers/_no_dm_handler.py @@ -1,6 +1,6 @@ from telegram import Update -from telegram.ext import ContextTypes, CommandHandler -import telegram.ext.filters as Filters +from telegram.ext import CommandHandler, ContextTypes, filters + from kipubot.constants import STRINGS @@ -11,5 +11,5 @@ async def chat_only(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None no_dm_handler = CommandHandler( ["moro", "hello", "kuvaaja", "graph", "voittaja", "winner"], chat_only, - Filters.ChatType.PRIVATE, + filters.ChatType.PRIVATE, ) diff --git a/kipubot/handlers/_raffle_setup_handler.py b/kipubot/handlers/_raffle_setup_handler.py index 2274040..7a9f475 100644 --- a/kipubot/handlers/_raffle_setup_handler.py +++ b/kipubot/handlers/_raffle_setup_handler.py @@ -1,19 +1,21 @@ import os -from typing import Optional, Union +from typing import Literal + import pandas as pd -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.ext import ConversationHandler, CallbackQueryHandler, CallbackContext +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import CallbackContext, CallbackQueryHandler, ConversationHandler + from kipubot.constants import STRINGS +from kipubot.errors import NoRaffleError from kipubot.utils import ( - get_raffle, - save_raffle, - read_excel_to_df, - is_int, - is_float, get_cur_time_hel, + get_raffle, int_price_to_str, + is_float, + is_int, + read_excel_to_df, + save_raffle, ) -from kipubot.errors import NoRaffleError # ================== # = UTIL FUNCTIONS = @@ -46,9 +48,12 @@ async def convo_timeout(update: Update, context: CallbackContext) -> int: # KEYBOARD COMPONENTS # -------------------- +RAFFLE_CHAT_SELECTED_DATA_LENGTH = 4 +RAFFLE_DATE_UPDATE_DATA_LENGTH = 5 +RAFFLE_FEE_UPDATE_DATA_LENGTH = 4 -def raffle_keyboard(has_existing: bool = False) -> InlineKeyboardMarkup: +def raffle_keyboard(*, has_existing: bool = False) -> InlineKeyboardMarkup: if has_existing: keyboard = [ [ @@ -80,10 +85,7 @@ def raffle_keyboard(has_existing: bool = False) -> InlineKeyboardMarkup: return InlineKeyboardMarkup(keyboard) -def date_keyboard(which: str) -> InlineKeyboardMarkup: - if which not in ["start", "end"]: - raise Exception("Invalid date type, should be start or end!") - +def date_keyboard(which: Literal["start", "end"]) -> InlineKeyboardMarkup: rough_controls = [ InlineKeyboardButton("-1 d", callback_data=f"raffle:date:{which}:update:-24"), InlineKeyboardButton("-12 h", callback_data=f"raffle:date:{which}:update:-12"), @@ -145,7 +147,7 @@ def fee_keyboard() -> InlineKeyboardMarkup: # ================= -async def setup_raffle(update: Update, context: CallbackContext) -> Union[str, int]: +async def setup_raffle(update: Update, context: CallbackContext) -> str | int: query = update.callback_query if query.data == "raffle:cancel": @@ -153,7 +155,7 @@ async def setup_raffle(update: Update, context: CallbackContext) -> Union[str, i if ( query.data.startswith("raffle:chat_selected") - and len(query.data.split(":")) == 4 + and len(query.data.split(":")) == RAFFLE_CHAT_SELECTED_DATA_LENGTH and is_int(query.data.split(":")[2]) ): args = query.data.split(":") @@ -189,7 +191,7 @@ async def setup_raffle(update: Update, context: CallbackContext) -> Union[str, i return await convo_error(update, context) -async def setup_start_date(update: Update, context: CallbackContext) -> Optional[str]: +async def setup_start_date(update: Update, context: CallbackContext) -> str | None: query = update.callback_query if query.data == "raffle:setup:new": @@ -197,7 +199,7 @@ async def setup_start_date(update: Update, context: CallbackContext) -> Optional if ( query.data.startswith("raffle:date:start:update") - and len(query.data.split(":")) == 5 + and len(query.data.split(":")) == RAFFLE_DATE_UPDATE_DATA_LENGTH and is_float(query.data.split(":")[4]) ): diff = float(query.data.split(":")[4]) @@ -224,7 +226,7 @@ async def setup_start_date(update: Update, context: CallbackContext) -> Optional return None -async def setup_end_date(update: Update, context: CallbackContext) -> Optional[str]: +async def setup_end_date(update: Update, context: CallbackContext) -> str | None: query = update.callback_query if query.data == "raffle:date:start:confirmed": @@ -232,7 +234,7 @@ async def setup_end_date(update: Update, context: CallbackContext) -> Optional[s if ( query.data.startswith("raffle:date:end:update") - and len(query.data.split(":")) == 5 + and len(query.data.split(":")) == RAFFLE_DATE_UPDATE_DATA_LENGTH and is_float(query.data.split(":")[4]) ): diff = float(query.data.split(":")[4]) @@ -273,7 +275,7 @@ async def setup_end_date(update: Update, context: CallbackContext) -> Optional[s return None -async def setup_fee(update: Update, context: CallbackContext) -> Optional[str]: +async def setup_fee(update: Update, context: CallbackContext) -> str | None: query = update.callback_query if query.data == "raffle:date:end:confirmed": @@ -281,7 +283,7 @@ async def setup_fee(update: Update, context: CallbackContext) -> Optional[str]: if ( query.data.startswith("raffle:fee:update") - and len(query.data.split(":")) == 4 + and len(query.data.split(":")) == RAFFLE_FEE_UPDATE_DATA_LENGTH and is_int(query.data.split(":")[3]) ): diff = int(query.data.split(":")[3]) @@ -324,7 +326,7 @@ async def setup_fee(update: Update, context: CallbackContext) -> Optional[str]: return None -async def finish_setup(update: Update, context: CallbackContext) -> Optional[int]: +async def finish_setup(update: Update, context: CallbackContext) -> int | None: query = update.callback_query dm_id = update.effective_chat.id diff --git a/kipubot/handlers/_start_handler.py b/kipubot/handlers/_start_handler.py index fcb1c80..d240b1b 100644 --- a/kipubot/handlers/_start_handler.py +++ b/kipubot/handlers/_start_handler.py @@ -1,5 +1,6 @@ from telegram import Update -from telegram.ext import ContextTypes, CommandHandler +from telegram.ext import CommandHandler, ContextTypes + from kipubot.constants import STRINGS diff --git a/kipubot/handlers/_winner_handler.py b/kipubot/handlers/_winner_handler.py index a292945..c300bb7 100644 --- a/kipubot/handlers/_winner_handler.py +++ b/kipubot/handlers/_winner_handler.py @@ -1,20 +1,27 @@ +import logging + +import psycopg.errors as pserrors from telegram import Update -from telegram.ext import ContextTypes, CommandHandler from telegram.constants import MessageEntityType -import telegram.ext.filters as Filters -import psycopg.errors as PSErrors +from telegram.ext import CommandHandler, ContextTypes, filters + from kipubot.constants import STRINGS from kipubot.db import ( admin_cycle_winners, cycle_winners, - get_registered_member_ids, get_admin_ids, get_prev_winner_ids, + get_registered_member_ids, get_winner_id, replace_cur_winner, ) from kipubot.utils import get_chat_member_opt +_logger = logging.getLogger(__name__) + + +TWO_ENTITIES = 2 + async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: # only usable by admin, previous winner (in case of typos) and current winner @@ -25,7 +32,7 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: chat_id = update.effective_chat.id user_id = update.effective_user.id - if len(ent) != 2 or ent[1].type != MessageEntityType.MENTION: + if len(ent) != TWO_ENTITIES or ent[1].type != MessageEntityType.MENTION: await update.message.reply_text(STRINGS["invalid_winner_usage"]) return @@ -44,8 +51,8 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: registered_member_ids = get_registered_member_ids(chat_id) registered_members = [ - await get_chat_member_opt(update.effective_chat, id) - for id in registered_member_ids + await get_chat_member_opt(update.effective_chat, member_id) + for member_id in registered_member_ids ] # drop None values registered_members = [m for m in registered_members if m] @@ -72,8 +79,8 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: # winner: moves themselves to prev and makes new current else: cycle_winners(user_id, winner_id, chat_id) - except PSErrors.Error as e: - print(e) + except pserrors.Error: + _logger.exception("SQLite Error:") await update.message.reply_text(STRINGS["user_not_found"]) return @@ -83,5 +90,5 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: winner_handler = CommandHandler( - ["voittaja", "winner"], winner, ~Filters.ChatType.PRIVATE + ["voittaja", "winner"], winner, ~filters.ChatType.PRIVATE ) diff --git a/kipubot/utils.py b/kipubot/utils.py index 7ceb2af..fc4ee92 100644 --- a/kipubot/utils.py +++ b/kipubot/utils.py @@ -1,43 +1,47 @@ import os import re -from typing import NamedTuple, Optional -import pytz -import matplotlib.pyplot as plt +from typing import NamedTuple + import matplotlib.dates as mdates -from matplotlib.ticker import AutoMinorLocator -import pandas as pd +import matplotlib.pyplot as plt import numpy as np -from scipy import stats -from scipy.optimize import curve_fit +import pandas as pd +import pytz import uncertainties as unc import uncertainties.unumpy as unp -from telegram import ChatMember, Chat +from matplotlib.ticker import AutoMinorLocator +from scipy import stats +from scipy.optimize import curve_fit +from telegram import Chat, ChatMember from telegram.error import BadRequest -from kipubot.errors import NoRaffleError + from kipubot import db +from kipubot.errors import NoRaffleError class RaffleData(NamedTuple): start_date: pd.Timestamp end_date: pd.Timestamp entry_fee: int - df: Optional[pd.DataFrame] + df: pd.DataFrame | None def is_int(x: str) -> bool: try: int(x) - return True except ValueError: return False + else: + return True def is_float(x: str) -> bool: try: float(x) - return True except ValueError: return False + else: + return True def int_price_to_str(num: int) -> str: @@ -56,13 +60,13 @@ def int_price_to_str(num: int) -> str: return str_num -async def get_chat_member_opt(chat: Chat, member_id: int) -> Optional[ChatMember]: +async def get_chat_member_opt(chat: Chat, member_id: int) -> ChatMember | None: try: return await chat.get_member(member_id) except BadRequest as e: if e.message == "User not found": return None - raise e + raise def preband(x, xd, yd, p, func): @@ -162,11 +166,12 @@ def read_excel_to_df( return df -def get_raffle(chat_id: int, include_df: bool = False) -> RaffleData: +def get_raffle(chat_id: int, *, include_df: bool = False) -> RaffleData: query_result = db.get_raffle_data(chat_id) if query_result is None: - raise NoRaffleError(f"No raffle found for chat {chat_id}") + error_text = f"No raffle found for chat {chat_id}" + raise NoRaffleError(error_text) _, start_date, end_date, entry_fee, dates, entries, amounts = query_result @@ -216,10 +221,8 @@ def parse_expected(raffle_data: RaffleData) -> RaffleData: df["win_odds"] = 1.0 / df["unique"] df["next_expected"] = ( ( - ( - -entry_fee * (1 - df["win_odds"]) - + (df["amount"] - entry_fee) * df["win_odds"] - ) + -entry_fee * (1 - df["win_odds"]) + + (df["amount"] - entry_fee) * df["win_odds"] ) .fillna(0) .round() diff --git a/pyproject.toml b/pyproject.toml index 917191a..eab0287 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,12 +11,13 @@ readme = "README.md" [tool.poe.tasks] -start = "poetry run python -m kipubot" +start = "python3 -m kipubot" dev = "watchfiles 'poetry run poe start' kipubot" -test = "poetry run pytest" +test = "pytest" test_hot = "watchfiles 'pytest' kipubot tests" -lint = "poetry run ruff ." -type = "poetry run pytype ." +lint = "ruff ." +type = "pytype ." +format = [{ cmd = "black ." }, { cmd = "ruff . --fix" }] [tool.poetry.dependencies] python = ">=3.10,<3.11" @@ -41,3 +42,121 @@ poethepoet = "^0.19.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.ruff] +line-length = 88 +target-version = "py310" + +select = [ + "A", + "B", + "C90", + "DTZ", + "E", + "EM", + "F", + "FBT", + "I", + "ICN", + "ISC", + "N", + "PLC", + "PLE", + "PLR", + "PLW", + "Q", + "RUF", + "S", + "SIM", + "TRY", + "TID", + "UP", + "W", + "YTT", +] +ignore = [] + +fixable = [ + "A", + "B", + "C90", # C + "D", + "E", + "F", + "G", + "I", + "N", + "Q", + "S", + "W", + "ANN", + "ARG", + "BLE", + "COM", + "DJ", + "DTZ", + "EM", + "ERA", + "EXE", + "FBT", + "ICN", + "INP", + "ISC", + "NPY", + "PD", + "PGH", + "PIE", + "PL", + "PT", + "PTH", + "PYI", + "RET", + "RSE", + "RUF", + "SIM", + "SLF", + "TCH", + "TID", + #"T", + "TRY", + "UP", + "YTT", +] +unfixable = [] + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] + +# Same as Black. + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + + +[tool.ruff.mccabe] +# Unlike Flake8, default to a complexity level of 10. +max-complexity = 10 + +[tool.ruff.per-file-ignores] +"tests/*" = ["S101"] diff --git a/tests/test_utils.py b/tests/test_utils.py index c73b4cb..6da6149 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,15 +1,17 @@ #!/usr/bin/env python3 -import pytest from datetime import datetime -from kipubot.db import delete_chat, delete_raffle_data, save_chat_or_ignore, _init_db -from kipubot import DATABASE_URL + +import pytest from pandas.testing import assert_frame_equal + +from kipubot import DATABASE_URL +from kipubot.db import _init_db, delete_chat, delete_raffle_data, save_chat_or_ignore from kipubot.utils import ( get_raffle, int_price_to_str, - remove_emojis, read_excel_to_df, + remove_emojis, save_raffle, ) @@ -100,7 +102,7 @@ def test_graph_save(self): entry_fee = 1 df = read_excel_to_df(file_path, start_date, end_date) save_raffle(1, start_date, end_date, entry_fee, df) - raffle_from_db = get_raffle(1, True) + raffle_from_db = get_raffle(1, include_df=True) delete_raffle_data(1) assert start_date == raffle_from_db.start_date assert end_date == raffle_from_db.end_date From 6efe16497d38624dcbfe7f2e8cd3620d5f478f78 Mon Sep 17 00:00:00 2001 From: Mikael Siidorow Date: Mon, 17 Apr 2023 15:34:24 +0300 Subject: [PATCH 3/8] fix: switch to mypy, fix all type issues --- docker-compose.test.yml | 2 +- kipubot/__init__.py | 33 +- kipubot/bot.py | 10 +- kipubot/db.py | 259 ++++---- kipubot/handlers/__init__.py | 3 +- kipubot/handlers/_bot_added_handler.py | 13 +- kipubot/handlers/_error_handler.py | 27 +- kipubot/handlers/_excel_file_handler.py | 25 +- .../{_graph_handlers.py => _graph_handler.py} | 41 +- kipubot/handlers/_moro_handler.py | 3 + kipubot/handlers/_no_dm_handler.py | 2 + kipubot/handlers/_raffle_setup_handler.py | 88 ++- kipubot/handlers/_start_handler.py | 3 + kipubot/handlers/_winner_handler.py | 15 +- kipubot/utils.py | 64 +- poetry.lock | 578 ++++++++---------- pyproject.toml | 31 +- setup.cfg | 2 - tests/test_utils.py | 12 +- 19 files changed, 645 insertions(+), 566 deletions(-) rename kipubot/handlers/{_graph_handlers.py => _graph_handler.py} (64%) delete mode 100644 setup.cfg diff --git a/docker-compose.test.yml b/docker-compose.test.yml index ecc5e8c..7156a7b 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -15,7 +15,7 @@ services: - ./tests:/bot/tests environment: - MODE=TEST - - PYTHONPATH=${PYTHONPATH}:kipubot - DATABASE_URL=postgresql://username:password@test-database:5432 + - BOT_TOKEN=fake depends_on: - test-database diff --git a/kipubot/__init__.py b/kipubot/__init__.py index 12d9cab..6a9530d 100644 --- a/kipubot/__init__.py +++ b/kipubot/__init__.py @@ -1,12 +1,7 @@ import logging import os -import sys -from dotenv import load_dotenv - -import kipubot.db - -load_dotenv() +from pydantic import BaseSettings # LOGGING CONFIG logging.basicConfig( @@ -14,29 +9,23 @@ ) -# ENV VARIABLES -BOT_TOKEN = os.getenv("BOT_TOKEN", default=None) -DATABASE_URL = os.getenv("DATABASE_URL", default=None) -DEVELOPER_CHAT_ID = os.getenv("DEVELOPER_CHAT_ID", default=None) -MODE = os.getenv("MODE", default=None) -if BOT_TOKEN is None and MODE != "TEST": - logging.error("Bot token is not set!") - sys.exit(1) +# ENV CONFIG +class Settings(BaseSettings): + BOT_TOKEN: str + DATABASE_URL: str + DEVELOPER_CHAT_ID: str | None = None -if DATABASE_URL is None: - logging.error("Database URL is not set!") - sys.exit(1) + class Config: + env_file = ".env" + env_file_encoding = "utf-8" -if DEVELOPER_CHAT_ID is None: - logging.warning("Developer chat ID is not set!") +config = Settings() -# INITIALIZE DB AND CREATE TABLES IF THEY DON'T EXIST -kipubot.db._init_db(DATABASE_URL) # pylint: disable=protected-access # CHECK/CREATE DATA DIRECTORY if not os.path.exists("data"): logging.info("Creating ./data/ directory...") os.mkdir("data") -__all__ = ("BOT_TOKEN", "DATABASE_URL", "DEVELOPER_CHAT_ID") +__all__ = ("config",) diff --git a/kipubot/bot.py b/kipubot/bot.py index 43ac146..710747e 100644 --- a/kipubot/bot.py +++ b/kipubot/bot.py @@ -2,12 +2,12 @@ from telegram.ext import ApplicationBuilder, PicklePersistence -from kipubot import BOT_TOKEN +from kipubot import config +from kipubot.db import _init_db from kipubot.handlers import ( bot_added_handler, error_handler, excel_file_handler, - expected_value_handler, graph_handler, moro_handler, no_dm_handler, @@ -18,8 +18,11 @@ def main() -> None: + # INITIALIZE DB AND CREATE TABLES IF THEY DON'T EXIST + _init_db() + persistence = PicklePersistence(filepath="data/.pkl") - app = ApplicationBuilder().token(BOT_TOKEN).persistence(persistence).build() + app = ApplicationBuilder().token(config.BOT_TOKEN).persistence(persistence).build() app.add_handler(start_handler) @@ -30,7 +33,6 @@ def main() -> None: app.add_handler(moro_handler) app.add_handler(graph_handler) app.add_handler(winner_handler) - app.add_handler(expected_value_handler) # warning about using a command in a private chat app.add_handler(no_dm_handler) diff --git a/kipubot/db.py b/kipubot/db.py index 2bc923f..58a14d2 100644 --- a/kipubot/db.py +++ b/kipubot/db.py @@ -1,30 +1,44 @@ import logging -from contextlib import suppress +from collections.abc import Generator +from contextlib import contextmanager, suppress import psycopg import psycopg.errors as pserrors from pandas import DataFrame, Timestamp +from psycopg.rows import TupleRow +from kipubot import config from kipubot.errors import AlreadyRegisteredError -# STORE DB CONNECTION -_CON: psycopg.Connection | None = None - # LOGGER _logger = logging.getLogger(__name__) -def _init_db(url: str) -> None: - global _CON # noqa: PLW0603 - - if not _CON: - _logger.info("Connecting to DB...") - _CON = psycopg.connect(url) - _logger.info("Connected!") +# DB CONNECTION +@contextmanager +def get_pg_conn() -> Generator[psycopg.Connection[TupleRow], None, None]: + """ + Get a connection to the postgres database. + """ + _logger.info("Connecting to DB...") + conn = psycopg.connect(config.DATABASE_URL) + _logger.info("Connected!") - _logger.info("Initializing database...") try: - _CON.execute( + yield conn + except pserrors.Error: + _logger.exception("Unknown error during database operation!") + conn.rollback() + else: + _logger.info("Committing changes...") + conn.commit() + finally: + conn.close() + + +def _init_db() -> None: + with get_pg_conn() as conn: + conn.execute( """CREATE TABLE IF NOT EXISTS chat ( chat_id BIGINT PRIMARY KEY, title VARCHAR(128), @@ -34,13 +48,13 @@ def _init_db(url: str) -> None: )""" ) - _CON.execute( + conn.execute( """CREATE TABLE IF NOT EXISTS chat_user ( user_id BIGINT PRIMARY KEY )""" ) - _CON.execute( + conn.execute( """CREATE TABLE IF NOT EXISTS in_chat ( user_id BIGINT REFERENCES chat_user(user_id), chat_id BIGINT REFERENCES chat(chat_id), @@ -48,7 +62,7 @@ def _init_db(url: str) -> None: )""" ) - _CON.execute( + conn.execute( """CREATE TABLE IF NOT EXISTS raffle ( chat_id BIGINT PRIMARY KEY REFERENCES chat(chat_id), start_date TIMESTAMP, @@ -59,59 +73,64 @@ def _init_db(url: str) -> None: amounts INTEGER[] )""" ) - except pserrors.Error: - _logger.exception("Unknown error during database initialization!") - _CON.rollback() - else: - _logger.info("Database succesfully initialized!") - _CON.commit() def get_registered_member_ids(chat_id: int) -> list[int]: - return [ - row[0] - for row in _CON.execute( - """SELECT chat_user.user_id - FROM chat_user, in_chat - WHERE chat_id = %s AND chat_user.user_id = in_chat.user_id""", - (chat_id,), - ).fetchall() - ] + with get_pg_conn() as conn: + return [ + row[0] + for row in conn.execute( + """SELECT chat_user.user_id + FROM chat_user, in_chat + WHERE chat_id = %s AND chat_user.user_id = in_chat.user_id""", + (chat_id,), + ).fetchall() + ] def get_admin_ids(chat_id: int) -> list[int]: - return _CON.execute( - "SELECT admins FROM chat WHERE chat_id = %s", (chat_id,) - ).fetchone()[0] + with get_pg_conn() as conn: + data = conn.execute( + "SELECT admins FROM chat WHERE chat_id = %s", (chat_id,) + ).fetchone() + return data[0] if data else [] def get_prev_winner_ids(chat_id: int) -> list[int]: - return _CON.execute( - "SELECT prev_winners FROM chat WHERE chat_id = %s", (chat_id,) - ).fetchone()[0] + with get_pg_conn() as conn: + data = conn.execute( + "SELECT prev_winners FROM chat WHERE chat_id = %s", (chat_id,) + ).fetchone() + return data[0] if data else [] def get_winner_id(chat_id: int) -> int: - return _CON.execute( - "SELECT cur_winner FROM chat WHERE chat_id = %s", (chat_id,) - ).fetchone()[0] + with get_pg_conn() as conn: + data = conn.execute( + "SELECT cur_winner FROM chat WHERE chat_id = %s", (chat_id,) + ).fetchone() + return data[0] if data else None def get_chats_where_winner(user_id: int) -> list[tuple[int, str]]: - return _CON.execute( - """SELECT c.chat_id, c.title - FROM chat AS c, in_chat as i - WHERE i.user_id = %(id)s - AND c.chat_id = i.chat_id - AND (c.cur_winner = %(id)s)""", - {"id": user_id}, - ).fetchall() + with get_pg_conn() as conn: + return conn.execute( # type: ignore + """SELECT c.chat_id, c.title + FROM chat AS c, in_chat as i + WHERE i.user_id = %(id)s + AND c.chat_id = i.chat_id + AND (c.cur_winner = %(id)s)""", + {"id": user_id}, + ).fetchall() def get_raffle_data( chat_id: int, ) -> tuple[int, Timestamp, Timestamp, int, list[Timestamp], list[str], list[int]]: - return _CON.execute("SELECT * FROM raffle WHERE chat_id = %s", [chat_id]).fetchone() + with get_pg_conn() as conn: + return conn.execute( # type: ignore + "SELECT * FROM raffle WHERE chat_id = %s", [chat_id] + ).fetchone() def save_raffle_data( @@ -125,70 +144,73 @@ def save_raffle_data( entries = df["name"].tolist() amounts = df["amount"].tolist() - _CON.execute( - """INSERT INTO raffle - VALUES (%s, %s, %s, %s, %s, %s, %s) - ON CONFLICT (chat_id) - DO UPDATE SET - start_date = EXCLUDED.start_date, - end_date = EXCLUDED.end_date, - entry_fee = EXCLUDED.entry_fee, - dates = EXCLUDED.dates, - entries = EXCLUDED.entries, - amounts = EXCLUDED.amounts""", - (chat_id, start_date, end_date, entry_fee, dates, entries, amounts), - ) + with get_pg_conn() as conn: + conn.execute( + """INSERT INTO raffle + VALUES (%s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (chat_id) + DO UPDATE SET + start_date = EXCLUDED.start_date, + end_date = EXCLUDED.end_date, + entry_fee = EXCLUDED.entry_fee, + dates = EXCLUDED.dates, + entries = EXCLUDED.entries, + amounts = EXCLUDED.amounts""", + (chat_id, start_date, end_date, entry_fee, dates, entries, amounts), + ) - _CON.commit() + conn.commit() def delete_raffle_data(chat_id: int): - _CON.execute("""DELETE FROM raffle where chat_id=%s""", (chat_id,)) - _CON.commit() + with get_pg_conn() as conn: + conn.execute("""DELETE FROM raffle where chat_id=%s""", (chat_id,)) + conn.commit() def save_user_or_ignore(user_id: int) -> None: - _CON.execute( - """INSERT INTO chat_user - VALUES (%s) - ON CONFLICT (user_id) - DO NOTHING""", - (user_id,), - ) - - _CON.commit() + with get_pg_conn() as conn: + conn.execute( + """INSERT INTO chat_user + VALUES (%s) + ON CONFLICT (user_id) + DO NOTHING""", + (user_id,), + ) def save_chat_or_ignore(chat_id: int, title: str, admin_ids: list[int]) -> None: - _CON.execute( - """INSERT INTO chat (chat_id, title, admins) - VALUES (%s, %s, %s) - ON CONFLICT (chat_id) - DO NOTHING""", - (chat_id, title, admin_ids), - ) - _CON.commit() + with get_pg_conn() as conn: + conn.execute( + """INSERT INTO chat (chat_id, title, admins) + VALUES (%s, %s, %s) + ON CONFLICT (chat_id) + DO NOTHING""", + (chat_id, title, admin_ids), + ) def delete_chat(chat_id: int): - _CON.execute("""DELETE FROM chat where chat_id=%s""", (chat_id,)) - _CON.commit() + with get_pg_conn() as conn: + conn.execute("""DELETE FROM chat where chat_id=%s""", (chat_id,)) + conn.commit() def register_user(chat_id: int, user_id: int) -> None: save_user_or_ignore(user_id) - try: - _CON.execute( - """INSERT INTO in_chat(user_id, chat_id) - VALUES (%s, %s)""", - (user_id, chat_id), - ) - except pserrors.UniqueViolation as e: - _CON.rollback() - raise AlreadyRegisteredError from e - else: - _CON.commit() + with get_pg_conn() as conn: + try: + conn.execute( + """INSERT INTO in_chat(user_id, chat_id) + VALUES (%s, %s)""", + (user_id, chat_id), + ) + except pserrors.UniqueViolation as e: + conn.rollback() + raise AlreadyRegisteredError from e + else: + conn.commit() def register_user_or_ignore(chat_id: int, user_id: int) -> None: @@ -197,32 +219,33 @@ def register_user_or_ignore(chat_id: int, user_id: int) -> None: def admin_cycle_winners(winner_id: int, chat_id: int) -> None: - _CON.execute( - """UPDATE chat - SET prev_winners = array_append(prev_winners, cur_winner), - cur_winner=%s - WHERE chat_id=%s""", - (winner_id, chat_id), - ) - _CON.commit() + with get_pg_conn() as conn: + conn.execute( + """UPDATE chat + SET prev_winners = array_append(prev_winners, cur_winner), + cur_winner=%s + WHERE chat_id=%s""", + (winner_id, chat_id), + ) + conn.commit() def replace_cur_winner(winner_id: int, chat_id: int) -> None: - _CON.execute( - """UPDATE chat - SET cur_winner=%s - WHERE chat_id=%s""", - (winner_id, chat_id), - ) - _CON.commit() + with get_pg_conn() as conn: + conn.execute( + """UPDATE chat + SET cur_winner=%s + WHERE chat_id=%s""", + (winner_id, chat_id), + ) def cycle_winners(user_id: int, winner_id: int, chat_id: int) -> None: - _CON.execute( - """UPDATE chat - SET prev_winners=array_append(prev_winners, %s), - cur_winner=%s' - WHERE chat_id=%s""", - (user_id, winner_id, chat_id), - ) - _CON.commit() + with get_pg_conn() as conn: + conn.execute( + """UPDATE chat + SET prev_winners=array_append(prev_winners, %s), + cur_winner=%s' + WHERE chat_id=%s""", + (user_id, winner_id, chat_id), + ) diff --git a/kipubot/handlers/__init__.py b/kipubot/handlers/__init__.py index c509acc..435b0dc 100644 --- a/kipubot/handlers/__init__.py +++ b/kipubot/handlers/__init__.py @@ -5,7 +5,6 @@ "bot_added_handler", "winner_handler", "graph_handler", - "expected_value_handler", "raffle_setup_handler", "no_dm_handler", "error_handler", @@ -14,7 +13,7 @@ from ._bot_added_handler import bot_added_handler from ._error_handler import error_handler from ._excel_file_handler import excel_file_handler -from ._graph_handlers import expected_value_handler, graph_handler +from ._graph_handler import graph_handler from ._moro_handler import moro_handler from ._no_dm_handler import no_dm_handler from ._raffle_setup_handler import raffle_setup_handler diff --git a/kipubot/handlers/_bot_added_handler.py b/kipubot/handlers/_bot_added_handler.py index 69216d1..4c4c333 100644 --- a/kipubot/handlers/_bot_added_handler.py +++ b/kipubot/handlers/_bot_added_handler.py @@ -8,6 +8,13 @@ async def bot_added(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if ( + not update.my_chat_member + or not update.effective_chat + or not update.effective_user + ): + return + # when bot is added to channel: # -> add the channel to a database # -> set the adder as the current winner of the channel @@ -15,7 +22,11 @@ async def bot_added(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if update.my_chat_member.new_chat_member.status != ChatMemberStatus.LEFT: chat_id = update.effective_chat.id - title = update.effective_chat.title + title = ( + update.effective_chat.title + if update.effective_chat.title + else "untitled chat" + ) user_id = update.effective_user.id admins = await update.effective_chat.get_administrators() admin_ids = list(set([admin.user.id for admin in admins] + [user_id])) diff --git a/kipubot/handlers/_error_handler.py b/kipubot/handlers/_error_handler.py index 694c1c2..c87984c 100644 --- a/kipubot/handlers/_error_handler.py +++ b/kipubot/handlers/_error_handler.py @@ -7,13 +7,13 @@ from telegram.constants import ParseMode from telegram.ext import ContextTypes -from kipubot import DEVELOPER_CHAT_ID +from kipubot import config from kipubot.constants import STRINGS _logger = logging.getLogger(__name__) -async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: +async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None: """Log the error and send a telegram message to notify the developer.""" # Log the error before we do anything else, # so we can see it even if something breaks. @@ -22,9 +22,11 @@ async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N # traceback.format_exception returns the usual python message about an exception, # but as a list of strings rather than a single string, # so we have to join them together. - tb_list = traceback.format_exception( - None, context.error, context.error.__traceback__ + context_error = context.error + context_traceback = ( + context_error.__traceback__ if context_error is not None else None ) + tb_list = traceback.format_exception(None, context.error, context_traceback) tb_string = "".join(tb_list) # Build the message with some markup and additional information about what happened. @@ -42,11 +44,14 @@ async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N f"
{html.escape(tb_string)}
" ) - # Finally, send the message - await context.bot.send_message( - chat_id=DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML - ) + # Finally, send the message if DEVELOPER_CHAT_ID is set. + if config.DEVELOPER_CHAT_ID: + await context.bot.send_message( + chat_id=config.DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML + ) + # Also send a message to the user who triggered the error. - await context.bot.send_message( - chat_id=update.effective_chat.id, text=STRINGS["server_error"] - ) + if isinstance(update, Update) and update.effective_chat is not None: + await context.bot.send_message( + chat_id=update.effective_chat.id, text=STRINGS["server_error"] + ) diff --git a/kipubot/handlers/_excel_file_handler.py b/kipubot/handlers/_excel_file_handler.py index 4ebfc56..bf44205 100644 --- a/kipubot/handlers/_excel_file_handler.py +++ b/kipubot/handlers/_excel_file_handler.py @@ -1,14 +1,22 @@ import os from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.ext import ContextTypes, ConversationHandler, MessageHandler +from telegram.ext import ContextTypes, ConversationHandler, MessageHandler, filters -from kipubot.constants import STRINGS +from kipubot.constants import EXCEL_MIME, STRINGS from kipubot.db import get_chats_where_winner from kipubot.utils import validate_excel -async def excel_file(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str | None: +async def excel_file(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + if ( + not update.effective_chat + or not update.effective_user + or not update.message + or not update.message.document + ): + return ConversationHandler.END + user_id = update.effective_user.id dm_id = update.effective_chat.id @@ -26,13 +34,12 @@ async def excel_file(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str if not os.path.exists(base_path): os.mkdir(base_path) - with open(excel_path, "wb") as f: - await file.download(out=f) + await file.download_to_drive(excel_path) if not validate_excel(excel_path): await update.message.reply_text(STRINGS["invalid_file"]) os.remove(excel_path) - return + return ConversationHandler.END chat_buttons = [] @@ -55,5 +62,9 @@ async def excel_file(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str STRINGS["choose_channel"], reply_markup=reply_markup ) + return ConversationHandler.END + -excel_file_handler = MessageHandler() +excel_file_handler = MessageHandler( + filters.Document.MimeType(EXCEL_MIME) & filters.ChatType.PRIVATE, excel_file +) diff --git a/kipubot/handlers/_graph_handlers.py b/kipubot/handlers/_graph_handler.py similarity index 64% rename from kipubot/handlers/_graph_handlers.py rename to kipubot/handlers/_graph_handler.py index 01c0f8b..afecdcd 100644 --- a/kipubot/handlers/_graph_handlers.py +++ b/kipubot/handlers/_graph_handler.py @@ -1,5 +1,3 @@ -from enum import Enum - import psycopg.errors as pserrors from telegram import Update from telegram.ext import CommandHandler, ContextTypes, filters @@ -8,14 +6,16 @@ from kipubot.errors import NoEntriesError, NoRaffleError from kipubot.utils import generate_expected, generate_graph - -class GraphType(Enum): - EXPECTED = "expected" - GRAPH = "graph" +GRAPH_TYPE = dict( + expected="expected", + odotusarvo="expected", + graph="graph", + kuvaaja="graph", +) -def get_graph_img(graph_type: GraphType) -> str: - if graph_type == GraphType.EXPECTED: +def get_graph_img(graph_type: str) -> str: + if graph_type == "expected": return "expected.png" return "graph.png" @@ -24,14 +24,25 @@ def get_graph_img(graph_type: GraphType) -> str: async def graph( update: Update, _context: ContextTypes.DEFAULT_TYPE, - graph_type: GraphType = GraphType.GRAPH, ) -> None: + if ( + not update.effective_chat + or not update.effective_user + or not update.message + or not update.message.text + ): + return None + chat_id = update.effective_chat.id - chat_title = update.effective_chat.title + chat_title = ( + update.effective_chat.title if update.effective_chat.title else "untitled chat" + ) + graph_type_text = update.message.text[1::] + graph_type = GRAPH_TYPE[graph_type_text] graph_path = f"data/{chat_id}/{get_graph_img(graph_type)}" try: - if graph_type == GraphType.EXPECTED: + if graph_type == "expected": generate_expected(graph_path, chat_id, chat_title) else: generate_graph(graph_path, chat_id, chat_title) @@ -54,10 +65,6 @@ async def graph( await update.message.reply_text(STRINGS["no_data"] % {"chat_title": chat_title}) -graph_handler = CommandHandler(["kuvaaja", "graph"], graph, ~filters.ChatType.PRIVATE) - -expected_value_handler = CommandHandler( - ["odotusarvo", "expected"], - lambda u, c: graph(u, c, graph_type=GraphType.EXPECTED), - ~filters.ChatType.PRIVATE, +graph_handler = CommandHandler( + ["kuvaaja", "graph", "odotusarvo", "expected"], graph, ~filters.ChatType.PRIVATE ) diff --git a/kipubot/handlers/_moro_handler.py b/kipubot/handlers/_moro_handler.py index 2eb87ea..985b608 100644 --- a/kipubot/handlers/_moro_handler.py +++ b/kipubot/handlers/_moro_handler.py @@ -7,6 +7,9 @@ async def hello(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: + if not update.effective_chat or not update.effective_user or not update.message: + return None + chat_id = update.effective_chat.id user_id = update.effective_user.id username = update.effective_user.username diff --git a/kipubot/handlers/_no_dm_handler.py b/kipubot/handlers/_no_dm_handler.py index cf02ed1..bd9d408 100644 --- a/kipubot/handlers/_no_dm_handler.py +++ b/kipubot/handlers/_no_dm_handler.py @@ -5,6 +5,8 @@ async def chat_only(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: + if not update.message: + return None await update.message.reply_text(STRINGS["no_dm_warn"]) diff --git a/kipubot/handlers/_raffle_setup_handler.py b/kipubot/handlers/_raffle_setup_handler.py index 7a9f475..a9b5627 100644 --- a/kipubot/handlers/_raffle_setup_handler.py +++ b/kipubot/handlers/_raffle_setup_handler.py @@ -3,13 +3,13 @@ import pandas as pd from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.ext import CallbackContext, CallbackQueryHandler, ConversationHandler +from telegram.ext import CallbackQueryHandler, ContextTypes, ConversationHandler from kipubot.constants import STRINGS from kipubot.errors import NoRaffleError from kipubot.utils import ( get_cur_time_hel, - get_raffle, + get_raffle_stats, int_price_to_str, is_float, is_int, @@ -25,24 +25,36 @@ # ---------------- -async def cancel_convo(update: Update, context: CallbackContext) -> int: +async def cancel_convo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: query = update.callback_query - await query.message.edit_text(STRINGS["cancelled"], reply_markup=None) - context.user_data.clear() + if not query or not query.message: + return ConversationHandler.END + await query.answer(STRINGS["cancelled"]) + await query.message.edit_text(STRINGS["cancelled"]) + if context.user_data: + context.user_data.clear() return ConversationHandler.END -async def convo_error(update: Update, context: CallbackContext) -> int: +async def convo_error(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: query = update.callback_query - await query.message.edit_text(STRINGS["unknown_error"], reply_markup=None) - context.user_data.clear() + if not query or not query.message: + return ConversationHandler.END + await query.answer(STRINGS["unknown_error"]) + await query.message.edit_text(STRINGS["unknown_error"]) + if context.user_data: + context.user_data.clear() return ConversationHandler.END -async def convo_timeout(update: Update, context: CallbackContext) -> int: +async def convo_timeout(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: query = update.callback_query - await query.message.edit_text(STRINGS["timed_out"], reply_markup=None) - context.user_data.clear() + if not query or not query.message: + return ConversationHandler.END + await query.answer(STRINGS["timed_out"]) + await query.message.edit_text(STRINGS["timed_out"]) + if context.user_data: + context.user_data.clear() return ConversationHandler.END @@ -147,8 +159,12 @@ def fee_keyboard() -> InlineKeyboardMarkup: # ================= -async def setup_raffle(update: Update, context: CallbackContext) -> str | int: +async def setup_raffle(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str | int: query = update.callback_query + if not query or not query.message or not query.data or context.user_data is None: + if query: + await query.answer(STRINGS["unknown_error"]) + return ConversationHandler.END if query.data == "raffle:cancel": return await cancel_convo(update, context) @@ -168,7 +184,7 @@ async def setup_raffle(update: Update, context: CallbackContext) -> str | int: context.user_data["raffle_chat_title"] = chat_title try: - get_raffle(chat_id) + get_raffle_stats(chat_id) msg = ( STRINGS["raffle_setup_base"] + STRINGS["raffle_setup_update_or_new"] @@ -191,8 +207,12 @@ async def setup_raffle(update: Update, context: CallbackContext) -> str | int: return await convo_error(update, context) -async def setup_start_date(update: Update, context: CallbackContext) -> str | None: +async def setup_start_date( + update: Update, context: ContextTypes.DEFAULT_TYPE +) -> str | int: query = update.callback_query + if not query or not query.message or not query.data or not context.user_data: + return ConversationHandler.END if query.data == "raffle:setup:new": context.user_data["raffle_start_date"] = get_cur_time_hel().floor(freq="15T") @@ -204,7 +224,7 @@ async def setup_start_date(update: Update, context: CallbackContext) -> str | No ): diff = float(query.data.split(":")[4]) old_date = context.user_data["raffle_start_date"] - new_date = old_date + pd.Timedelta(diff, unit="h") + new_date = old_date + pd.Timedelta(diff, unit="h") # type: ignore context.user_data["raffle_start_date"] = new_date @@ -223,11 +243,15 @@ async def setup_start_date(update: Update, context: CallbackContext) -> str | No return "raffle_setup_state:start_date" - return None + return ConversationHandler.END -async def setup_end_date(update: Update, context: CallbackContext) -> str | None: +async def setup_end_date( + update: Update, context: ContextTypes.DEFAULT_TYPE +) -> str | int: query = update.callback_query + if not query or not query.message or not query.data or not context.user_data: + return ConversationHandler.END if query.data == "raffle:date:start:confirmed": context.user_data["raffle_end_date"] = context.user_data["raffle_start_date"] @@ -235,9 +259,9 @@ async def setup_end_date(update: Update, context: CallbackContext) -> str | None if ( query.data.startswith("raffle:date:end:update") and len(query.data.split(":")) == RAFFLE_DATE_UPDATE_DATA_LENGTH - and is_float(query.data.split(":")[4]) + and is_int(query.data.split(":")[4]) ): - diff = float(query.data.split(":")[4]) + diff = int(query.data.split(":")[4]) old_date = context.user_data["raffle_end_date"] new_date = old_date + pd.Timedelta(diff, unit="h") @@ -272,11 +296,13 @@ async def setup_end_date(update: Update, context: CallbackContext) -> str | None return "raffle_setup_state:end_date" - return None + return ConversationHandler.END -async def setup_fee(update: Update, context: CallbackContext) -> str | None: +async def setup_fee(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str | int: query = update.callback_query + if not query or not query.message or not query.data or not context.user_data: + return ConversationHandler.END if query.data == "raffle:date:end:confirmed": context.user_data["raffle_fee"] = 100 @@ -323,11 +349,20 @@ async def setup_fee(update: Update, context: CallbackContext) -> str | None: return "raffle_setup_state:fee" - return None + return ConversationHandler.END -async def finish_setup(update: Update, context: CallbackContext) -> int | None: +async def finish_setup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int | int: query = update.callback_query + if ( + not query + or not query.message + or not query.data + or not context.user_data + or not update.effective_chat + or not update.effective_user + ): + return ConversationHandler.END dm_id = update.effective_chat.id if query.data == "raffle:setup:old": @@ -335,7 +370,7 @@ async def finish_setup(update: Update, context: CallbackContext) -> int | None: chat_id = context.user_data["raffle_chat_id"] dm_id = update.effective_chat.id - start_date, end_date, entry_fee, _ = get_raffle(chat_id) + start_date, end_date, entry_fee = get_raffle_stats(chat_id) excel_path = f"data/{dm_id}/data.xlsx" df = read_excel_to_df(excel_path, start_date, end_date) save_raffle(chat_id, start_date, end_date, entry_fee, df) @@ -378,7 +413,8 @@ async def finish_setup(update: Update, context: CallbackContext) -> int | None: "fee": int_price_to_str(fee), } - await query.message.edit_text(msg, reply_markup=None) + await query.message.edit_text(msg) + await query.answer() await context.bot.send_message( chat_id, STRINGS["raffle_created_chat"] @@ -391,7 +427,7 @@ async def finish_setup(update: Update, context: CallbackContext) -> int | None: return ConversationHandler.END - return None + return ConversationHandler.END raffle_setup_handler = ConversationHandler( diff --git a/kipubot/handlers/_start_handler.py b/kipubot/handlers/_start_handler.py index d240b1b..145ecb5 100644 --- a/kipubot/handlers/_start_handler.py +++ b/kipubot/handlers/_start_handler.py @@ -5,6 +5,9 @@ async def start(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: + if not update.message: + return None + with open("kipubot/resources/info.png", "rb") as f: await update.message.reply_photo(caption=STRINGS["start_prompt"], photo=f) diff --git a/kipubot/handlers/_winner_handler.py b/kipubot/handlers/_winner_handler.py index c300bb7..9684260 100644 --- a/kipubot/handlers/_winner_handler.py +++ b/kipubot/handlers/_winner_handler.py @@ -24,6 +24,15 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: + if ( + not update.effective_chat + or not update.effective_user + or not update.message + or not update.message.entities + or not update.message.text + ): + return None + # only usable by admin, previous winner (in case of typos) and current winner # usage: /winner @username # -> set the winner to the given username @@ -34,7 +43,7 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: if len(ent) != TWO_ENTITIES or ent[1].type != MessageEntityType.MENTION: await update.message.reply_text(STRINGS["invalid_winner_usage"]) - return + return None username = update.message.text.split(" ")[1][1:] @@ -50,12 +59,12 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: return registered_member_ids = get_registered_member_ids(chat_id) - registered_members = [ + all_members = [ await get_chat_member_opt(update.effective_chat, member_id) for member_id in registered_member_ids ] # drop None values - registered_members = [m for m in registered_members if m] + registered_members = [m for m in all_members if m] supposed_winner = [ member for member in registered_members if member.user.username == username ] diff --git a/kipubot/utils.py b/kipubot/utils.py index fc4ee92..40072a8 100644 --- a/kipubot/utils.py +++ b/kipubot/utils.py @@ -1,17 +1,17 @@ import os import re -from typing import NamedTuple +from typing import Any, NamedTuple -import matplotlib.dates as mdates -import matplotlib.pyplot as plt +import matplotlib.dates as mdates # type: ignore +import matplotlib.pyplot as plt # type: ignore import numpy as np import pandas as pd import pytz -import uncertainties as unc -import uncertainties.unumpy as unp -from matplotlib.ticker import AutoMinorLocator -from scipy import stats -from scipy.optimize import curve_fit +import uncertainties as unc # type: ignore +import uncertainties.unumpy as unp # type: ignore +from matplotlib.ticker import AutoMinorLocator # type: ignore +from scipy import stats # type: ignore +from scipy.optimize import curve_fit # type: ignore from telegram import Chat, ChatMember from telegram.error import BadRequest @@ -19,11 +19,17 @@ from kipubot.errors import NoRaffleError +class RaffleStatsData(NamedTuple): + start_date: pd.Timestamp + end_date: pd.Timestamp + entry_fee: int + + class RaffleData(NamedTuple): start_date: pd.Timestamp end_date: pd.Timestamp entry_fee: int - df: pd.DataFrame | None + df: pd.DataFrame def is_int(x: str) -> bool: @@ -45,9 +51,11 @@ def is_float(x: str) -> bool: def int_price_to_str(num: int) -> str: - num = num / 100.0 + float_num = num / 100.0 - str_num: str = format(num, ".2f") if num >= 0 else "-" + format(-num, ".2f") + str_num: str = ( + format(float_num, ".2f") if float_num >= 0 else "-" + format(-float_num, ".2f") + ) euros, cents = str_num.split(".") @@ -86,9 +94,7 @@ def preband(x, xd, yd, p, func): return lpb, upb -def fit_timedata( - x_series: "pd.Series[np.int64]", y_series: "pd.Series[np.int64]" -): # pylint: disable=too-many-locals +def fit_timedata(x_series: "pd.Series[Any]", y_series: "pd.Series[Any]"): # ignore the end date in curve fitting x = x_series.values[:-1] y = y_series.values[:-1] @@ -96,7 +102,6 @@ def fit_timedata( def f(x, slope, intercept): return slope * x + intercept - # pylint: disable=unbalanced-tuple-unpacking popt, pcov = curve_fit(f, x, y) a, b = unc.correlated_values(popt, pcov) @@ -115,9 +120,9 @@ def f(x, slope, intercept): lpb, upb = preband(px, x, y, popt, f) # convert back to dates - px = [pd.to_datetime(x, unit="ns") for x in px] + px_dates = [pd.to_datetime(x, unit="ns") for x in px] - return (px, nom, std, lpb, upb) + return (px_dates, nom, std, lpb, upb) def remove_emojis(text: str) -> str: @@ -166,7 +171,7 @@ def read_excel_to_df( return df -def get_raffle(chat_id: int, *, include_df: bool = False) -> RaffleData: +def get_raffle_stats(chat_id: int) -> RaffleStatsData: query_result = db.get_raffle_data(chat_id) if query_result is None: @@ -175,12 +180,21 @@ def get_raffle(chat_id: int, *, include_df: bool = False) -> RaffleData: _, start_date, end_date, entry_fee, dates, entries, amounts = query_result - if include_df: - df = pd.DataFrame(data={"date": dates, "name": entries, "amount": amounts}) - df.set_index("date", inplace=True) - return RaffleData(start_date, end_date, entry_fee, df) + return RaffleStatsData(start_date, end_date, entry_fee) + + +def get_raffle(chat_id: int) -> RaffleData: + query_result = db.get_raffle_data(chat_id) + + if query_result is None: + error_text = f"No raffle found for chat {chat_id}" + raise NoRaffleError(error_text) + + _, start_date, end_date, entry_fee, dates, entries, amounts = query_result - return RaffleData(start_date, end_date, entry_fee, None) + df = pd.DataFrame(data={"date": dates, "name": entries, "amount": amounts}) + df.set_index("date", inplace=True) + return RaffleData(start_date, end_date, entry_fee, df) def get_cur_time_hel() -> pd.Timestamp: @@ -267,7 +281,7 @@ def configure_and_save_plot(out_img_path: str) -> None: def generate_graph(out_img_path: str, chat_id: int, chat_title: str) -> None: # -- get raffle data -- - raffle_data = get_raffle(chat_id, include_df=True) + raffle_data = get_raffle(chat_id) # -- parse and fit data -- start_date, end_date, _, df = parse_graph(raffle_data) @@ -307,7 +321,7 @@ def generate_graph(out_img_path: str, chat_id: int, chat_title: str) -> None: def generate_expected(out_img_path: str, chat_id: int, chat_title: str) -> None: # -- get raffle data -- - raffle_data = get_raffle(chat_id, include_df=True) + raffle_data = get_raffle(chat_id) # -- parse and fit data -- start_date, _, entry_fee, df = parse_expected(raffle_data) diff --git a/poetry.lock b/poetry.lock index b0acdf8..d44455c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -22,23 +22,34 @@ test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>= trio = ["trio (>=0.16,<0.22)"] [[package]] -name = "attrs" -version = "23.1.0" -description = "Classes Without Boilerplate" -category = "dev" +name = "apscheduler" +version = "3.10.1" +description = "In-process task scheduler with Cron-like capabilities" +category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "APScheduler-3.10.1-py3-none-any.whl", hash = "sha256:e813ad5ada7aff36fb08cdda746b520531eaac7757832abc204868ba78e0c8f6"}, + {file = "APScheduler-3.10.1.tar.gz", hash = "sha256:0293937d8f6051a0f493359440c1a1b93e882c57daf0197afeff0e727777b96e"}, ] +[package.dependencies] +pytz = "*" +setuptools = ">=0.7" +six = ">=1.4.0" +tzlocal = ">=2.0,<3.0.0 || >=4.0.0" + [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +doc = ["sphinx", "sphinx-rtd-theme"] +gevent = ["gevent"] +mongodb = ["pymongo (>=3.0)"] +redis = ["redis (>=3.0)"] +rethinkdb = ["rethinkdb (>=2.4.0)"] +sqlalchemy = ["sqlalchemy (>=1.4)"] +testing = ["pytest", "pytest-asyncio", "pytest-cov", "pytest-tornado5"] +tornado = ["tornado (>=4.3)"] +twisted = ["twisted"] +zookeeper = ["kazoo"] [[package]] name = "black" @@ -349,21 +360,6 @@ files = [ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] -[[package]] -name = "importlab" -version = "0.8" -description = "A library to calculate python dependency graphs." -category = "dev" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "importlab-0.8-py2.py3-none-any.whl", hash = "sha256:a009ccde7b549b16f3e6b034fea748febc8d45ded9e8a09370a8f994acfda25b"}, - {file = "importlab-0.8.tar.gz", hash = "sha256:b24b3aac3b073966ae42fb2d3a7764f3377b30bb72c0d411fe29134cc9276e86"}, -] - -[package.dependencies] -networkx = ">=2" - [[package]] name = "iniconfig" version = "2.0.0" @@ -376,24 +372,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - [[package]] name = "kiwisolver" version = "1.4.4" @@ -472,114 +450,6 @@ files = [ {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, ] -[[package]] -name = "libcst" -version = "0.4.9" -description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 programs." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "libcst-0.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f9e42085c403e22201e5c41e707ef73e4ea910ad9fc67983ceee2368097f54e"}, - {file = "libcst-0.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1266530bf840cc40633a04feb578bb4cac1aa3aea058cc3729e24eab09a8e996"}, - {file = "libcst-0.4.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9679177391ccb9b0cdde3185c22bf366cb672457c4b7f4031fcb3b5e739fbd6"}, - {file = "libcst-0.4.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d67bc87e0d8db9434f2ea063734938a320f541f4c6da1074001e372f840f385d"}, - {file = "libcst-0.4.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e316da5a126f2a9e1d7680f95f907b575f082a35e2f8bd5620c59b2aaaebfe0a"}, - {file = "libcst-0.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:7415569ab998a85b0fc9af3a204611ea7fadb2d719a12532c448f8fc98f5aca4"}, - {file = "libcst-0.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:15ded11ff7f4572f91635e02b519ae959f782689fdb4445bbebb7a3cc5c71d75"}, - {file = "libcst-0.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b266867b712a120fad93983de432ddb2ccb062eb5fd2bea748c9a94cb200c36"}, - {file = "libcst-0.4.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045b3b0b06413cdae6e9751b5f417f789ffa410f2cb2815e3e0e0ea6bef10ec0"}, - {file = "libcst-0.4.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e799add8fba4976628b9c1a6768d73178bf898f0ed1bd1322930c2d3db9063ba"}, - {file = "libcst-0.4.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10479371d04ee8dc978c889c1774bbf6a83df88fa055fcb0159a606f6679c565"}, - {file = "libcst-0.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:7a98286cbbfa90a42d376900c875161ad02a5a2a6b7c94c0f7afd9075e329ce4"}, - {file = "libcst-0.4.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:400166fc4efb9aa06ce44498d443aa78519082695b1894202dd73cd507d2d712"}, - {file = "libcst-0.4.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46123863fba35cc84f7b54dd68826419cabfd9504d8a101c7fe3313ea03776f9"}, - {file = "libcst-0.4.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27be8db54c0e5fe440021a771a38b81a7dbc23cd630eb8b0e9828b7717f9b702"}, - {file = "libcst-0.4.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:132bec627b064bd567e7e4cd6c89524d02842151eb0d8f5f3f7ffd2579ec1b09"}, - {file = "libcst-0.4.9-cp37-cp37m-win_amd64.whl", hash = "sha256:596860090aeed3ee6ad1e59c35c6c4110a57e4e896abf51b91cae003ec720a11"}, - {file = "libcst-0.4.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4487608258109f774300466d4ca97353df29ae6ac23d1502e13e5509423c9d5"}, - {file = "libcst-0.4.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa53993e9a2853efb3ed3605da39f2e7125df6430f613eb67ef886c1ce4f94b5"}, - {file = "libcst-0.4.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6ce794483d4c605ef0f5b199a49fb6996f9586ca938b7bfef213bd13858d7ab"}, - {file = "libcst-0.4.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:786e562b54bbcd17a060d1244deeef466b7ee07fe544074c252c4a169e38f1ee"}, - {file = "libcst-0.4.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794250d2359edd518fb698e5d21c38a5bdfc5e4a75d0407b4c19818271ce6742"}, - {file = "libcst-0.4.9-cp38-cp38-win_amd64.whl", hash = "sha256:76491f67431318c3145442e97dddcead7075b074c59eac51be7cc9e3fffec6ee"}, - {file = "libcst-0.4.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3cf48d7aec6dc54b02aec0b1bb413c5bb3b02d852fd6facf1f05c7213e61a176"}, - {file = "libcst-0.4.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b3348c6b7711a5235b133bd8e11d22e903c388db42485b8ceb5f2aa0fae9b9f"}, - {file = "libcst-0.4.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e33b66762efaa014c38819efae5d8f726dd823e32d5d691035484411d2a2a69"}, - {file = "libcst-0.4.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1350d375d3fb9b20a6cf10c09b2964baca9be753a033dde7c1aced49d8e58387"}, - {file = "libcst-0.4.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3822056dc13326082362db35b3f649e0f4a97e36ddb4e487441da8e0fb9db7b3"}, - {file = "libcst-0.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:183636141b839aa35b639e100883813744523bc7c12528906621121731b28443"}, - {file = "libcst-0.4.9.tar.gz", hash = "sha256:01786c403348f76f274dbaf3888ae237ffb73e6ed6973e65eba5c1fc389861dd"}, -] - -[package.dependencies] -pyyaml = ">=5.2" -typing-extensions = ">=3.7.4.2" -typing-inspect = ">=0.4.0" - -[package.extras] -dev = ["Sphinx (>=5.1.1)", "black (==22.10.0)", "coverage (>=4.5.4)", "fixit (==0.1.1)", "flake8 (>=3.7.8,<5)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.2)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<0.14)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.9)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.0.1)", "usort (==1.0.5)"] - -[[package]] -name = "markupsafe" -version = "2.1.2" -description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, - {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, -] - [[package]] name = "matplotlib" version = "3.7.1" @@ -642,6 +512,53 @@ pillow = ">=6.2.0" pyparsing = ">=2.3.1" python-dateutil = ">=2.7" +[[package]] +name = "mypy" +version = "1.2.0" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:701189408b460a2ff42b984e6bd45c3f41f0ac9f5f58b8873bbedc511900086d"}, + {file = "mypy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe91be1c51c90e2afe6827601ca14353bbf3953f343c2129fa1e247d55fd95ba"}, + {file = "mypy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d26b513225ffd3eacece727f4387bdce6469192ef029ca9dd469940158bc89e"}, + {file = "mypy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3a2d219775a120581a0ae8ca392b31f238d452729adbcb6892fa89688cb8306a"}, + {file = "mypy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:2e93a8a553e0394b26c4ca683923b85a69f7ccdc0139e6acd1354cc884fe0128"}, + {file = "mypy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3efde4af6f2d3ccf58ae825495dbb8d74abd6d176ee686ce2ab19bd025273f41"}, + {file = "mypy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:695c45cea7e8abb6f088a34a6034b1d273122e5530aeebb9c09626cea6dca4cb"}, + {file = "mypy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0e9464a0af6715852267bf29c9553e4555b61f5904a4fc538547a4d67617937"}, + {file = "mypy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8293a216e902ac12779eb7a08f2bc39ec6c878d7c6025aa59464e0c4c16f7eb9"}, + {file = "mypy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f46af8d162f3d470d8ffc997aaf7a269996d205f9d746124a179d3abe05ac602"}, + {file = "mypy-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:031fc69c9a7e12bcc5660b74122ed84b3f1c505e762cc4296884096c6d8ee140"}, + {file = "mypy-1.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:390bc685ec209ada4e9d35068ac6988c60160b2b703072d2850457b62499e336"}, + {file = "mypy-1.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4b41412df69ec06ab141808d12e0bf2823717b1c363bd77b4c0820feaa37249e"}, + {file = "mypy-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4e4a682b3f2489d218751981639cffc4e281d548f9d517addfd5a2917ac78119"}, + {file = "mypy-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a197ad3a774f8e74f21e428f0de7f60ad26a8d23437b69638aac2764d1e06a6a"}, + {file = "mypy-1.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9a084bce1061e55cdc0493a2ad890375af359c766b8ac311ac8120d3a472950"}, + {file = "mypy-1.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaeaa0888b7f3ccb7bcd40b50497ca30923dba14f385bde4af78fac713d6d6f6"}, + {file = "mypy-1.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bea55fc25b96c53affab852ad94bf111a3083bc1d8b0c76a61dd101d8a388cf5"}, + {file = "mypy-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:4c8d8c6b80aa4a1689f2a179d31d86ae1367ea4a12855cc13aa3ba24bb36b2d8"}, + {file = "mypy-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70894c5345bea98321a2fe84df35f43ee7bb0feec117a71420c60459fc3e1eed"}, + {file = "mypy-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4a99fe1768925e4a139aace8f3fb66db3576ee1c30b9c0f70f744ead7e329c9f"}, + {file = "mypy-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023fe9e618182ca6317ae89833ba422c411469156b690fde6a315ad10695a521"}, + {file = "mypy-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d19f1a239d59f10fdc31263d48b7937c585810288376671eaf75380b074f238"}, + {file = "mypy-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:2de7babe398cb7a85ac7f1fd5c42f396c215ab3eff731b4d761d68d0f6a80f48"}, + {file = "mypy-1.2.0-py3-none-any.whl", hash = "sha256:d8e9187bfcd5ffedbe87403195e1fc340189a68463903c39e2b63307c9fa0394"}, + {file = "mypy-1.2.0.tar.gz", hash = "sha256:f70a40410d774ae23fcb4afbbeca652905a04de7948eaf0b1789c8d1426b72d1"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -655,53 +572,26 @@ files = [ ] [[package]] -name = "networkx" -version = "2.8.3" -description = "Python package for creating and manipulating graphs and networks" -category = "dev" +name = "nptyping" +version = "2.5.0" +description = "Type hints for NumPy." +category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "networkx-2.8.3-py3-none-any.whl", hash = "sha256:f151edac6f9b0cf11fecce93e236ac22b499bb9ff8d6f8393b9fef5ad09506cc"}, - {file = "networkx-2.8.3.tar.gz", hash = "sha256:67fab04a955a73eb660fe7bf281b6fa71a003bc6e23a92d2f6227654c5223dbe"}, + {file = "nptyping-2.5.0-py3-none-any.whl", hash = "sha256:764e51836faae33a7ae2e928af574cfb701355647accadcc89f2ad793630b7c8"}, + {file = "nptyping-2.5.0.tar.gz", hash = "sha256:e3d35b53af967e6fb407c3016ff9abae954d3a0568f7cc13a461084224e8e20a"}, ] -[package.extras] -default = ["matplotlib (>=3.4)", "numpy (>=1.19)", "pandas (>=1.3)", "scipy (>=1.8)"] -developer = ["mypy (>=0.960)", "pre-commit (>=2.19)"] -doc = ["nb2plots (>=0.6)", "numpydoc (>=1.3)", "pillow (>=9.1)", "pydata-sphinx-theme (>=0.8.1)", "sphinx (>=4.5)", "sphinx-gallery (>=0.10)", "texext (>=0.6.6)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.9)", "sympy (>=1.10)"] -test = ["codecov (>=2.1)", "pytest (>=7.1)", "pytest-cov (>=3.0)"] - -[[package]] -name = "ninja" -version = "1.11.1" -description = "Ninja is a small build system with a focus on speed" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "ninja-1.11.1-py2.py3-none-macosx_10_9_universal2.macosx_10_9_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:f48c3c6eea204062f6bbf089dfc63e1ad41a08640e1da46ef2b30fa426f7ce23"}, - {file = "ninja-1.11.1-py2.py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:edec1053e141253076b2df7ec03a246ff581e9270aa1ca9759397b21e2760e57"}, - {file = "ninja-1.11.1-py2.py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:817e2aee2a4d28a708a67bcfba1817ae502c32c6d8ef80e50d63b0f23adf3a08"}, - {file = "ninja-1.11.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df11b8afea0501883e33faeb1c43d2ef67f466d5f4bd85f9c376e9a93a43a277"}, - {file = "ninja-1.11.1-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a564fe755ddfbdbccb07b0b758e3f8460e5f8ba1adaab40a5eaa2f8c01ce68"}, - {file = "ninja-1.11.1-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c474326e11fba3f8c2582715d79216292e327d3335367c0e87e9647a002cc4a"}, - {file = "ninja-1.11.1-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f6465a7efe6473a2a34edab83633594de19d59406a727316e1367ebcc528908"}, - {file = "ninja-1.11.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:642cb64d859276998f14972724850e0c5b7febbc1bce3d2065b7e0cb7d3a0b79"}, - {file = "ninja-1.11.1-py2.py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:60179bb4f22c88279c53a5402bb5fe81c97c627a28d93c737d1fa067d892115d"}, - {file = "ninja-1.11.1-py2.py3-none-musllinux_1_1_i686.whl", hash = "sha256:34753459493543782d87267e4cad63dd4639b07f8394ffe6d4417e9eda05c8a8"}, - {file = "ninja-1.11.1-py2.py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:779f228e407c54a8b6e4cbf8f835489998dd250f67bf1b9bd7b8a8ab6bdcdc7b"}, - {file = "ninja-1.11.1-py2.py3-none-musllinux_1_1_s390x.whl", hash = "sha256:ba50a32424912e5f3ee40d791b506a160dc0eeda7de5ad8faebe7aa8006244dc"}, - {file = "ninja-1.11.1-py2.py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3b28b595ed580752240ade7821b6cb7a5a4c6a604c865dc474bd38f06e2eb7f5"}, - {file = "ninja-1.11.1-py2.py3-none-win32.whl", hash = "sha256:3329b4b7c1694730772522a3ba0ba40fd15c012476ed3e1c9f0fd9e76190394e"}, - {file = "ninja-1.11.1-py2.py3-none-win_amd64.whl", hash = "sha256:4e547bc759c570773d83d110c41fd5ca9a94c0a9a8388f5a3ea37bdf97d002b0"}, - {file = "ninja-1.11.1-py2.py3-none-win_arm64.whl", hash = "sha256:8cf96f92ccc851c600cb3e1251c34db06f1dd682de79188ad490c33cddc66981"}, - {file = "ninja-1.11.1.tar.gz", hash = "sha256:c833a47d39b2d1eee3f9ca886fa1581efd5be6068b82734ac229961ee8748f90"}, -] +[package.dependencies] +numpy = {version = ">=1.20.0,<2.0.0", markers = "python_version >= \"3.8\""} [package.extras] -test = ["codecov (>=2.0.5)", "coverage (>=4.2)", "flake8 (>=3.0.4)", "pytest (>=4.5.0)", "pytest-cov (>=2.7.1)", "pytest-runner (>=5.1)", "pytest-virtualenv (>=1.7.0)", "virtualenv (>=15.0.3)"] +build = ["invoke (>=1.6.0)", "pip-tools (>=6.5.0)"] +complete = ["pandas", "pandas-stubs-fork"] +dev = ["autoflake", "beartype (<0.10.0)", "beartype (>=0.10.0)", "black", "codecov (>=2.1.0)", "coverage", "feedparser", "invoke (>=1.6.0)", "isort", "mypy", "pandas", "pandas-stubs-fork", "pip-tools (>=6.5.0)", "pylint", "pyright", "setuptools", "typeguard", "wheel"] +pandas = ["pandas", "pandas-stubs-fork"] +qa = ["autoflake", "beartype (<0.10.0)", "beartype (>=0.10.0)", "black", "codecov (>=2.1.0)", "coverage", "feedparser", "isort", "mypy", "pylint", "pyright", "setuptools", "typeguard", "wheel"] [[package]] name = "numpy" @@ -832,6 +722,21 @@ sql-other = ["SQLAlchemy (>=1.4.16)"] test = ["hypothesis (>=6.34.2)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.6.3)"] +[[package]] +name = "pandas-stubs" +version = "2.0.0.230412" +description = "Type annotations for pandas" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pandas_stubs-2.0.0.230412-py3-none-any.whl", hash = "sha256:311ab8b42ee574d9fea5061d1f63aeca297e472de6073ba84bf2a017c6cb1b6b"}, + {file = "pandas_stubs-2.0.0.230412.tar.gz", hash = "sha256:016f567cb9947edd0067ea2665ab00b77fa47e73a65ce1a097de4f499b3485c0"}, +] + +[package.dependencies] +types-pytz = ">=2022.1.1" + [[package]] name = "pastel" version = "0.2.1" @@ -1012,19 +917,57 @@ pool = ["psycopg-pool"] test = ["mypy (>=0.990)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-asyncio (>=0.17)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] [[package]] -name = "pydot" -version = "1.4.2" -description = "Python interface to Graphviz's Dot" -category = "dev" +name = "pydantic" +version = "1.10.7" +description = "Data validation and settings management using python type hints" +category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" files = [ - {file = "pydot-1.4.2-py2.py3-none-any.whl", hash = "sha256:66c98190c65b8d2e2382a441b4c0edfdb4f4c025ef9cb9874de478fb0793a451"}, - {file = "pydot-1.4.2.tar.gz", hash = "sha256:248081a39bcb56784deb018977e428605c1c758f10897a339fce1dd728ff007d"}, + {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"}, + {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"}, + {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"}, + {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"}, + {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"}, + {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"}, + {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"}, + {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"}, + {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"}, + {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"}, ] [package.dependencies] -pyparsing = ">=2.1.4" +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] [[package]] name = "pyparsing" @@ -1107,7 +1050,9 @@ files = [ ] [package.dependencies] +APScheduler = {version = ">=3.10.1,<3.11.0", optional = true, markers = "extra == \"job-queue\""} httpx = ">=0.23.3,<0.24.0" +pytz = {version = ">=2018.6", optional = true, markers = "extra == \"job-queue\""} [package.extras] all = ["APScheduler (>=3.10.1,<3.11.0)", "aiolimiter (>=1.0.0,<1.1.0)", "cachetools (>=5.3.0,<5.4.0)", "cryptography (>=39.0.1)", "httpx[http2]", "httpx[socks]", "pytz (>=2018.6)", "tornado (>=6.2,<7.0)"] @@ -1120,41 +1065,6 @@ rate-limiter = ["aiolimiter (>=1.0.0,<1.1.0)"] socks = ["httpx[socks]"] webhooks = ["tornado (>=6.2,<7.0)"] -[[package]] -name = "pytype" -version = "2023.4.11" -description = "Python type inferencer" -category = "dev" -optional = false -python-versions = "<3.11,>=3.7" -files = [ - {file = "pytype-2023.4.11-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:2079e3d1618f8b790ad0533445dfc5389c06b39378603b29fb1abbf2ac2d532b"}, - {file = "pytype-2023.4.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb37bfb8fddf108359f89a6ae7560343561aa3e8cbc5d4445f6b766cd8971e2c"}, - {file = "pytype-2023.4.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cdb329e1bc9819cca1bf1e00b608c417ef1cec0bd2b1e62ea3834c13c3bd14a"}, - {file = "pytype-2023.4.11-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:0698c1663d103298f6112d1da1d2c4532a22f2c69f9dc4c0021ddc4f4b12401d"}, - {file = "pytype-2023.4.11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4afd2f4bcdd1118948d7696d9fea57d853797bf31898ee58c399680138ec38d3"}, - {file = "pytype-2023.4.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83aed054788d66d3a0cd05a0f34364a102f64eefcdd573d4119bee5122c245d6"}, - {file = "pytype-2023.4.11-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:7f053490736d2ac0c3d3f3e46e7b54354b4277387103d783467416d275a8ae9d"}, - {file = "pytype-2023.4.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44eeb17a8288c20ed04ffa9d09daf2e009997518e60da66f2b50a82459434185"}, - {file = "pytype-2023.4.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9209c187bf109f46dd57201656dcd7bd1a9cd6378e113658c0ac9b34f0ce5ed8"}, - {file = "pytype-2023.4.11-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:485f1dbbc579cf14028c666022fd00dfd0029ff153bec20c4cb72b2bf344335e"}, - {file = "pytype-2023.4.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22389ac370a594645757b69213a464a48defa6bd5d8469420c2960bae023f605"}, - {file = "pytype-2023.4.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cfe61030a7257a168bcde462f30c04ea5e858b17007a6d0497c6a4f8e75afe"}, - {file = "pytype-2023.4.11.tar.gz", hash = "sha256:90c9f5b83709b9f87253dae519eb1c0bfda72ca01461fc7023ab0f27d7e1296e"}, -] - -[package.dependencies] -attrs = ">=21.4.0" -importlab = ">=0.8" -jinja2 = ">=3.1.2" -libcst = ">=0.4.9" -networkx = "<2.8.4" -ninja = ">=1.10.0.post2" -pydot = ">=1.4.2" -tabulate = ">=0.8.10" -toml = ">=0.10.2" -typing-extensions = ">=4.3.0" - [[package]] name = "pytz" version = "2023.3" @@ -1168,55 +1078,20 @@ files = [ ] [[package]] -name = "pyyaml" -version = "6.0" -description = "YAML parser and emitter for Python" -category = "dev" +name = "pytz-deprecation-shim" +version = "0.1.0.post0" +description = "Shims to make deprecation of pytz easier" +category = "main" optional = false -python-versions = ">=3.6" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, + {file = "pytz_deprecation_shim-0.1.0.post0-py2.py3-none-any.whl", hash = "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6"}, + {file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"}, ] +[package.dependencies] +tzdata = {version = "*", markers = "python_version >= \"3.6\""} + [[package]] name = "rfc3986" version = "1.5.0" @@ -1301,6 +1176,23 @@ dev = ["click", "doit (>=0.36.0)", "flake8", "mypy", "pycodestyle", "pydevtool", doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +[[package]] +name = "setuptools" +version = "67.6.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, + {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "six" version = "1.16.0" @@ -1326,71 +1218,104 @@ files = [ ] [[package]] -name = "tabulate" -version = "0.9.0" -description = "Pretty-print tabular data" +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, - {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -[package.extras] -widechars = ["wcwidth"] - [[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" +name = "types-cachetools" +version = "5.3.0.5" +description = "Typing stubs for cachetools" category = "dev" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "*" files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, + {file = "types-cachetools-5.3.0.5.tar.gz", hash = "sha256:67fa46d51a650896770aee0ba80f0e61dc4a7d1373198eec1bc0622263eaa256"}, + {file = "types_cachetools-5.3.0.5-py3-none-any.whl", hash = "sha256:c0c5fa00199017d974c935bf043c467d5204e4f835141e489b48765b5ac1d960"}, ] [[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" +name = "types-docutils" +version = "0.19.1.7" +description = "Typing stubs for docutils" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = "*" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "types-docutils-0.19.1.7.tar.gz", hash = "sha256:cb6e0dd1c895c723002ec4dc8334bf9df04726ef0cf13f5597501578b47eef30"}, + {file = "types_docutils-0.19.1.7-py3-none-any.whl", hash = "sha256:bbf89cf3b9460ce780434f8cc52ce846b0f14505ff3496fc6a17acd9967aee9e"}, ] [[package]] -name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" +name = "types-openpyxl" +version = "3.1.0.4" +description = "Typing stubs for openpyxl" +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = "*" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "types-openpyxl-3.1.0.4.tar.gz", hash = "sha256:2c0c0ee1380208e458b3a6567bbd1c1796809b9ccec230845d4918f25e0cfd24"}, + {file = "types_openpyxl-3.1.0.4-py3-none-any.whl", hash = "sha256:9a42dea6a8c44f426e46f4a5c3537eed72f897db3638bbd0ff1a0418e3065913"}, ] [[package]] -name = "typing-inspect" -version = "0.8.0" -description = "Runtime inspection utilities for typing module." +name = "types-pygments" +version = "2.15.0.0" +description = "Typing stubs for Pygments" category = "dev" optional = false python-versions = "*" files = [ - {file = "typing_inspect-0.8.0-py3-none-any.whl", hash = "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188"}, - {file = "typing_inspect-0.8.0.tar.gz", hash = "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d"}, + {file = "types-Pygments-2.15.0.0.tar.gz", hash = "sha256:b5909267fe258dd00f3e65da8f098be30c0f7262805bef54e54eb69d75e2bc11"}, + {file = "types_Pygments-2.15.0.0-py3-none-any.whl", hash = "sha256:40b0cc6925cdbafbec46c5b2d994a1219e264e9d44ea6ab5c76cb8b87e8257e1"}, ] [package.dependencies] -mypy-extensions = ">=0.3.0" -typing-extensions = ">=3.7.4" +types-docutils = "*" +types-setuptools = "*" + +[[package]] +name = "types-pytz" +version = "2023.3.0.0" +description = "Typing stubs for pytz" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-pytz-2023.3.0.0.tar.gz", hash = "sha256:ecdc70d543aaf3616a7e48631543a884f74205f284cefd6649ddf44c6a820aac"}, + {file = "types_pytz-2023.3.0.0-py3-none-any.whl", hash = "sha256:4fc2a7fbbc315f0b6630e0b899fd6c743705abe1094d007b0e612d10da15e0f3"}, +] + +[[package]] +name = "types-setuptools" +version = "67.6.0.7" +description = "Typing stubs for setuptools" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-setuptools-67.6.0.7.tar.gz", hash = "sha256:f46b11773b1aeddbd2ef32fd6a6091ef33aa9b32daa124f6ce63f616de59ae51"}, + {file = "types_setuptools-67.6.0.7-py3-none-any.whl", hash = "sha256:ea2873dc8dd9e8421929dc50617ac7c2054c9a873942c5b5b606e2effef5db12"}, +] + +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] [[package]] name = "tzdata" @@ -1404,6 +1329,25 @@ files = [ {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, ] +[[package]] +name = "tzlocal" +version = "4.3" +description = "tzinfo object for the local timezone" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tzlocal-4.3-py3-none-any.whl", hash = "sha256:b44c4388f3d34f25862cfbb387578a4d70fec417649da694a132f628a23367e2"}, + {file = "tzlocal-4.3.tar.gz", hash = "sha256:3f21d09e1b2aa9f2dacca12da240ca37de3ba5237a93addfd6d593afe9073355"}, +] + +[package.dependencies] +pytz-deprecation-shim = "*" +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + [[package]] name = "uncertainties" version = "3.1.7" @@ -1463,4 +1407,4 @@ anyio = ">=3.0.0" [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.11" -content-hash = "24472122b86803712adcc56dc8e2cee99ddf221affbddb501f9a7c16c025a1d8" +content-hash = "c0d4b5336103b97443f41b0aac4ace631aa2cedabeeae14fa86da47babaa3b17" diff --git a/pyproject.toml b/pyproject.toml index eab0287..24cae48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dev = "watchfiles 'poetry run poe start' kipubot" test = "pytest" test_hot = "watchfiles 'pytest' kipubot tests" lint = "ruff ." -type = "pytype ." +type = "mypy ." format = [{ cmd = "black ." }, { cmd = "ruff . --fix" }] [tool.poetry.dependencies] @@ -25,19 +25,25 @@ pandas = "^2.0.0" matplotlib = "^3.7.1" scipy = "^1.10.1" python-dotenv = "^1.0.0" -python-telegram-bot = "^20.2" +python-telegram-bot = { extras = ["job-queue"], version = "^20.2" } psycopg = "^3.1.8" openpyxl = "^3.1.2" pytz = "^2023.3" uncertainties = "^3.1.7" +pydantic = "^1.10.7" +nptyping = "^2.5.0" [tool.poetry.group.dev.dependencies] watchfiles = "^0.19.0" ruff = "^0.0.261" -pytype = "^2023.4.11" pytest = "^7.3.1" black = "^23.3.0" poethepoet = "^0.19.0" +mypy = "^1.2.0" +pandas-stubs = "^2.0.0.230412" +types-pygments = "^2.15.0.0" +types-cachetools = "^5.3.0.5" +types-openpyxl = "^3.1.0.4" [build-system] requires = ["poetry-core"] @@ -160,3 +166,22 @@ max-complexity = 10 [tool.ruff.per-file-ignores] "tests/*" = ["S101"] + +[tool.mypy] +python_version = "3.10" +plugins = ["pydantic.mypy"] + +warn_redundant_casts = true +warn_unused_ignores = true +disallow_any_generics = true +check_untyped_defs = true +no_implicit_reexport = true + +# hopefully switch to strict in the future +# disallow_untyped_defs = true + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true +warn_untyped_fields = true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 18bcf8f..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[pytype] -inputs = kipubot \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py index 6da6149..6d606bf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,11 +1,9 @@ #!/usr/bin/env python3 -from datetime import datetime - import pytest +from pandas import Timestamp from pandas.testing import assert_frame_equal -from kipubot import DATABASE_URL from kipubot.db import _init_db, delete_chat, delete_raffle_data, save_chat_or_ignore from kipubot.utils import ( get_raffle, @@ -90,19 +88,19 @@ def test_remove_emojis(self): class TestGraphSave: @pytest.fixture(autouse=True) def create_chat(self): - _init_db(DATABASE_URL) + _init_db() save_chat_or_ignore(1, "testing", [1]) yield 1 delete_chat(1) def test_graph_save(self): file_path = "tests/example_data/example_1.xlsx" - start_date = datetime.fromisoformat("2022-08-01 03:15:00") - end_date = datetime.fromisoformat("2022-08-12 03:15:00") + start_date = Timestamp("2022-08-01 03:15:00") + end_date = Timestamp("2022-08-12 03:15:00") entry_fee = 1 df = read_excel_to_df(file_path, start_date, end_date) save_raffle(1, start_date, end_date, entry_fee, df) - raffle_from_db = get_raffle(1, include_df=True) + raffle_from_db = get_raffle(1) delete_raffle_data(1) assert start_date == raffle_from_db.start_date assert end_date == raffle_from_db.end_date From d5b9692a01022c5f8aa7059b092b44bd50dfa7e9 Mon Sep 17 00:00:00 2001 From: Mikael Siidorow Date: Mon, 17 Apr 2023 18:07:28 +0300 Subject: [PATCH 4/8] perf: use connection pool --- kipubot/bot.py | 5 ++- kipubot/db.py | 76 ++++++++++++++++++++++----------------------- poetry.lock | 17 +++++++++- pyproject.toml | 1 + tests/test_utils.py | 4 +-- 5 files changed, 58 insertions(+), 45 deletions(-) diff --git a/kipubot/bot.py b/kipubot/bot.py index 710747e..0d5b0b9 100644 --- a/kipubot/bot.py +++ b/kipubot/bot.py @@ -3,7 +3,7 @@ from telegram.ext import ApplicationBuilder, PicklePersistence from kipubot import config -from kipubot.db import _init_db +from kipubot.db import init_db from kipubot.handlers import ( bot_added_handler, error_handler, @@ -19,8 +19,7 @@ def main() -> None: # INITIALIZE DB AND CREATE TABLES IF THEY DON'T EXIST - _init_db() - + init_db() persistence = PicklePersistence(filepath="data/.pkl") app = ApplicationBuilder().token(config.BOT_TOKEN).persistence(persistence).build() diff --git a/kipubot/db.py b/kipubot/db.py index 58a14d2..7ec8d63 100644 --- a/kipubot/db.py +++ b/kipubot/db.py @@ -1,43 +1,37 @@ import logging -from collections.abc import Generator from contextlib import contextmanager, suppress -import psycopg import psycopg.errors as pserrors from pandas import DataFrame, Timestamp -from psycopg.rows import TupleRow +from psycopg_pool import ConnectionPool from kipubot import config from kipubot.errors import AlreadyRegisteredError -# LOGGER +_pool = ConnectionPool( + config.DATABASE_URL, +) + _logger = logging.getLogger(__name__) -# DB CONNECTION @contextmanager -def get_pg_conn() -> Generator[psycopg.Connection[TupleRow], None, None]: - """ - Get a connection to the postgres database. - """ - _logger.info("Connecting to DB...") - conn = psycopg.connect(config.DATABASE_URL) - _logger.info("Connected!") - +def logging_connection(): + """Log errors while connecting to the database""" try: - yield conn - except pserrors.Error: - _logger.exception("Unknown error during database operation!") - conn.rollback() + _logger.info("Getting db connection...") + with _pool.connection() as conn: + yield conn + except Exception: + _logger.exception("Error while getting connection!") else: - _logger.info("Committing changes...") - conn.commit() + _logger.info("Connection to database successful!") finally: - conn.close() + _logger.info("Freeing database connection...") -def _init_db() -> None: - with get_pg_conn() as conn: +def init_db(): + with logging_connection() as conn: conn.execute( """CREATE TABLE IF NOT EXISTS chat ( chat_id BIGINT PRIMARY KEY, @@ -76,7 +70,7 @@ def _init_db() -> None: def get_registered_member_ids(chat_id: int) -> list[int]: - with get_pg_conn() as conn: + with logging_connection() as conn: return [ row[0] for row in conn.execute( @@ -89,7 +83,7 @@ def get_registered_member_ids(chat_id: int) -> list[int]: def get_admin_ids(chat_id: int) -> list[int]: - with get_pg_conn() as conn: + with logging_connection() as conn: data = conn.execute( "SELECT admins FROM chat WHERE chat_id = %s", (chat_id,) ).fetchone() @@ -97,7 +91,7 @@ def get_admin_ids(chat_id: int) -> list[int]: def get_prev_winner_ids(chat_id: int) -> list[int]: - with get_pg_conn() as conn: + with logging_connection() as conn: data = conn.execute( "SELECT prev_winners FROM chat WHERE chat_id = %s", (chat_id,) ).fetchone() @@ -105,7 +99,7 @@ def get_prev_winner_ids(chat_id: int) -> list[int]: def get_winner_id(chat_id: int) -> int: - with get_pg_conn() as conn: + with logging_connection() as conn: data = conn.execute( "SELECT cur_winner FROM chat WHERE chat_id = %s", (chat_id,) ).fetchone() @@ -113,8 +107,8 @@ def get_winner_id(chat_id: int) -> int: def get_chats_where_winner(user_id: int) -> list[tuple[int, str]]: - with get_pg_conn() as conn: - return conn.execute( # type: ignore + with logging_connection() as conn: + return conn.execute( """SELECT c.chat_id, c.title FROM chat AS c, in_chat as i WHERE i.user_id = %(id)s @@ -127,8 +121,8 @@ def get_chats_where_winner(user_id: int) -> list[tuple[int, str]]: def get_raffle_data( chat_id: int, ) -> tuple[int, Timestamp, Timestamp, int, list[Timestamp], list[str], list[int]]: - with get_pg_conn() as conn: - return conn.execute( # type: ignore + with logging_connection() as conn: + return conn.execute( "SELECT * FROM raffle WHERE chat_id = %s", [chat_id] ).fetchone() @@ -144,7 +138,7 @@ def save_raffle_data( entries = df["name"].tolist() amounts = df["amount"].tolist() - with get_pg_conn() as conn: + with logging_connection() as conn: conn.execute( """INSERT INTO raffle VALUES (%s, %s, %s, %s, %s, %s, %s) @@ -163,13 +157,13 @@ def save_raffle_data( def delete_raffle_data(chat_id: int): - with get_pg_conn() as conn: + with logging_connection() as conn: conn.execute("""DELETE FROM raffle where chat_id=%s""", (chat_id,)) conn.commit() def save_user_or_ignore(user_id: int) -> None: - with get_pg_conn() as conn: + with logging_connection() as conn: conn.execute( """INSERT INTO chat_user VALUES (%s) @@ -180,7 +174,7 @@ def save_user_or_ignore(user_id: int) -> None: def save_chat_or_ignore(chat_id: int, title: str, admin_ids: list[int]) -> None: - with get_pg_conn() as conn: + with logging_connection() as conn: conn.execute( """INSERT INTO chat (chat_id, title, admins) VALUES (%s, %s, %s) @@ -191,7 +185,7 @@ def save_chat_or_ignore(chat_id: int, title: str, admin_ids: list[int]) -> None: def delete_chat(chat_id: int): - with get_pg_conn() as conn: + with logging_connection() as conn: conn.execute("""DELETE FROM chat where chat_id=%s""", (chat_id,)) conn.commit() @@ -199,7 +193,7 @@ def delete_chat(chat_id: int): def register_user(chat_id: int, user_id: int) -> None: save_user_or_ignore(user_id) - with get_pg_conn() as conn: + with logging_connection() as conn: try: conn.execute( """INSERT INTO in_chat(user_id, chat_id) @@ -219,7 +213,7 @@ def register_user_or_ignore(chat_id: int, user_id: int) -> None: def admin_cycle_winners(winner_id: int, chat_id: int) -> None: - with get_pg_conn() as conn: + with logging_connection() as conn: conn.execute( """UPDATE chat SET prev_winners = array_append(prev_winners, cur_winner), @@ -231,7 +225,7 @@ def admin_cycle_winners(winner_id: int, chat_id: int) -> None: def replace_cur_winner(winner_id: int, chat_id: int) -> None: - with get_pg_conn() as conn: + with logging_connection() as conn: conn.execute( """UPDATE chat SET cur_winner=%s @@ -241,7 +235,7 @@ def replace_cur_winner(winner_id: int, chat_id: int) -> None: def cycle_winners(user_id: int, winner_id: int, chat_id: int) -> None: - with get_pg_conn() as conn: + with logging_connection() as conn: conn.execute( """UPDATE chat SET prev_winners=array_append(prev_winners, %s), @@ -249,3 +243,7 @@ def cycle_winners(user_id: int, winner_id: int, chat_id: int) -> None: WHERE chat_id=%s""", (user_id, winner_id, chat_id), ) + + +if __name__ == "__main__": + init_db() diff --git a/poetry.lock b/poetry.lock index d44455c..6858aa5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -916,6 +916,21 @@ docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)" pool = ["psycopg-pool"] test = ["mypy (>=0.990)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-asyncio (>=0.17)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] +[[package]] +name = "psycopg-pool" +version = "3.1.7" +description = "Connection Pool for Psycopg" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "psycopg-pool-3.1.7.tar.gz", hash = "sha256:d02741dc48303495f4021900630442af87d6b1c3bfd1a3ece54cc11aa43d7dde"}, + {file = "psycopg_pool-3.1.7-py3-none-any.whl", hash = "sha256:ca1f2c366b5910acd400e16e812912827c57836af638c1717ba495111d22073b"}, +] + +[package.dependencies] +typing-extensions = ">=3.10" + [[package]] name = "pydantic" version = "1.10.7" @@ -1407,4 +1422,4 @@ anyio = ">=3.0.0" [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.11" -content-hash = "c0d4b5336103b97443f41b0aac4ace631aa2cedabeeae14fa86da47babaa3b17" +content-hash = "b369103fed65e044dc5fb20fb78c46cfd2c262f2da7b5c2258cff38ad9784f00" diff --git a/pyproject.toml b/pyproject.toml index 24cae48..8ac07de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ pytz = "^2023.3" uncertainties = "^3.1.7" pydantic = "^1.10.7" nptyping = "^2.5.0" +psycopg-pool = "^3.1.7" [tool.poetry.group.dev.dependencies] watchfiles = "^0.19.0" diff --git a/tests/test_utils.py b/tests/test_utils.py index 6d606bf..b4bc746 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,7 +4,7 @@ from pandas import Timestamp from pandas.testing import assert_frame_equal -from kipubot.db import _init_db, delete_chat, delete_raffle_data, save_chat_or_ignore +from kipubot.db import delete_chat, delete_raffle_data, init_db, save_chat_or_ignore from kipubot.utils import ( get_raffle, int_price_to_str, @@ -88,7 +88,7 @@ def test_remove_emojis(self): class TestGraphSave: @pytest.fixture(autouse=True) def create_chat(self): - _init_db() + init_db() save_chat_or_ignore(1, "testing", [1]) yield 1 delete_chat(1) From d60b46e8f66216c745cb765a2fe1f0bdebc1c6d1 Mon Sep 17 00:00:00 2001 From: Mikael Siidorow Date: Mon, 17 Apr 2023 18:37:02 +0300 Subject: [PATCH 5/8] docs: enforce required docstrings --- kipubot/__init__.py | 6 ++++++ kipubot/__main__.py | 1 + kipubot/bot.py | 3 +++ kipubot/constants.py | 7 ++++++- kipubot/db.py | 27 ++++++++++++++++++++++++++- kipubot/errors.py | 9 +++++++++ kipubot/handlers/__init__.py | 2 ++ kipubot/utils.py | 27 ++++++++++++++++++++++++++- pyproject.toml | 5 +++-- 9 files changed, 82 insertions(+), 5 deletions(-) diff --git a/kipubot/__init__.py b/kipubot/__init__.py index 6a9530d..ea50a04 100644 --- a/kipubot/__init__.py +++ b/kipubot/__init__.py @@ -1,3 +1,5 @@ +"""Kipubot - A Telegram bot for graphing friday raffles.""" + import logging import os @@ -11,11 +13,15 @@ # ENV CONFIG class Settings(BaseSettings): + """Configuration for the bot.""" + BOT_TOKEN: str DATABASE_URL: str DEVELOPER_CHAT_ID: str | None = None class Config: + """Environment variables to load from.""" + env_file = ".env" env_file_encoding = "utf-8" diff --git a/kipubot/__main__.py b/kipubot/__main__.py index 93ccc6f..9905d23 100644 --- a/kipubot/__main__.py +++ b/kipubot/__main__.py @@ -1,3 +1,4 @@ +"""Entrypoint for the kipubot package.""" from kipubot.bot import main main() diff --git a/kipubot/bot.py b/kipubot/bot.py index 0d5b0b9..1904d8d 100644 --- a/kipubot/bot.py +++ b/kipubot/bot.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +"""Main kipubot file for running the bot.""" + from telegram.ext import ApplicationBuilder, PicklePersistence from kipubot import config @@ -18,6 +20,7 @@ def main() -> None: + """Run the bot with all handlers.""" # INITIALIZE DB AND CREATE TABLES IF THEY DON'T EXIST init_db() persistence = PicklePersistence(filepath="data/.pkl") diff --git a/kipubot/constants.py b/kipubot/constants.py index 12ae209..af2d3df 100644 --- a/kipubot/constants.py +++ b/kipubot/constants.py @@ -1,4 +1,9 @@ -# CONSTANTS +"""Constants used in the bot. + +- EXCEL_MIME: The MIME type of the Excel file sent by MobilePay. +- STRINGS: A dictionary of strings used in the bot. +""" + EXCEL_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" STRINGS = { diff --git a/kipubot/db.py b/kipubot/db.py index 7ec8d63..a26ea06 100644 --- a/kipubot/db.py +++ b/kipubot/db.py @@ -1,3 +1,5 @@ +"""Database connection and queries for Kipubot.""" + import logging from contextlib import contextmanager, suppress @@ -17,7 +19,7 @@ @contextmanager def logging_connection(): - """Log errors while connecting to the database""" + """Log errors while connecting to the database.""" try: _logger.info("Getting db connection...") with _pool.connection() as conn: @@ -31,6 +33,7 @@ def logging_connection(): def init_db(): + """Initialize the database tables if they don't exist.""" with logging_connection() as conn: conn.execute( """CREATE TABLE IF NOT EXISTS chat ( @@ -70,6 +73,7 @@ def init_db(): def get_registered_member_ids(chat_id: int) -> list[int]: + """Get a list of all registered user IDs in a chat.""" with logging_connection() as conn: return [ row[0] @@ -83,6 +87,7 @@ def get_registered_member_ids(chat_id: int) -> list[int]: def get_admin_ids(chat_id: int) -> list[int]: + """Get a list of all admin IDs in a chat.""" with logging_connection() as conn: data = conn.execute( "SELECT admins FROM chat WHERE chat_id = %s", (chat_id,) @@ -91,6 +96,7 @@ def get_admin_ids(chat_id: int) -> list[int]: def get_prev_winner_ids(chat_id: int) -> list[int]: + """Get a list of all previous winner IDs in a chat.""" with logging_connection() as conn: data = conn.execute( "SELECT prev_winners FROM chat WHERE chat_id = %s", (chat_id,) @@ -99,6 +105,7 @@ def get_prev_winner_ids(chat_id: int) -> list[int]: def get_winner_id(chat_id: int) -> int: + """Get the current winner ID in a chat.""" with logging_connection() as conn: data = conn.execute( "SELECT cur_winner FROM chat WHERE chat_id = %s", (chat_id,) @@ -107,6 +114,7 @@ def get_winner_id(chat_id: int) -> int: def get_chats_where_winner(user_id: int) -> list[tuple[int, str]]: + """Get a list of all chats where a user is the current winner.""" with logging_connection() as conn: return conn.execute( """SELECT c.chat_id, c.title @@ -121,6 +129,7 @@ def get_chats_where_winner(user_id: int) -> list[tuple[int, str]]: def get_raffle_data( chat_id: int, ) -> tuple[int, Timestamp, Timestamp, int, list[Timestamp], list[str], list[int]]: + """Get the raffle data for a chat.""" with logging_connection() as conn: return conn.execute( "SELECT * FROM raffle WHERE chat_id = %s", [chat_id] @@ -134,6 +143,7 @@ def save_raffle_data( entry_fee: int, df: DataFrame, ) -> None: + """Save the raffle data for a chat.""" dates = df["date"].tolist() entries = df["name"].tolist() amounts = df["amount"].tolist() @@ -157,12 +167,14 @@ def save_raffle_data( def delete_raffle_data(chat_id: int): + """Delete the raffle data for a chat.""" with logging_connection() as conn: conn.execute("""DELETE FROM raffle where chat_id=%s""", (chat_id,)) conn.commit() def save_user_or_ignore(user_id: int) -> None: + """Save a user to the database, or ignore if already exists.""" with logging_connection() as conn: conn.execute( """INSERT INTO chat_user @@ -174,6 +186,7 @@ def save_user_or_ignore(user_id: int) -> None: def save_chat_or_ignore(chat_id: int, title: str, admin_ids: list[int]) -> None: + """Save a chat to the database, or ignore if already exists.""" with logging_connection() as conn: conn.execute( """INSERT INTO chat (chat_id, title, admins) @@ -185,12 +198,14 @@ def save_chat_or_ignore(chat_id: int, title: str, admin_ids: list[int]) -> None: def delete_chat(chat_id: int): + """Delete a chat from the database.""" with logging_connection() as conn: conn.execute("""DELETE FROM chat where chat_id=%s""", (chat_id,)) conn.commit() def register_user(chat_id: int, user_id: int) -> None: + """Register a user in a chat.""" save_user_or_ignore(user_id) with logging_connection() as conn: @@ -208,11 +223,16 @@ def register_user(chat_id: int, user_id: int) -> None: def register_user_or_ignore(chat_id: int, user_id: int) -> None: + """Register a user in a chat, or ignore if already registered.""" with suppress(AlreadyRegisteredError): register_user(chat_id, user_id) def admin_cycle_winners(winner_id: int, chat_id: int) -> None: + """Admin set current winner to a specific user. + + Move the previous winner to the list of previous winners. + """ with logging_connection() as conn: conn.execute( """UPDATE chat @@ -225,6 +245,7 @@ def admin_cycle_winners(winner_id: int, chat_id: int) -> None: def replace_cur_winner(winner_id: int, chat_id: int) -> None: + """Replace the current winner in a chat.""" with logging_connection() as conn: conn.execute( """UPDATE chat @@ -235,6 +256,10 @@ def replace_cur_winner(winner_id: int, chat_id: int) -> None: def cycle_winners(user_id: int, winner_id: int, chat_id: int) -> None: + """Set the current winner in a chat. + + Move the previous winner to the list of previous winners. + """ with logging_connection() as conn: conn.execute( """UPDATE chat diff --git a/kipubot/errors.py b/kipubot/errors.py index 9ffa16f..1cb3993 100644 --- a/kipubot/errors.py +++ b/kipubot/errors.py @@ -1,10 +1,19 @@ +"""Custom exceptions for Kipubot.""" + + class NoEntriesError(Exception): + """Raised when there are no entries in a raffle.""" + pass class NoRaffleError(Exception): + """Raised when there is no raffle in a chat.""" + pass class AlreadyRegisteredError(Exception): + """Raised when a user tries to register twice.""" + pass diff --git a/kipubot/handlers/__init__.py b/kipubot/handlers/__init__.py index 435b0dc..e65c1bf 100644 --- a/kipubot/handlers/__init__.py +++ b/kipubot/handlers/__init__.py @@ -1,3 +1,5 @@ +"""Handlers for Kipubot.""" + __all__ = ( "start_handler", "moro_handler", diff --git a/kipubot/utils.py b/kipubot/utils.py index 40072a8..72fb91e 100644 --- a/kipubot/utils.py +++ b/kipubot/utils.py @@ -1,3 +1,5 @@ +"""Utility functions for Kipubot.""" + import os import re from typing import Any, NamedTuple @@ -20,12 +22,16 @@ class RaffleStatsData(NamedTuple): + """Data for raffle stats without entry data.""" + start_date: pd.Timestamp end_date: pd.Timestamp entry_fee: int class RaffleData(NamedTuple): + """Data for raffle stats with entry data.""" + start_date: pd.Timestamp end_date: pd.Timestamp entry_fee: int @@ -33,6 +39,7 @@ class RaffleData(NamedTuple): def is_int(x: str) -> bool: + """Safely check if a string is an integer.""" try: int(x) except ValueError: @@ -42,6 +49,7 @@ def is_int(x: str) -> bool: def is_float(x: str) -> bool: + """Safely check if a string is a float.""" try: float(x) except ValueError: @@ -51,6 +59,7 @@ def is_float(x: str) -> bool: def int_price_to_str(num: int) -> str: + """Format an integer price to a string.""" float_num = num / 100.0 str_num: str = ( @@ -69,6 +78,7 @@ def int_price_to_str(num: int) -> str: async def get_chat_member_opt(chat: Chat, member_id: int) -> ChatMember | None: + """Get a chat member, or None if the user is not in the chat.""" try: return await chat.get_member(member_id) except BadRequest as e: @@ -78,6 +88,7 @@ async def get_chat_member_opt(chat: Chat, member_id: int) -> ChatMember | None: def preband(x, xd, yd, p, func): + """Calculate the prediction band for a curve fit.""" conf = 0.95 alpha = 1.0 - conf quantile = stats.t.ppf(1.0 - alpha / 2.0, xd.size - len(p)) @@ -95,6 +106,7 @@ def preband(x, xd, yd, p, func): def fit_timedata(x_series: "pd.Series[Any]", y_series: "pd.Series[Any]"): + """Fit a curve to the data.""" # ignore the end date in curve fitting x = x_series.values[:-1] y = y_series.values[:-1] @@ -126,6 +138,7 @@ def f(x, slope, intercept): def remove_emojis(text: str) -> str: + """Remove emojis from a string.""" emojis = re.compile( pattern="[" "\U0001F600-\U0001F64F" # emoticons @@ -139,6 +152,7 @@ def remove_emojis(text: str) -> str: def validate_excel(excel_path: str) -> bool: + """Validate that the submitted excel file is in the correct (MP) format.""" df = pd.read_excel( excel_path, usecols="A,B,D", @@ -157,6 +171,7 @@ def validate_excel(excel_path: str) -> bool: def read_excel_to_df( excel_path: str, start_date: pd.Timestamp, end_date: pd.Timestamp ) -> pd.DataFrame: + """Read the excel file to a dataframe.""" df = pd.read_excel( excel_path, usecols="A,B,D", @@ -172,18 +187,20 @@ def read_excel_to_df( def get_raffle_stats(chat_id: int) -> RaffleStatsData: + """Get the stats of a raffle, not including dataframe.""" query_result = db.get_raffle_data(chat_id) if query_result is None: error_text = f"No raffle found for chat {chat_id}" raise NoRaffleError(error_text) - _, start_date, end_date, entry_fee, dates, entries, amounts = query_result + _, start_date, end_date, entry_fee, _, _, _ = query_result return RaffleStatsData(start_date, end_date, entry_fee) def get_raffle(chat_id: int) -> RaffleData: + """Get the data of a raffle, including dataframe.""" query_result = db.get_raffle_data(chat_id) if query_result is None: @@ -198,6 +215,7 @@ def get_raffle(chat_id: int) -> RaffleData: def get_cur_time_hel() -> pd.Timestamp: + """Get the current time in Helsinki as a Timestamp.""" # take current time in helsinki and convert it to naive time, # as mobilepay times are naive (naive = no timezone specified). helsinki_tz = pytz.timezone("Europe/Helsinki") @@ -213,10 +231,12 @@ def save_raffle( entry_fee: int, df: pd.DataFrame, ) -> None: + """Save a raffle to the database.""" db.save_raffle_data(chat_id, start_date, end_date, entry_fee, df) def parse_df_essentials(raffle_data: RaffleData) -> RaffleData: + """Parse the essentials of a raffle dataframe.""" start_date, end_date, fee, df = raffle_data df.at[start_date, "amount"] = 0 @@ -230,6 +250,7 @@ def parse_df_essentials(raffle_data: RaffleData) -> RaffleData: def parse_expected(raffle_data: RaffleData) -> RaffleData: + """Parse the expected values of a raffle dataframe.""" start_date, end_date, entry_fee, df = parse_df_essentials(raffle_data) df["win_odds"] = 1.0 / df["unique"] @@ -247,6 +268,7 @@ def parse_expected(raffle_data: RaffleData) -> RaffleData: def parse_graph(raffle_data: RaffleData) -> RaffleData: + """Parse the graph values of a raffle dataframe.""" df = raffle_data.df df.at[get_cur_time_hel(), "amount"] = 0 @@ -258,6 +280,7 @@ def parse_graph(raffle_data: RaffleData) -> RaffleData: def configure_and_save_plot(out_img_path: str) -> None: + """Configure and save the plot to an image file.""" ax = plt.gca() # toggle legend @@ -280,6 +303,7 @@ def configure_and_save_plot(out_img_path: str) -> None: def generate_graph(out_img_path: str, chat_id: int, chat_title: str) -> None: + """Generate a graph of raffle progress.""" # -- get raffle data -- raffle_data = get_raffle(chat_id) @@ -320,6 +344,7 @@ def generate_graph(out_img_path: str, chat_id: int, chat_title: str) -> None: def generate_expected(out_img_path: str, chat_id: int, chat_title: str) -> None: + """Generate a graph of expected values.""" # -- get raffle data -- raffle_data = get_raffle(chat_id) diff --git a/pyproject.toml b/pyproject.toml index 8ac07de..cabb86b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ select = [ "C90", "DTZ", "E", + "D", "EM", "F", "FBT", @@ -81,7 +82,7 @@ select = [ "W", "YTT", ] -ignore = [] +ignore = ["D203", "D213"] fixable = [ "A", @@ -166,7 +167,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" max-complexity = 10 [tool.ruff.per-file-ignores] -"tests/*" = ["S101"] +"tests/*" = ["S101", "D"] [tool.mypy] python_version = "3.10" From 2d1c9ac4b7a8945a0d30abb165963c31dbc0f085 Mon Sep 17 00:00:00 2001 From: Mikael Siidorow Date: Mon, 17 Apr 2023 22:08:15 +0300 Subject: [PATCH 6/8] feat!: store historical raffles with new active column --- kipubot/bot.py | 2 + kipubot/constants.py | 1 + kipubot/db.py | 115 ++++++++++++++++++---- kipubot/handlers/__init__.py | 2 + kipubot/handlers/_close_handler.py | 33 +++++++ kipubot/handlers/_raffle_setup_handler.py | 16 +-- kipubot/handlers/_winner_handler.py | 3 + kipubot/utils.py | 30 ++++-- tests/test_utils.py | 15 ++- 9 files changed, 177 insertions(+), 40 deletions(-) create mode 100644 kipubot/handlers/_close_handler.py diff --git a/kipubot/bot.py b/kipubot/bot.py index 1904d8d..3d05f48 100644 --- a/kipubot/bot.py +++ b/kipubot/bot.py @@ -8,6 +8,7 @@ from kipubot.db import init_db from kipubot.handlers import ( bot_added_handler, + close_handler, error_handler, excel_file_handler, graph_handler, @@ -35,6 +36,7 @@ def main() -> None: app.add_handler(moro_handler) app.add_handler(graph_handler) app.add_handler(winner_handler) + app.add_handler(close_handler) # warning about using a command in a private chat app.add_handler(no_dm_handler) diff --git a/kipubot/constants.py b/kipubot/constants.py index af2d3df..6cfb291 100644 --- a/kipubot/constants.py +++ b/kipubot/constants.py @@ -59,4 +59,5 @@ "user_not_found": ("Error getting user!\nPerhaps they haven't /moro ed? 🤔"), "already_winner": "You are already the winner!", "winner_confirmation": "%(username)s is the new winner!", + "close_confirmation": "Raffle closed by @%(username)s! ✔️", } diff --git a/kipubot/db.py b/kipubot/db.py index a26ea06..ea4b0ef 100644 --- a/kipubot/db.py +++ b/kipubot/db.py @@ -4,11 +4,12 @@ from contextlib import contextmanager, suppress import psycopg.errors as pserrors -from pandas import DataFrame, Timestamp +from pandas import Timestamp from psycopg_pool import ConnectionPool from kipubot import config from kipubot.errors import AlreadyRegisteredError +from kipubot.utils import RaffleData _pool = ConnectionPool( config.DATABASE_URL, @@ -61,16 +62,29 @@ def init_db(): conn.execute( """CREATE TABLE IF NOT EXISTS raffle ( - chat_id BIGINT PRIMARY KEY REFERENCES chat(chat_id), + raffle_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), start_date TIMESTAMP, end_date TIMESTAMP, + active BOOLEAN DEFAULT TRUE, entry_fee INTEGER, dates TIMESTAMP[], entries VARCHAR(128)[], - amounts INTEGER[] + amounts INTEGER[], + chat_id BIGINT REFERENCES chat(chat_id), + user_id BIGINT REFERENCES chat_user(user_id) + )""" ) + conn.execute( + """CREATE INDEX IF NOT EXISTS raffle_chat_id_idx ON raffle (chat_id)""" + ) + + conn.execute( + """CREATE UNIQUE INDEX IF NOT EXISTS raffle_chat_id_active + ON raffle (chat_id) WHERE active = TRUE""" + ) + def get_registered_member_ids(chat_id: int) -> list[int]: """Get a list of all registered user IDs in a chat.""" @@ -128,22 +142,39 @@ def get_chats_where_winner(user_id: int) -> list[tuple[int, str]]: def get_raffle_data( chat_id: int, -) -> tuple[int, Timestamp, Timestamp, int, list[Timestamp], list[str], list[int]]: - """Get the raffle data for a chat.""" +) -> tuple[ + str, + Timestamp, + Timestamp, + bool, + int, + list[Timestamp], + list[str], + list[int], + int, + int, +]: + """Get the raffle data for a chat. + + Returns + ------- + Tuple of the raffle_id, chat_id, start date, end date, + entry fee, dates, entries, and amounts. + """ with logging_connection() as conn: return conn.execute( - "SELECT * FROM raffle WHERE chat_id = %s", [chat_id] + """SELECT * FROM raffle WHERE chat_id = %s AND active = true""", + [chat_id], ).fetchone() def save_raffle_data( chat_id: int, - start_date: Timestamp, - end_date: Timestamp, - entry_fee: int, - df: DataFrame, + user_id: int, + raffle_data: RaffleData, ) -> None: """Save the raffle data for a chat.""" + start_date, end_date, entry_fee, df = raffle_data dates = df["date"].tolist() entries = df["name"].tolist() amounts = df["amount"].tolist() @@ -151,16 +182,60 @@ def save_raffle_data( with logging_connection() as conn: conn.execute( """INSERT INTO raffle - VALUES (%s, %s, %s, %s, %s, %s, %s) - ON CONFLICT (chat_id) - DO UPDATE SET - start_date = EXCLUDED.start_date, - end_date = EXCLUDED.end_date, - entry_fee = EXCLUDED.entry_fee, - dates = EXCLUDED.dates, - entries = EXCLUDED.entries, - amounts = EXCLUDED.amounts""", - (chat_id, start_date, end_date, entry_fee, dates, entries, amounts), + (start_date, end_date, entry_fee, + dates, entries, amounts, chat_id, user_id) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s)""", + ( + start_date, + end_date, + entry_fee, + dates, + entries, + amounts, + chat_id, + user_id, + ), + ) + + conn.commit() + + +def update_raffle_data( + raffle_id: str, + raffle_data: RaffleData, +) -> None: + """Update the raffle data for a chat.""" + start_date, end_date, entry_fee, df = raffle_data + dates = df["date"].tolist() + entries = df["name"].tolist() + amounts = df["amount"].tolist() + + with logging_connection() as conn: + conn.execute( + """UPDATE raffle + SET start_date = %s, + end_date = %s, + entry_fee = %s, + dates = %s, + entries = %s, + amounts = %s + WHERE raffle_id = %s""", + (start_date, end_date, entry_fee, dates, entries, amounts, raffle_id), + ) + + conn.commit() + + +def close_raffle( + chat_id: int, +) -> None: + """Close a raffle.""" + with logging_connection() as conn: + conn.execute( + """UPDATE raffle + SET active = false + WHERE chat_id = %s AND active = true""", + (chat_id,), ) conn.commit() diff --git a/kipubot/handlers/__init__.py b/kipubot/handlers/__init__.py index e65c1bf..744340b 100644 --- a/kipubot/handlers/__init__.py +++ b/kipubot/handlers/__init__.py @@ -10,9 +10,11 @@ "raffle_setup_handler", "no_dm_handler", "error_handler", + "close_handler", ) from ._bot_added_handler import bot_added_handler +from ._close_handler import close_handler from ._error_handler import error_handler from ._excel_file_handler import excel_file_handler from ._graph_handler import graph_handler diff --git a/kipubot/handlers/_close_handler.py b/kipubot/handlers/_close_handler.py new file mode 100644 index 0000000..9c7c8eb --- /dev/null +++ b/kipubot/handlers/_close_handler.py @@ -0,0 +1,33 @@ +from telegram import Update +from telegram.ext import CommandHandler, ContextTypes, filters + +from kipubot.constants import STRINGS +from kipubot.db import ( + close_raffle, + get_admin_ids, + get_winner_id, +) + + +async def close(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: + if not update.effective_chat or not update.effective_user or not update.message: + return None + + chat_id = update.effective_chat.id + user_id = update.effective_user.id + + is_admin = user_id in get_admin_ids(chat_id) + is_cur_winner = user_id == get_winner_id(chat_id) + + if not is_admin and not is_cur_winner: + await update.message.reply_text(STRINGS["forbidden_command"]) + return + + close_raffle(chat_id) + + await update.message.reply_text( + STRINGS["close_confirmation"] % {"username": update.effective_user.username} + ) + + +close_handler = CommandHandler(["sulje", "close"], close, ~filters.ChatType.PRIVATE) diff --git a/kipubot/handlers/_raffle_setup_handler.py b/kipubot/handlers/_raffle_setup_handler.py index a9b5627..3737528 100644 --- a/kipubot/handlers/_raffle_setup_handler.py +++ b/kipubot/handlers/_raffle_setup_handler.py @@ -8,6 +8,7 @@ from kipubot.constants import STRINGS from kipubot.errors import NoRaffleError from kipubot.utils import ( + RaffleData, get_cur_time_hel, get_raffle_stats, int_price_to_str, @@ -15,6 +16,7 @@ is_int, read_excel_to_df, save_raffle, + update_raffle, ) # ================== @@ -68,11 +70,6 @@ async def convo_timeout(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i def raffle_keyboard(*, has_existing: bool = False) -> InlineKeyboardMarkup: if has_existing: keyboard = [ - [ - InlineKeyboardButton( - STRINGS["new_raffle_button"], callback_data="raffle:setup:new" - ) - ], [ InlineKeyboardButton( STRINGS["update_raffle_button"], callback_data="raffle:setup:old" @@ -370,10 +367,11 @@ async def finish_setup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> in chat_id = context.user_data["raffle_chat_id"] dm_id = update.effective_chat.id - start_date, end_date, entry_fee = get_raffle_stats(chat_id) + raffle_id, (start_date, end_date, entry_fee) = get_raffle_stats(chat_id) excel_path = f"data/{dm_id}/data.xlsx" df = read_excel_to_df(excel_path, start_date, end_date) - save_raffle(chat_id, start_date, end_date, entry_fee, df) + raffle_data = RaffleData(start_date, end_date, entry_fee, df) + update_raffle(raffle_id, raffle_data) await query.message.edit_text( STRINGS["updated_raffle"] % {"chat_title": chat_title} @@ -392,13 +390,15 @@ async def finish_setup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> in if query.data == "raffle:fee:confirmed": chat_title = context.user_data["raffle_chat_title"] chat_id = context.user_data["raffle_chat_id"] + user_id = update.effective_user.id start_date = context.user_data["raffle_start_date"] end_date = context.user_data["raffle_end_date"] fee = context.user_data["raffle_fee"] excel_path = f"data/{dm_id}/data.xlsx" df = read_excel_to_df(excel_path, start_date, end_date) - save_raffle(chat_id, start_date, end_date, fee, df) + raffle_data = RaffleData(start_date, end_date, fee, df) + save_raffle(chat_id, user_id, raffle_data) msg = ( STRINGS["raffle_setup_base"] diff --git a/kipubot/handlers/_winner_handler.py b/kipubot/handlers/_winner_handler.py index 9684260..0f662e2 100644 --- a/kipubot/handlers/_winner_handler.py +++ b/kipubot/handlers/_winner_handler.py @@ -8,6 +8,7 @@ from kipubot.constants import STRINGS from kipubot.db import ( admin_cycle_winners, + close_raffle, cycle_winners, get_admin_ids, get_prev_winner_ids, @@ -88,6 +89,8 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: # winner: moves themselves to prev and makes new current else: cycle_winners(user_id, winner_id, chat_id) + + close_raffle(chat_id) except pserrors.Error: _logger.exception("SQLite Error:") await update.message.reply_text(STRINGS["user_not_found"]) diff --git a/kipubot/utils.py b/kipubot/utils.py index 72fb91e..c5f0062 100644 --- a/kipubot/utils.py +++ b/kipubot/utils.py @@ -18,7 +18,7 @@ from telegram.error import BadRequest from kipubot import db -from kipubot.errors import NoRaffleError +from kipubot.errors import NoEntriesError, NoRaffleError class RaffleStatsData(NamedTuple): @@ -186,7 +186,7 @@ def read_excel_to_df( return df -def get_raffle_stats(chat_id: int) -> RaffleStatsData: +def get_raffle_stats(chat_id: int) -> tuple[str, RaffleStatsData]: """Get the stats of a raffle, not including dataframe.""" query_result = db.get_raffle_data(chat_id) @@ -194,9 +194,9 @@ def get_raffle_stats(chat_id: int) -> RaffleStatsData: error_text = f"No raffle found for chat {chat_id}" raise NoRaffleError(error_text) - _, start_date, end_date, entry_fee, _, _, _ = query_result + raffle_id, start_date, end_date, _, entry_fee, _, _, _, _, _ = query_result - return RaffleStatsData(start_date, end_date, entry_fee) + return (raffle_id, RaffleStatsData(start_date, end_date, entry_fee)) def get_raffle(chat_id: int) -> RaffleData: @@ -207,7 +207,11 @@ def get_raffle(chat_id: int) -> RaffleData: error_text = f"No raffle found for chat {chat_id}" raise NoRaffleError(error_text) - _, start_date, end_date, entry_fee, dates, entries, amounts = query_result + _, start_date, end_date, _, entry_fee, dates, entries, amounts, _, _ = query_result + + if len(dates) == 0 or len(entries) == 0 or len(amounts) == 0: + error_text = f"No entries found for chat {chat_id}" + raise NoEntriesError(error_text) df = pd.DataFrame(data={"date": dates, "name": entries, "amount": amounts}) df.set_index("date", inplace=True) @@ -226,13 +230,19 @@ def get_cur_time_hel() -> pd.Timestamp: def save_raffle( chat_id: int, - start_date: pd.Timestamp, - end_date: pd.Timestamp, - entry_fee: int, - df: pd.DataFrame, + user_id: int, + raffle_data: RaffleData, ) -> None: """Save a raffle to the database.""" - db.save_raffle_data(chat_id, start_date, end_date, entry_fee, df) + db.save_raffle_data(chat_id, user_id, raffle_data) + + +def update_raffle( + raffle_id: str, + raffle_data: RaffleData, +) -> None: + """Update a raffle in the database.""" + db.update_raffle_data(raffle_id, raffle_data) def parse_df_essentials(raffle_data: RaffleData) -> RaffleData: diff --git a/tests/test_utils.py b/tests/test_utils.py index b4bc746..57d9edc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,8 +4,16 @@ from pandas import Timestamp from pandas.testing import assert_frame_equal -from kipubot.db import delete_chat, delete_raffle_data, init_db, save_chat_or_ignore +from kipubot.db import ( + delete_chat, + delete_raffle_data, + init_db, + register_user, + save_chat_or_ignore, + save_user_or_ignore, +) from kipubot.utils import ( + RaffleData, get_raffle, int_price_to_str, read_excel_to_df, @@ -90,6 +98,8 @@ class TestGraphSave: def create_chat(self): init_db() save_chat_or_ignore(1, "testing", [1]) + save_user_or_ignore(1) + register_user(1, 1) yield 1 delete_chat(1) @@ -99,7 +109,8 @@ def test_graph_save(self): end_date = Timestamp("2022-08-12 03:15:00") entry_fee = 1 df = read_excel_to_df(file_path, start_date, end_date) - save_raffle(1, start_date, end_date, entry_fee, df) + raffle_data = RaffleData(start_date, end_date, entry_fee, df) + save_raffle(1, 1, raffle_data) raffle_from_db = get_raffle(1) delete_raffle_data(1) assert start_date == raffle_from_db.start_date From b19950af76a5a3d711894282898597abf3ff2ab8 Mon Sep 17 00:00:00 2001 From: Mikael Siidorow Date: Mon, 17 Apr 2023 22:46:08 +0300 Subject: [PATCH 7/8] fix: circular import breaking tests --- kipubot/db.py | 2 +- kipubot/handlers/_raffle_setup_handler.py | 2 +- kipubot/types.py | 22 +++++++++++++++++ kipubot/utils.py | 30 ++++++----------------- tests/test_utils.py | 2 +- 5 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 kipubot/types.py diff --git a/kipubot/db.py b/kipubot/db.py index ea4b0ef..295a5ca 100644 --- a/kipubot/db.py +++ b/kipubot/db.py @@ -9,7 +9,7 @@ from kipubot import config from kipubot.errors import AlreadyRegisteredError -from kipubot.utils import RaffleData +from kipubot.types import RaffleData _pool = ConnectionPool( config.DATABASE_URL, diff --git a/kipubot/handlers/_raffle_setup_handler.py b/kipubot/handlers/_raffle_setup_handler.py index 3737528..6bcb21f 100644 --- a/kipubot/handlers/_raffle_setup_handler.py +++ b/kipubot/handlers/_raffle_setup_handler.py @@ -7,8 +7,8 @@ from kipubot.constants import STRINGS from kipubot.errors import NoRaffleError +from kipubot.types import RaffleData from kipubot.utils import ( - RaffleData, get_cur_time_hel, get_raffle_stats, int_price_to_str, diff --git a/kipubot/types.py b/kipubot/types.py new file mode 100644 index 0000000..135056b --- /dev/null +++ b/kipubot/types.py @@ -0,0 +1,22 @@ +"""Type definitions and classes for Kipubot.""" + +from typing import NamedTuple + +from pandas import DataFrame, Timestamp + + +class RaffleStatsData(NamedTuple): + """Data for raffle stats without entry data.""" + + start_date: Timestamp + end_date: Timestamp + entry_fee: int + + +class RaffleData(NamedTuple): + """Data for raffle stats with entry data.""" + + start_date: Timestamp + end_date: Timestamp + entry_fee: int + df: DataFrame diff --git a/kipubot/utils.py b/kipubot/utils.py index c5f0062..1a32e1d 100644 --- a/kipubot/utils.py +++ b/kipubot/utils.py @@ -2,7 +2,7 @@ import os import re -from typing import Any, NamedTuple +from typing import Any import matplotlib.dates as mdates # type: ignore import matplotlib.pyplot as plt # type: ignore @@ -17,25 +17,9 @@ from telegram import Chat, ChatMember from telegram.error import BadRequest -from kipubot import db +from kipubot.db import get_raffle_data, save_raffle_data, update_raffle_data from kipubot.errors import NoEntriesError, NoRaffleError - - -class RaffleStatsData(NamedTuple): - """Data for raffle stats without entry data.""" - - start_date: pd.Timestamp - end_date: pd.Timestamp - entry_fee: int - - -class RaffleData(NamedTuple): - """Data for raffle stats with entry data.""" - - start_date: pd.Timestamp - end_date: pd.Timestamp - entry_fee: int - df: pd.DataFrame +from kipubot.types import RaffleData, RaffleStatsData def is_int(x: str) -> bool: @@ -188,7 +172,7 @@ def read_excel_to_df( def get_raffle_stats(chat_id: int) -> tuple[str, RaffleStatsData]: """Get the stats of a raffle, not including dataframe.""" - query_result = db.get_raffle_data(chat_id) + query_result = get_raffle_data(chat_id) if query_result is None: error_text = f"No raffle found for chat {chat_id}" @@ -201,7 +185,7 @@ def get_raffle_stats(chat_id: int) -> tuple[str, RaffleStatsData]: def get_raffle(chat_id: int) -> RaffleData: """Get the data of a raffle, including dataframe.""" - query_result = db.get_raffle_data(chat_id) + query_result = get_raffle_data(chat_id) if query_result is None: error_text = f"No raffle found for chat {chat_id}" @@ -234,7 +218,7 @@ def save_raffle( raffle_data: RaffleData, ) -> None: """Save a raffle to the database.""" - db.save_raffle_data(chat_id, user_id, raffle_data) + save_raffle_data(chat_id, user_id, raffle_data) def update_raffle( @@ -242,7 +226,7 @@ def update_raffle( raffle_data: RaffleData, ) -> None: """Update a raffle in the database.""" - db.update_raffle_data(raffle_id, raffle_data) + update_raffle_data(raffle_id, raffle_data) def parse_df_essentials(raffle_data: RaffleData) -> RaffleData: diff --git a/tests/test_utils.py b/tests/test_utils.py index 57d9edc..e6c7325 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -12,8 +12,8 @@ save_chat_or_ignore, save_user_or_ignore, ) +from kipubot.types import RaffleData from kipubot.utils import ( - RaffleData, get_raffle, int_price_to_str, read_excel_to_df, From c0f6171446bed623c1f77ebc0c84e0b7ededd57f Mon Sep 17 00:00:00 2001 From: Mikael Siidorow Date: Tue, 18 Apr 2023 21:00:59 +0300 Subject: [PATCH 8/8] style: enable more ruff rules, and fix warnings --- kipubot/__init__.py | 11 +- kipubot/bot.py | 2 - kipubot/db.py | 34 ++--- kipubot/errors.py | 6 - kipubot/handlers/_bot_added_handler.py | 14 ++- kipubot/handlers/_close_handler.py | 4 +- kipubot/handlers/_error_handler.py | 7 +- kipubot/handlers/_excel_file_handler.py | 15 +-- kipubot/handlers/_graph_handler.py | 33 +++-- kipubot/handlers/_moro_handler.py | 6 +- kipubot/handlers/_no_dm_handler.py | 2 +- kipubot/handlers/_raffle_setup_handler.py | 67 +++++----- kipubot/handlers/_start_handler.py | 6 +- kipubot/handlers/_winner_handler.py | 10 +- kipubot/utils.py | 143 +++++++++++++--------- pyproject.toml | 36 ++++-- tests/test_utils.py | 13 +- 17 files changed, 235 insertions(+), 174 deletions(-) diff --git a/kipubot/__init__.py b/kipubot/__init__.py index ea50a04..66277d9 100644 --- a/kipubot/__init__.py +++ b/kipubot/__init__.py @@ -1,13 +1,14 @@ """Kipubot - A Telegram bot for graphing friday raffles.""" import logging -import os +from pathlib import Path from pydantic import BaseSettings # LOGGING CONFIG logging.basicConfig( - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + level=logging.INFO, ) @@ -29,9 +30,7 @@ class Config: config = Settings() -# CHECK/CREATE DATA DIRECTORY -if not os.path.exists("data"): - logging.info("Creating ./data/ directory...") - os.mkdir("data") +logging.info("Creating ./data/ directory... ") +Path("data").mkdir(exist_ok=True) __all__ = ("config",) diff --git a/kipubot/bot.py b/kipubot/bot.py index 3d05f48..24d5483 100644 --- a/kipubot/bot.py +++ b/kipubot/bot.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """Main kipubot file for running the bot.""" from telegram.ext import ApplicationBuilder, PicklePersistence diff --git a/kipubot/db.py b/kipubot/db.py index 295a5ca..09285e6 100644 --- a/kipubot/db.py +++ b/kipubot/db.py @@ -1,10 +1,13 @@ """Database connection and queries for Kipubot.""" import logging +from collections.abc import Generator from contextlib import contextmanager, suppress +from typing import Any import psycopg.errors as pserrors from pandas import Timestamp +from psycopg import Connection from psycopg_pool import ConnectionPool from kipubot import config @@ -19,7 +22,7 @@ @contextmanager -def logging_connection(): +def logging_connection() -> Generator[Connection[Any], None, None]: """Log errors while connecting to the database.""" try: _logger.info("Getting db connection...") @@ -33,7 +36,7 @@ def logging_connection(): _logger.info("Freeing database connection...") -def init_db(): +def init_db() -> None: """Initialize the database tables if they don't exist.""" with logging_connection() as conn: conn.execute( @@ -43,13 +46,13 @@ def init_db(): admins BIGINT[], prev_winners BIGINT[], cur_winner BIGINT - )""" + )""", ) conn.execute( """CREATE TABLE IF NOT EXISTS chat_user ( user_id BIGINT PRIMARY KEY - )""" + )""", ) conn.execute( @@ -57,7 +60,7 @@ def init_db(): user_id BIGINT REFERENCES chat_user(user_id), chat_id BIGINT REFERENCES chat(chat_id), PRIMARY KEY (user_id, chat_id) - )""" + )""", ) conn.execute( @@ -73,16 +76,16 @@ def init_db(): chat_id BIGINT REFERENCES chat(chat_id), user_id BIGINT REFERENCES chat_user(user_id) - )""" + )""", ) conn.execute( - """CREATE INDEX IF NOT EXISTS raffle_chat_id_idx ON raffle (chat_id)""" + """CREATE INDEX IF NOT EXISTS raffle_chat_id_idx ON raffle (chat_id)""", ) conn.execute( """CREATE UNIQUE INDEX IF NOT EXISTS raffle_chat_id_active - ON raffle (chat_id) WHERE active = TRUE""" + ON raffle (chat_id) WHERE active = TRUE""", ) @@ -104,7 +107,8 @@ def get_admin_ids(chat_id: int) -> list[int]: """Get a list of all admin IDs in a chat.""" with logging_connection() as conn: data = conn.execute( - "SELECT admins FROM chat WHERE chat_id = %s", (chat_id,) + "SELECT admins FROM chat WHERE chat_id = %s", + (chat_id,), ).fetchone() return data[0] if data else [] @@ -113,7 +117,8 @@ def get_prev_winner_ids(chat_id: int) -> list[int]: """Get a list of all previous winner IDs in a chat.""" with logging_connection() as conn: data = conn.execute( - "SELECT prev_winners FROM chat WHERE chat_id = %s", (chat_id,) + "SELECT prev_winners FROM chat WHERE chat_id = %s", + (chat_id,), ).fetchone() return data[0] if data else [] @@ -122,7 +127,8 @@ def get_winner_id(chat_id: int) -> int: """Get the current winner ID in a chat.""" with logging_connection() as conn: data = conn.execute( - "SELECT cur_winner FROM chat WHERE chat_id = %s", (chat_id,) + "SELECT cur_winner FROM chat WHERE chat_id = %s", + (chat_id,), ).fetchone() return data[0] if data else None @@ -162,7 +168,7 @@ def get_raffle_data( entry fee, dates, entries, and amounts. """ with logging_connection() as conn: - return conn.execute( + return conn.execute( # type: ignore """SELECT * FROM raffle WHERE chat_id = %s AND active = true""", [chat_id], ).fetchone() @@ -241,7 +247,7 @@ def close_raffle( conn.commit() -def delete_raffle_data(chat_id: int): +def delete_raffle_data(chat_id: int) -> None: """Delete the raffle data for a chat.""" with logging_connection() as conn: conn.execute("""DELETE FROM raffle where chat_id=%s""", (chat_id,)) @@ -272,7 +278,7 @@ def save_chat_or_ignore(chat_id: int, title: str, admin_ids: list[int]) -> None: ) -def delete_chat(chat_id: int): +def delete_chat(chat_id: int) -> None: """Delete a chat from the database.""" with logging_connection() as conn: conn.execute("""DELETE FROM chat where chat_id=%s""", (chat_id,)) diff --git a/kipubot/errors.py b/kipubot/errors.py index 1cb3993..84b6750 100644 --- a/kipubot/errors.py +++ b/kipubot/errors.py @@ -4,16 +4,10 @@ class NoEntriesError(Exception): """Raised when there are no entries in a raffle.""" - pass - class NoRaffleError(Exception): """Raised when there is no raffle in a chat.""" - pass - class AlreadyRegisteredError(Exception): """Raised when a user tries to register twice.""" - - pass diff --git a/kipubot/handlers/_bot_added_handler.py b/kipubot/handlers/_bot_added_handler.py index 4c4c333..f67c20f 100644 --- a/kipubot/handlers/_bot_added_handler.py +++ b/kipubot/handlers/_bot_added_handler.py @@ -1,3 +1,5 @@ +import logging + import psycopg.errors as pserrors from telegram import Update from telegram.constants import ChatMemberStatus @@ -6,6 +8,8 @@ from kipubot.constants import STRINGS from kipubot.db import register_user_or_ignore, save_chat_or_ignore, save_user_or_ignore +_logger = logging.getLogger(__name__) + async def bot_added(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if ( @@ -36,10 +40,11 @@ async def bot_added(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: save_user_or_ignore(user_id) register_user_or_ignore(chat_id, user_id) - except pserrors.IntegrityError as e: - print("SQLite Error: " + str(e)) + except pserrors.IntegrityError: + _logger.exception("IntegrityError") await context.bot.send_message( - chat_id=chat_id, text=STRINGS["unknown_error"] + chat_id=chat_id, + text=STRINGS["unknown_error"], ) else: # Kiitos pääsystä! -stigu @@ -48,7 +53,8 @@ async def bot_added(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: sticker="CAACAgQAAxkBAAIBPmLicTHP2Xv8IcFzxHYocjLRFBvQAAI5AAMcLHsXd9jLHwYNcSEpBA", ) await context.bot.send_message( - chat_id=chat_id, text=STRINGS["moro_prompt"] % {"chat_title": title} + chat_id=chat_id, + text=STRINGS["moro_prompt"] % {"chat_title": title}, ) diff --git a/kipubot/handlers/_close_handler.py b/kipubot/handlers/_close_handler.py index 9c7c8eb..33a0807 100644 --- a/kipubot/handlers/_close_handler.py +++ b/kipubot/handlers/_close_handler.py @@ -11,7 +11,7 @@ async def close(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_chat or not update.effective_user or not update.message: - return None + return chat_id = update.effective_chat.id user_id = update.effective_user.id @@ -26,7 +26,7 @@ async def close(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: close_raffle(chat_id) await update.message.reply_text( - STRINGS["close_confirmation"] % {"username": update.effective_user.username} + STRINGS["close_confirmation"] % {"username": update.effective_user.username}, ) diff --git a/kipubot/handlers/_error_handler.py b/kipubot/handlers/_error_handler.py index c87984c..ee22bfe 100644 --- a/kipubot/handlers/_error_handler.py +++ b/kipubot/handlers/_error_handler.py @@ -47,11 +47,14 @@ async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> N # Finally, send the message if DEVELOPER_CHAT_ID is set. if config.DEVELOPER_CHAT_ID: await context.bot.send_message( - chat_id=config.DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML + chat_id=config.DEVELOPER_CHAT_ID, + text=message, + parse_mode=ParseMode.HTML, ) # Also send a message to the user who triggered the error. if isinstance(update, Update) and update.effective_chat is not None: await context.bot.send_message( - chat_id=update.effective_chat.id, text=STRINGS["server_error"] + chat_id=update.effective_chat.id, + text=STRINGS["server_error"], ) diff --git a/kipubot/handlers/_excel_file_handler.py b/kipubot/handlers/_excel_file_handler.py index bf44205..d3c79c3 100644 --- a/kipubot/handlers/_excel_file_handler.py +++ b/kipubot/handlers/_excel_file_handler.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ContextTypes, ConversationHandler, MessageHandler, filters @@ -31,14 +31,13 @@ async def excel_file(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: base_path = f"data/{dm_id}" excel_path = base_path + "/data.xlsx" - if not os.path.exists(base_path): - os.mkdir(base_path) + Path(base_path).mkdir(parents=True, exist_ok=True) await file.download_to_drive(excel_path) if not validate_excel(excel_path): await update.message.reply_text(STRINGS["invalid_file"]) - os.remove(excel_path) + Path(excel_path).unlink() return ConversationHandler.END chat_buttons = [] @@ -48,7 +47,7 @@ async def excel_file(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: InlineKeyboardButton( STRINGS["chat_button"] % {"chat_title": chat_title}, callback_data=f"raffle:chat_selected:{chat_id}:{chat_title}", - ) + ), ) keyboard = [ @@ -59,12 +58,14 @@ async def excel_file(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text( - STRINGS["choose_channel"], reply_markup=reply_markup + STRINGS["choose_channel"], + reply_markup=reply_markup, ) return ConversationHandler.END excel_file_handler = MessageHandler( - filters.Document.MimeType(EXCEL_MIME) & filters.ChatType.PRIVATE, excel_file + filters.Document.MimeType(EXCEL_MIME) & filters.ChatType.PRIVATE, + excel_file, ) diff --git a/kipubot/handlers/_graph_handler.py b/kipubot/handlers/_graph_handler.py index afecdcd..d6c52c3 100644 --- a/kipubot/handlers/_graph_handler.py +++ b/kipubot/handlers/_graph_handler.py @@ -1,3 +1,6 @@ +import logging +from pathlib import Path + import psycopg.errors as pserrors from telegram import Update from telegram.ext import CommandHandler, ContextTypes, filters @@ -6,12 +9,14 @@ from kipubot.errors import NoEntriesError, NoRaffleError from kipubot.utils import generate_expected, generate_graph -GRAPH_TYPE = dict( - expected="expected", - odotusarvo="expected", - graph="graph", - kuvaaja="graph", -) +GRAPH_TYPE = { + "expected": "expected", + "odotusarvo": "expected", + "graph": "graph", + "kuvaaja": "graph", +} + +_logger = logging.getLogger(__name__) def get_graph_img(graph_type: str) -> str: @@ -31,7 +36,7 @@ async def graph( or not update.message or not update.message.text ): - return None + return chat_id = update.effective_chat.id chat_title = ( @@ -47,24 +52,26 @@ async def graph( else: generate_graph(graph_path, chat_id, chat_title) - with open(graph_path, "rb") as f: + with Path(graph_path).open("rb") as f: await update.message.reply_photo(photo=f) except NoRaffleError: await update.message.reply_text( - STRINGS["no_raffle"] % {"chat_title": chat_title} + STRINGS["no_raffle"] % {"chat_title": chat_title}, ) except NoEntriesError: await update.message.reply_text( - STRINGS["no_entries"] % {"chat_title": chat_title} + STRINGS["no_entries"] % {"chat_title": chat_title}, ) - except pserrors.Error as e: - print(e) + except pserrors.Error: + _logger.exception("psycopg error") await update.message.reply_text(STRINGS["raffle_db_error"]) except FileNotFoundError: await update.message.reply_text(STRINGS["no_data"] % {"chat_title": chat_title}) graph_handler = CommandHandler( - ["kuvaaja", "graph", "odotusarvo", "expected"], graph, ~filters.ChatType.PRIVATE + ["kuvaaja", "graph", "odotusarvo", "expected"], + graph, + ~filters.ChatType.PRIVATE, ) diff --git a/kipubot/handlers/_moro_handler.py b/kipubot/handlers/_moro_handler.py index 985b608..8d4c93d 100644 --- a/kipubot/handlers/_moro_handler.py +++ b/kipubot/handlers/_moro_handler.py @@ -8,7 +8,7 @@ async def hello(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_chat or not update.effective_user or not update.message: - return None + return chat_id = update.effective_chat.id user_id = update.effective_user.id @@ -20,11 +20,11 @@ async def hello(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: except AlreadyRegisteredError: await update.message.reply_text( - STRINGS["double_moro"] % {"username": username, "chat_title": chat} + STRINGS["double_moro"] % {"username": username, "chat_title": chat}, ) else: await update.message.reply_text( - STRINGS["moro"] % {"username": username, "chat_title": chat} + STRINGS["moro"] % {"username": username, "chat_title": chat}, ) diff --git a/kipubot/handlers/_no_dm_handler.py b/kipubot/handlers/_no_dm_handler.py index bd9d408..9d42bd9 100644 --- a/kipubot/handlers/_no_dm_handler.py +++ b/kipubot/handlers/_no_dm_handler.py @@ -6,7 +6,7 @@ async def chat_only(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: if not update.message: - return None + return await update.message.reply_text(STRINGS["no_dm_warn"]) diff --git a/kipubot/handlers/_raffle_setup_handler.py b/kipubot/handlers/_raffle_setup_handler.py index 6bcb21f..1cc84c5 100644 --- a/kipubot/handlers/_raffle_setup_handler.py +++ b/kipubot/handlers/_raffle_setup_handler.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path from typing import Literal import pandas as pd @@ -72,13 +72,15 @@ def raffle_keyboard(*, has_existing: bool = False) -> InlineKeyboardMarkup: keyboard = [ [ InlineKeyboardButton( - STRINGS["update_raffle_button"], callback_data="raffle:setup:old" - ) + STRINGS["update_raffle_button"], + callback_data="raffle:setup:old", + ), ], [ InlineKeyboardButton( - STRINGS["cancel_button"], callback_data="raffle:cancel" - ) + STRINGS["cancel_button"], + callback_data="raffle:cancel", + ), ], ] return InlineKeyboardMarkup(keyboard) @@ -86,8 +88,9 @@ def raffle_keyboard(*, has_existing: bool = False) -> InlineKeyboardMarkup: keyboard = [ [ InlineKeyboardButton( - STRINGS["new_raffle_button"], callback_data="raffle:setup:new" - ) + STRINGS["new_raffle_button"], + callback_data="raffle:setup:new", + ), ], [InlineKeyboardButton(STRINGS["cancel_button"], callback_data="raffle:cancel")], ] @@ -108,10 +111,12 @@ def date_keyboard(which: Literal["start", "end"]) -> InlineKeyboardMarkup: InlineKeyboardButton("-1 h", callback_data=f"raffle:date:{which}:update:-1"), InlineKeyboardButton("-30 m", callback_data=f"raffle:date:{which}:update:-0.5"), InlineKeyboardButton( - "-15 m", callback_data=f"raffle:date:{which}:update:-0.25" + "-15 m", + callback_data=f"raffle:date:{which}:update:-0.25", ), InlineKeyboardButton( - "+15 m", callback_data=f"raffle:date:{which}:update:+0.25" + "+15 m", + callback_data=f"raffle:date:{which}:update:+0.25", ), InlineKeyboardButton("+30 m", callback_data=f"raffle:date:{which}:update:+0.5"), InlineKeyboardButton("+1 h", callback_data=f"raffle:date:{which}:update:+1"), @@ -124,7 +129,7 @@ def date_keyboard(which: Literal["start", "end"]) -> InlineKeyboardMarkup: InlineKeyboardButton( STRINGS["confirm_button"], callback_data=f"raffle:date:{which}:confirmed", - ) + ), ], [InlineKeyboardButton(STRINGS["cancel_button"], callback_data="raffle:cancel")], ] @@ -142,8 +147,9 @@ def fee_keyboard() -> InlineKeyboardMarkup: ], [ InlineKeyboardButton( - STRINGS["finish_raffle_button"], callback_data="raffle:fee:confirmed" - ) + STRINGS["finish_raffle_button"], + callback_data="raffle:fee:confirmed", + ), ], [InlineKeyboardButton(STRINGS["cancel_button"], callback_data="raffle:cancel")], ] @@ -188,12 +194,13 @@ async def setup_raffle(update: Update, context: ContextTypes.DEFAULT_TYPE) -> st ) % {"chat_title": chat_title} await query.message.edit_text( - msg, reply_markup=raffle_keyboard(has_existing=True) + msg, + reply_markup=raffle_keyboard(has_existing=True), ) except NoRaffleError: msg = (STRINGS["raffle_setup_base"] + STRINGS["raffle_setup_new"]) % { - "chat_title": chat_title + "chat_title": chat_title, } await query.message.edit_text(msg, reply_markup=raffle_keyboard()) @@ -205,7 +212,8 @@ async def setup_raffle(update: Update, context: ContextTypes.DEFAULT_TYPE) -> st async def setup_start_date( - update: Update, context: ContextTypes.DEFAULT_TYPE + update: Update, + context: ContextTypes.DEFAULT_TYPE, ) -> str | int: query = update.callback_query if not query or not query.message or not query.data or not context.user_data: @@ -226,7 +234,7 @@ async def setup_start_date( context.user_data["raffle_start_date"] = new_date if query.data == "raffle:setup:new" or query.data.startswith( - "raffle:date:start:update" + "raffle:date:start:update", ): chat_title = context.user_data["raffle_chat_title"] start_date = context.user_data["raffle_start_date"] @@ -244,7 +252,8 @@ async def setup_start_date( async def setup_end_date( - update: Update, context: ContextTypes.DEFAULT_TYPE + update: Update, + context: ContextTypes.DEFAULT_TYPE, ) -> str | int: query = update.callback_query if not query or not query.message or not query.data or not context.user_data: @@ -265,7 +274,7 @@ async def setup_end_date( context.user_data["raffle_end_date"] = new_date if query.data == "raffle:date:start:confirmed" or query.data.startswith( - "raffle:date:end:update" + "raffle:date:end:update", ): chat_title = context.user_data["raffle_chat_title"] start_date = context.user_data["raffle_start_date"] @@ -316,7 +325,7 @@ async def setup_fee(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str | context.user_data["raffle_fee"] = new_fee if query.data == "raffle:date:end:confirmed" or query.data.startswith( - "raffle:fee:update" + "raffle:fee:update", ): chat_title = context.user_data["raffle_chat_title"] start_date = context.user_data["raffle_start_date"] @@ -369,12 +378,12 @@ async def finish_setup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> in raffle_id, (start_date, end_date, entry_fee) = get_raffle_stats(chat_id) excel_path = f"data/{dm_id}/data.xlsx" - df = read_excel_to_df(excel_path, start_date, end_date) - raffle_data = RaffleData(start_date, end_date, entry_fee, df) + raffle_df = read_excel_to_df(excel_path, start_date, end_date) + raffle_data = RaffleData(start_date, end_date, entry_fee, raffle_df) update_raffle(raffle_id, raffle_data) await query.message.edit_text( - STRINGS["updated_raffle"] % {"chat_title": chat_title} + STRINGS["updated_raffle"] % {"chat_title": chat_title}, ) await context.bot.send_message( chat_id, @@ -383,7 +392,7 @@ async def finish_setup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> in ) # perform cleanup context.user_data.clear() - os.remove(excel_path) + Path(excel_path).unlink(missing_ok=True) return ConversationHandler.END @@ -396,8 +405,8 @@ async def finish_setup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> in fee = context.user_data["raffle_fee"] excel_path = f"data/{dm_id}/data.xlsx" - df = read_excel_to_df(excel_path, start_date, end_date) - raffle_data = RaffleData(start_date, end_date, fee, df) + raffle_df = read_excel_to_df(excel_path, start_date, end_date) + raffle_data = RaffleData(start_date, end_date, fee, raffle_df) save_raffle(chat_id, user_id, raffle_data) msg = ( @@ -423,7 +432,7 @@ async def finish_setup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> in # perform cleanup context.user_data.clear() - os.remove(excel_path) + Path(excel_path).unlink(missing_ok=True) return ConversationHandler.END @@ -439,10 +448,12 @@ async def finish_setup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> in ], "raffle_setup_state:start_date": [ CallbackQueryHandler( - setup_start_date, pattern="^raffle:date:start:update.*$" + setup_start_date, + pattern="^raffle:date:start:update.*$", ), CallbackQueryHandler( - setup_end_date, pattern="^raffle:date:start:confirmed$" + setup_end_date, + pattern="^raffle:date:start:confirmed$", ), ], "raffle_setup_state:end_date": [ diff --git a/kipubot/handlers/_start_handler.py b/kipubot/handlers/_start_handler.py index 145ecb5..569ea03 100644 --- a/kipubot/handlers/_start_handler.py +++ b/kipubot/handlers/_start_handler.py @@ -1,3 +1,5 @@ +from pathlib import Path + from telegram import Update from telegram.ext import CommandHandler, ContextTypes @@ -6,9 +8,9 @@ async def start(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: if not update.message: - return None + return - with open("kipubot/resources/info.png", "rb") as f: + with Path("kipubot/resources/info.png").open("rb") as f: await update.message.reply_photo(caption=STRINGS["start_prompt"], photo=f) diff --git a/kipubot/handlers/_winner_handler.py b/kipubot/handlers/_winner_handler.py index 0f662e2..ad948c4 100644 --- a/kipubot/handlers/_winner_handler.py +++ b/kipubot/handlers/_winner_handler.py @@ -32,7 +32,7 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: or not update.message.entities or not update.message.text ): - return None + return # only usable by admin, previous winner (in case of typos) and current winner # usage: /winner @username @@ -44,7 +44,7 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: if len(ent) != TWO_ENTITIES or ent[1].type != MessageEntityType.MENTION: await update.message.reply_text(STRINGS["invalid_winner_usage"]) - return None + return username = update.message.text.split(" ")[1][1:] @@ -97,10 +97,12 @@ async def winner(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: return await update.message.reply_text( - STRINGS["winner_confirmation"] % {"username": username} + STRINGS["winner_confirmation"] % {"username": username}, ) winner_handler = CommandHandler( - ["voittaja", "winner"], winner, ~filters.ChatType.PRIVATE + ["voittaja", "winner"], + winner, + ~filters.ChatType.PRIVATE, ) diff --git a/kipubot/utils.py b/kipubot/utils.py index 1a32e1d..ac0f44c 100644 --- a/kipubot/utils.py +++ b/kipubot/utils.py @@ -1,7 +1,7 @@ """Utility functions for Kipubot.""" -import os import re +from pathlib import Path from typing import Any import matplotlib.dates as mdates # type: ignore @@ -71,7 +71,7 @@ async def get_chat_member_opt(chat: Chat, member_id: int) -> ChatMember | None: raise -def preband(x, xd, yd, p, func): +def preband(x, xd, yd, p, func): # noqa: ANN201, ANN001 """Calculate the prediction band for a curve fit.""" conf = 0.95 alpha = 1.0 - conf @@ -89,13 +89,16 @@ def preband(x, xd, yd, p, func): return lpb, upb -def fit_timedata(x_series: "pd.Series[Any]", y_series: "pd.Series[Any]"): +def fit_timedata( # noqa: ANN201 + x_series: "pd.Series[Any]", + y_series: "pd.Series[Any]", +): """Fit a curve to the data.""" # ignore the end date in curve fitting - x = x_series.values[:-1] - y = y_series.values[:-1] + x = x_series.to_numpy()[:-1] + y = y_series.to_numpy()[:-1] - def f(x, slope, intercept): + def f(x, slope, intercept): # noqa: ANN001, ANN202 return slope * x + intercept popt, pcov = curve_fit(f, x, y) @@ -137,7 +140,7 @@ def remove_emojis(text: str) -> str: def validate_excel(excel_path: str) -> bool: """Validate that the submitted excel file is in the correct (MP) format.""" - df = pd.read_excel( + mp_excel_df = pd.read_excel( excel_path, usecols="A,B,D", header=None, @@ -145,29 +148,35 @@ def validate_excel(excel_path: str) -> bool: parse_dates=True, ) return ( - df.size > 0 - and df["date"].dtype == "datetime64[ns]" - and df["name"].dtype == "object" - and df["amount"].dtype in ("int64", "float64") + mp_excel_df.size > 0 + and mp_excel_df["date"].dtype == "datetime64[ns]" + and mp_excel_df["name"].dtype == "object" + and mp_excel_df["amount"].dtype in ("int64", "float64") ) def read_excel_to_df( - excel_path: str, start_date: pd.Timestamp, end_date: pd.Timestamp + excel_path: str, + start_date: pd.Timestamp, + end_date: pd.Timestamp, ) -> pd.DataFrame: """Read the excel file to a dataframe.""" - df = pd.read_excel( + mobilepay_df = pd.read_excel( excel_path, usecols="A,B,D", header=None, names=["date", "name", "amount"], parse_dates=True, ) - df.drop(df[df["amount"] <= 0].index, inplace=True) - df.drop(df[df["date"] > end_date].index, inplace=True) - df.drop(df[df["date"] < start_date].index, inplace=True) - df["amount"] = df["amount"] * 100 - return df + mobilepay_df = mobilepay_df.drop(mobilepay_df[mobilepay_df["amount"] <= 0].index) + mobilepay_df = mobilepay_df.drop( + mobilepay_df[mobilepay_df["date"] > end_date].index, + ) + mobilepay_df = mobilepay_df.drop( + mobilepay_df[mobilepay_df["date"] < start_date].index, + ) + mobilepay_df["amount"] = mobilepay_df["amount"] * 100 + return mobilepay_df def get_raffle_stats(chat_id: int) -> tuple[str, RaffleStatsData]: @@ -197,19 +206,23 @@ def get_raffle(chat_id: int) -> RaffleData: error_text = f"No entries found for chat {chat_id}" raise NoEntriesError(error_text) - df = pd.DataFrame(data={"date": dates, "name": entries, "amount": amounts}) - df.set_index("date", inplace=True) - return RaffleData(start_date, end_date, entry_fee, df) + mp_dataframe = pd.DataFrame( + data={"date": dates, "name": entries, "amount": amounts}, + ) + mp_dataframe = mp_dataframe.set_index("date") + + return RaffleData(start_date, end_date, entry_fee, mp_dataframe) def get_cur_time_hel() -> pd.Timestamp: - """Get the current time in Helsinki as a Timestamp.""" - # take current time in helsinki and convert it to naive time, - # as mobilepay times are naive (naive = no timezone specified). + """Get the current time in Helsinki as a Timestamp. + + Take current time in helsinki and convert it to naive time, + as mobilepay times are naive (naive = no timezone specified). + """ helsinki_tz = pytz.timezone("Europe/Helsinki") - cur_time_hel = pd.Timestamp.utcnow().astimezone(helsinki_tz).replace(tzinfo=None) - return cur_time_hel + return pd.Timestamp.utcnow().astimezone(helsinki_tz).replace(tzinfo=None) def save_raffle( @@ -231,46 +244,46 @@ def update_raffle( def parse_df_essentials(raffle_data: RaffleData) -> RaffleData: """Parse the essentials of a raffle dataframe.""" - start_date, end_date, fee, df = raffle_data + start_date, end_date, fee, mp_dataframe = raffle_data - df.at[start_date, "amount"] = 0 + mp_dataframe.loc[start_date, "amount"] = 0 - df["datenum"] = pd.to_numeric(df.index.values) - df = df.sort_values("datenum") - df["amount"] = df["amount"].cumsum().astype(int) - df["unique"] = (~df["name"].duplicated()).cumsum() - 1 + mp_dataframe["datenum"] = pd.to_numeric(mp_dataframe.index.values) + mp_dataframe = mp_dataframe.sort_values("datenum") + mp_dataframe["amount"] = mp_dataframe["amount"].cumsum().astype(int) + mp_dataframe["unique"] = (~mp_dataframe["name"].duplicated()).cumsum() - 1 - return RaffleData(start_date, end_date, fee, df) + return RaffleData(start_date, end_date, fee, mp_dataframe) def parse_expected(raffle_data: RaffleData) -> RaffleData: """Parse the expected values of a raffle dataframe.""" - start_date, end_date, entry_fee, df = parse_df_essentials(raffle_data) + start_date, end_date, entry_fee, mp_dataframe = parse_df_essentials(raffle_data) - df["win_odds"] = 1.0 / df["unique"] - df["next_expected"] = ( + mp_dataframe["win_odds"] = 1.0 / mp_dataframe["unique"] + mp_dataframe["next_expected"] = ( ( - -entry_fee * (1 - df["win_odds"]) - + (df["amount"] - entry_fee) * df["win_odds"] + -entry_fee * (1 - mp_dataframe["win_odds"]) + + (mp_dataframe["amount"] - entry_fee) * mp_dataframe["win_odds"] ) .fillna(0) .round() .astype(int) ) - return RaffleData(start_date, end_date, entry_fee, df) + return RaffleData(start_date, end_date, entry_fee, mp_dataframe) def parse_graph(raffle_data: RaffleData) -> RaffleData: """Parse the graph values of a raffle dataframe.""" - df = raffle_data.df - - df.at[get_cur_time_hel(), "amount"] = 0 - df.at[raffle_data.end_date, "amount"] = 0 + mp_dataframe = raffle_data.df - parsed_raffle_data = parse_df_essentials(raffle_data._replace(df=df)) + mp_dataframe.loc[get_cur_time_hel(), "amount"] = 0 + mp_dataframe.loc[raffle_data.end_date, "amount"] = 0 - return parsed_raffle_data + return parse_df_essentials( + raffle_data._replace(df=mp_dataframe), + ) def configure_and_save_plot(out_img_path: str) -> None: @@ -289,8 +302,7 @@ def configure_and_save_plot(out_img_path: str) -> None: ax.yaxis.set_minor_locator(AutoMinorLocator(2)) plt.grid(visible=True, which="major", axis="both", linestyle="--", linewidth=0.5) - if not os.path.exists(os.path.dirname(out_img_path)): - os.makedirs(os.path.dirname(out_img_path)) + Path(out_img_path).parent.mkdir(parents=True, exist_ok=True) plt.savefig(out_img_path) plt.clf() @@ -302,15 +314,18 @@ def generate_graph(out_img_path: str, chat_id: int, chat_title: str) -> None: raffle_data = get_raffle(chat_id) # -- parse and fit data -- - start_date, end_date, _, df = parse_graph(raffle_data) - px, nom, std, lpb, upb = fit_timedata(df["datenum"], df["amount"]) + start_date, end_date, _, mp_dataframe = parse_graph(raffle_data) + px, nom, std, lpb, upb = fit_timedata( + mp_dataframe["datenum"], + mp_dataframe["amount"], + ) # -- plot -- # clear previous plot in case of leftovers plt.clf() ax = plt.gca() # plot data - df["amount"][:-1].plot(ax=ax, marker="o", style="r", label="Pool") + mp_dataframe["amount"][:-1].plot(ax=ax, marker="o", style="r", label="Pool") # plot regression ax.plot(px, nom, "-", color="black", label="y=ax+b") # uncertainty lines (95% conf) @@ -322,14 +337,15 @@ def generate_graph(out_img_path: str, chat_id: int, chat_title: str) -> None: # -- style graph -- pred_max_pool = (nom + 1.96 * std)[-1] - pool_total = df["amount"].max() + pool_total = mp_dataframe["amount"].max() plt.ylim(0, max(pred_max_pool, pool_total)) plt.xlim((pd.to_datetime(start_date), pd.to_datetime(end_date))) plt.title( str(remove_emojis(chat_title).strip()) + "\n" - + f"Entries {df['unique'].max()} | Pool {int_price_to_str(pool_total)} €" + + f"Entries {mp_dataframe['unique'].max()}" + + f" | Pool {int_price_to_str(pool_total)} €", ) plt.xlabel(None) plt.ylabel("Pool (€)") @@ -343,26 +359,35 @@ def generate_expected(out_img_path: str, chat_id: int, chat_title: str) -> None: raffle_data = get_raffle(chat_id) # -- parse and fit data -- - start_date, _, entry_fee, df = parse_expected(raffle_data) + start_date, _, entry_fee, mp_dataframe = parse_expected(raffle_data) # -- plot -- # clear previous plot in case of leftovers plt.clf() ax = plt.gca() - df["next_expected"].plot(ax=ax, marker="o", style="r", label="Expected Value") + mp_dataframe["next_expected"].plot( + ax=ax, + marker="o", + style="r", + label="Expected Value", + ) # -- style graph -- plt.ylim( - float(int_price_to_str((df["next_expected"].min() - 100) * 110)), - float(int_price_to_str((df["next_expected"].max() + 100) * 110)), + float(int_price_to_str((mp_dataframe["next_expected"].min() - 100) * 110)), + float(int_price_to_str((mp_dataframe["next_expected"].max() + 100) * 110)), ) plt.xlim((pd.to_datetime(start_date), pd.to_datetime(get_cur_time_hel()))) plt.title( - str(remove_emojis(chat_title).strip()) - + f" | Fee {int_price_to_str(entry_fee)} €\n" - + f"Expected Value { int_price_to_str(df['next_expected'].iloc[-1])} €" + ( + f"{remove_emojis(chat_title).strip()}" + " | " + f"Fee {int_price_to_str(entry_fee)} €\n" + "Expected Value " + f"{int_price_to_str(mp_dataframe['next_expected'].iloc[-1])} €" + ), ) plt.xlabel(None) plt.ylabel("Expected Value (€)") diff --git a/pyproject.toml b/pyproject.toml index cabb86b..2e34d9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ authors = [ license = "GPL-3.0" readme = "README.md" - [tool.poe.tasks] start = "python3 -m kipubot" dev = "watchfiles 'poetry run poe start' kipubot" @@ -53,7 +52,6 @@ build-backend = "poetry.core.masonry.api" [tool.ruff] line-length = 88 target-version = "py310" - select = [ "A", "B", @@ -80,14 +78,33 @@ select = [ "TID", "UP", "W", + "SLF", "YTT", + "NPY", + "ERA", + "PD", + "PTH", + "ARG", + "TCH", + "RET", + "RSE", + "PT", + "T20", + "PIE", + "INP", + "G", + "EXE", + "T10", + "C4", + "COM", + "BLE", + "ANN", ] ignore = ["D203", "D213"] - fixable = [ "A", "B", - "C90", # C + "C90", "D", "E", "F", @@ -109,6 +126,7 @@ fixable = [ "FBT", "ICN", "INP", + "T20", "ISC", "NPY", "PD", @@ -125,13 +143,12 @@ fixable = [ "SLF", "TCH", "TID", - #"T", "TRY", "UP", "YTT", + "C4", ] unfixable = [] - # Exclude a variety of commonly ignored directories. exclude = [ ".bzr", @@ -155,24 +172,20 @@ exclude = [ "node_modules", "venv", ] - # Same as Black. - # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" - [tool.ruff.mccabe] # Unlike Flake8, default to a complexity level of 10. max-complexity = 10 [tool.ruff.per-file-ignores] -"tests/*" = ["S101", "D"] +"tests/*" = ["S101", "D", "ANN"] [tool.mypy] python_version = "3.10" plugins = ["pydantic.mypy"] - warn_redundant_casts = true warn_unused_ignores = true disallow_any_generics = true @@ -181,7 +194,6 @@ no_implicit_reexport = true # hopefully switch to strict in the future # disallow_untyped_defs = true - [tool.pydantic-mypy] init_forbid_extra = true init_typed = true diff --git a/tests/test_utils.py b/tests/test_utils.py index e6c7325..22a03c3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import pytest from pandas import Timestamp from pandas.testing import assert_frame_equal @@ -70,9 +68,6 @@ def test_int_price_to_str(self): assert results == expected_results - # for case, ex_res in zip(test_cases, expected_results): - # assert int_price_to_str(case) == ex_res - def test_remove_emojis(self): assert remove_emojis("💩") == " " assert ( @@ -108,8 +103,8 @@ def test_graph_save(self): start_date = Timestamp("2022-08-01 03:15:00") end_date = Timestamp("2022-08-12 03:15:00") entry_fee = 1 - df = read_excel_to_df(file_path, start_date, end_date) - raffle_data = RaffleData(start_date, end_date, entry_fee, df) + raffle_dataframe = read_excel_to_df(file_path, start_date, end_date) + raffle_data = RaffleData(start_date, end_date, entry_fee, raffle_dataframe) save_raffle(1, 1, raffle_data) raffle_from_db = get_raffle(1) delete_raffle_data(1) @@ -119,5 +114,5 @@ def test_graph_save(self): # behavior that get_raffle returns without index and # read returns with probably should be changed. - df.set_index("date", inplace=True) - assert_frame_equal(df, raffle_from_db.df) + raffle_dataframe = raffle_dataframe.set_index("date") + assert_frame_equal(raffle_dataframe, raffle_from_db.df)