From 5ebb00111fb6345e8bfcdd20949a9e250024d17e Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Sat, 4 Mar 2023 13:22:47 +0000 Subject: [PATCH 1/5] Begin tooling improvements --- .github/workflows/python-package.yml | 38 -- .github/workflows/tests.yml | 26 + .pre-commit-config.yaml | 13 + .pylintrc | 508 --------------- poetry.lock | 893 ++++++++++++++++----------- pyproject.toml | 25 +- quiffen/core/account.py | 20 +- quiffen/core/base.py | 2 +- quiffen/core/category.py | 20 +- quiffen/core/class_type.py | 2 +- quiffen/core/investment.py | 30 +- quiffen/core/qif.py | 10 +- quiffen/core/security.py | 16 +- quiffen/core/split.py | 22 +- quiffen/core/transaction.py | 42 +- quiffen/utils.py | 29 +- tox.ini | 46 ++ 17 files changed, 740 insertions(+), 1002 deletions(-) delete mode 100644 .github/workflows/python-package.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .pre-commit-config.yaml delete mode 100644 .pylintrc create mode 100644 tox.ini diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml deleted file mode 100644 index 7e23780..0000000 --- a/.github/workflows/python-package.yml +++ /dev/null @@ -1,38 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Python lint and test - -on: - push: - branches: [ "*" ] - pull_request: - branches: [ "main" ] - -jobs: - test: - - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] - - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install poetry - poetry install - - name: Lint with pylint - run: | - poetry run pylint quiffen --rcfile=.pylintrc --output-format=colorized - poetry run pylint tests --rcfile=.pylintrc --output-format=colorized - - name: Test with pytest - run: | - poetry run pytest \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c3f1aed --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,26 @@ +name: Tests + +on: + - push + - pull_request + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.8', '3.9', '3.10', '3.11'] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions poetry + - name: Test with tox + run: tox \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8004f58 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,13 @@ +repos: +- repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort +- repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.254 + hooks: + - id: ruff \ No newline at end of file diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index c0c62f9..0000000 --- a/.pylintrc +++ /dev/null @@ -1,508 +0,0 @@ -[MASTER] - -init-hook='import sys; sys.path.append("./quiffen")' - -# 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-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS,testing - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns=.+\.yml - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -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 modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# 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 - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# 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 reenable 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, - import-error, - logging-format-interpolation, - logging-fstring-interpolation, - missing-docstring, - invalid-name, - broad-except, - too-many-return-statements, - too-many-branches, - too-many-statements, - duplicate-code, - no-else-return, - len-as-condition, - literal-comparison, - too-many-locals, - no-value-for-parameter, - wildcard-import, - too-many-instance-attributes, - no-name-in-module, - -# 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 - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=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=colorized - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[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 - - -[LOGGING] - -# Format style used to check logging format string. `old` means using % -# formatting, while `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 - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package.. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[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 missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# 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 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 - -# 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= - -# 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 - - -[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 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 - - -[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=80 - -# Maximum number of lines in a module. -max-module-lines=3000 - -# 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 - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# 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. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-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. -#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. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# 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. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-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. -#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 - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[STRING] - -# This flag controls whether the implicit-str-concat-in-sequence should -# generate a warning on implicit string concatenation in sequences defined over -# several lines. -check-str-concat-over-line-jumps=no - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# 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 - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in 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 - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# 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 - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=10 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement. -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 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index e6c3af4..bff9db1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,32 +1,124 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + [[package]] -name = "astroid" -version = "2.12.12" -description = "An abstract syntax tree for Python with inference support." +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=3.7.2" +python-versions = ">=3.6" +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] + +[[package]] +name = "black" +version = "23.1.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, + {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, + {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, + {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, + {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, + {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, + {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, + {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, + {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, + {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, + {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, + {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, + {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, + {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, + {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, + {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, +] [package.dependencies] -lazy-object-proxy = ">=1.4.0" -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} -wrapt = [ - {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, - {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, +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\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[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 = "cachetools" +version = "5.3.0" +description = "Extensible memoizing collections and decorators" +category = "dev" +optional = false +python-versions = "~=3.7" +files = [ + {file = "cachetools-5.3.0-py3-none-any.whl", hash = "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"}, + {file = "cachetools-5.3.0.tar.gz", hash = "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14"}, ] [[package]] -name = "attrs" -version = "22.1.0" -description = "Classes Without Boilerplate" +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6.1" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] -[package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +[[package]] +name = "chardet" +version = "5.1.0" +description = "Universal encoding detector for Python 3" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chardet-5.1.0-py3-none-any.whl", hash = "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9"}, + {file = "chardet-5.1.0.tar.gz", hash = "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5"}, +] + +[[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" @@ -35,98 +127,218 @@ 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 = "dill" +name = "distlib" version = "0.3.6" -description = "serialize all of python" +description = "Distribution utilities" category = "dev" optional = false -python-versions = ">=3.7" - -[package.extras] -graph = ["objgraph (>=1.7.2)"] +python-versions = "*" +files = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] [[package]] name = "exceptiongroup" -version = "1.0.0" +version = "1.1.0" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, +] [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "filelock" +version = "3.9.0" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, + {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "identify" +version = "2.5.18" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "identify-2.5.18-py2.py3-none-any.whl", hash = "sha256:93aac7ecf2f6abf879b8f29a8002d3c6de7086b8c28d88e1ad15045a15ab63f9"}, + {file = "identify-2.5.18.tar.gz", hash = "sha256:89e144fa560cc4cffb6ef2ab5e9fb18ed9f9b3cb054384bab4b95c12f6c309fe"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" category = "dev" optional = false -python-versions = "*" +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 = "isort" -version = "4.3.21" +version = "5.12.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] [package.extras] -pipfile = ["pipreqs", "requirementslib"] -pyproject = ["toml"] -requirements = ["pip-api", "pipreqs"] -xdg-home = ["appdirs (>=1.4.0)"] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] -name = "lazy-object-proxy" -version = "1.8.0" -description = "A fast and thorough lazy object proxy." +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.7" +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 = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] + +[package.dependencies] +setuptools = "*" [[package]] name = "numpy" -version = "1.23.4" -description = "NumPy is the fundamental package for array computing with Python." +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 = "packaging" -version = "21.3" +version = "23.0" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.7" +files = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] [[package]] name = "pandas" -version = "1.5.1" +version = "1.5.3" description = "Powerful data structures for data analysis, time series, and statistics" category = "main" optional = false python-versions = ">=3.8" +files = [ + {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"}, + {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"}, + {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"}, + {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"}, + {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"}, + {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"}, + {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"}, + {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"}, + {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"}, + {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"}, + {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"}, + {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"}, + {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"}, + {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"}, + {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"}, + {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"}, + {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"}, + {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"}, +] [package.dependencies] numpy = [ - {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, {version = ">=1.20.3", markers = "python_version < \"3.10\""}, + {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, + {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, ] python-dateutil = ">=2.8.1" pytz = ">=2020.1" @@ -134,17 +346,33 @@ pytz = ">=2020.1" [package.extras] test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] +[[package]] +name = "pathspec" +version = "0.11.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, + {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, +] + [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "3.1.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.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"}, + {file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"}, +] [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +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.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -153,67 +381,137 @@ 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 = "pre-commit" +version = "3.1.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pre_commit-3.1.1-py2.py3-none-any.whl", hash = "sha256:b80254e60668e1dd1f5c03a1c9e0413941d61f568a57d745add265945f65bfe8"}, + {file = "pre_commit-3.1.1.tar.gz", hash = "sha256:d63e6537f9252d99f65755ae5b79c989b462d511ebbc481b561db6a297e1e865"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "pydantic" -version = "1.10.2" +version = "1.10.5" description = "Data validation and settings management using python type hints" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5920824fe1e21cbb3e38cf0f3dd24857c8959801d1031ce1fac1d50857a03bfb"}, + {file = "pydantic-1.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3bb99cf9655b377db1a9e47fa4479e3330ea96f4123c6c8200e482704bf1eda2"}, + {file = "pydantic-1.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2185a3b3d98ab4506a3f6707569802d2d92c3a7ba3a9a35683a7709ea6c2aaa2"}, + {file = "pydantic-1.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f582cac9d11c227c652d3ce8ee223d94eb06f4228b52a8adaafa9fa62e73d5c9"}, + {file = "pydantic-1.10.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c9e5b778b6842f135902e2d82624008c6a79710207e28e86966cd136c621bfee"}, + {file = "pydantic-1.10.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72ef3783be8cbdef6bca034606a5de3862be6b72415dc5cb1fb8ddbac110049a"}, + {file = "pydantic-1.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:45edea10b75d3da43cfda12f3792833a3fa70b6eee4db1ed6aed528cef17c74e"}, + {file = "pydantic-1.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:63200cd8af1af2c07964546b7bc8f217e8bda9d0a2ef0ee0c797b36353914984"}, + {file = "pydantic-1.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:305d0376c516b0dfa1dbefeae8c21042b57b496892d721905a6ec6b79494a66d"}, + {file = "pydantic-1.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd326aff5d6c36f05735c7c9b3d5b0e933b4ca52ad0b6e4b38038d82703d35b"}, + {file = "pydantic-1.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bb0452d7b8516178c969d305d9630a3c9b8cf16fcf4713261c9ebd465af0d73"}, + {file = "pydantic-1.10.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9a9d9155e2a9f38b2eb9374c88f02fd4d6851ae17b65ee786a87d032f87008f8"}, + {file = "pydantic-1.10.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f836444b4c5ece128b23ec36a446c9ab7f9b0f7981d0d27e13a7c366ee163f8a"}, + {file = "pydantic-1.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:8481dca324e1c7b715ce091a698b181054d22072e848b6fc7895cd86f79b4449"}, + {file = "pydantic-1.10.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87f831e81ea0589cd18257f84386bf30154c5f4bed373b7b75e5cb0b5d53ea87"}, + {file = "pydantic-1.10.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ce1612e98c6326f10888df951a26ec1a577d8df49ddcaea87773bfbe23ba5cc"}, + {file = "pydantic-1.10.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58e41dd1e977531ac6073b11baac8c013f3cd8706a01d3dc74e86955be8b2c0c"}, + {file = "pydantic-1.10.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6a4b0aab29061262065bbdede617ef99cc5914d1bf0ddc8bcd8e3d7928d85bd6"}, + {file = "pydantic-1.10.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:36e44a4de37b8aecffa81c081dbfe42c4d2bf9f6dff34d03dce157ec65eb0f15"}, + {file = "pydantic-1.10.5-cp37-cp37m-win_amd64.whl", hash = "sha256:261f357f0aecda005934e413dfd7aa4077004a174dafe414a8325e6098a8e419"}, + {file = "pydantic-1.10.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b429f7c457aebb7fbe7cd69c418d1cd7c6fdc4d3c8697f45af78b8d5a7955760"}, + {file = "pydantic-1.10.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:663d2dd78596c5fa3eb996bc3f34b8c2a592648ad10008f98d1348be7ae212fb"}, + {file = "pydantic-1.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51782fd81f09edcf265823c3bf43ff36d00db246eca39ee765ef58dc8421a642"}, + {file = "pydantic-1.10.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c428c0f64a86661fb4873495c4fac430ec7a7cef2b8c1c28f3d1a7277f9ea5ab"}, + {file = "pydantic-1.10.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:76c930ad0746c70f0368c4596020b736ab65b473c1f9b3872310a835d852eb19"}, + {file = "pydantic-1.10.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3257bd714de9db2102b742570a56bf7978e90441193acac109b1f500290f5718"}, + {file = "pydantic-1.10.5-cp38-cp38-win_amd64.whl", hash = "sha256:f5bee6c523d13944a1fdc6f0525bc86dbbd94372f17b83fa6331aabacc8fd08e"}, + {file = "pydantic-1.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:532e97c35719f137ee5405bd3eeddc5c06eb91a032bc755a44e34a712420daf3"}, + {file = "pydantic-1.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ca9075ab3de9e48b75fa8ccb897c34ccc1519177ad8841d99f7fd74cf43be5bf"}, + {file = "pydantic-1.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd46a0e6296346c477e59a954da57beaf9c538da37b9df482e50f836e4a7d4bb"}, + {file = "pydantic-1.10.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3353072625ea2a9a6c81ad01b91e5c07fa70deb06368c71307529abf70d23325"}, + {file = "pydantic-1.10.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3f9d9b2be177c3cb6027cd67fbf323586417868c06c3c85d0d101703136e6b31"}, + {file = "pydantic-1.10.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b473d00ccd5c2061fd896ac127b7755baad233f8d996ea288af14ae09f8e0d1e"}, + {file = "pydantic-1.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:5f3bc8f103b56a8c88021d481410874b1f13edf6e838da607dcb57ecff9b4594"}, + {file = "pydantic-1.10.5-py3-none-any.whl", hash = "sha256:7c5b94d598c90f2f46b3a983ffb46ab806a67099d118ae0da7ef21a2a4033b28"}, + {file = "pydantic-1.10.5.tar.gz", hash = "sha256:9e337ac83686645a46db0e825acceea8e02fca4062483f40e9ae178e8bd1103a"}, +] [package.dependencies] -typing-extensions = ">=4.1.0" +typing-extensions = ">=4.2.0" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] [[package]] -name = "pylint" -version = "2.15.5" -description = "python code static checker" +name = "pyproject-api" +version = "1.5.0" +description = "API to interact with the python pyproject.toml based projects" category = "dev" optional = false -python-versions = ">=3.7.2" +python-versions = ">=3.7" +files = [ + {file = "pyproject_api-1.5.0-py3-none-any.whl", hash = "sha256:4c111277dfb96bcd562c6245428f27250b794bfe3e210b8714c4f893952f2c17"}, + {file = "pyproject_api-1.5.0.tar.gz", hash = "sha256:0962df21f3e633b8ddb9567c011e6c1b3dcdfc31b7860c0ede7e24c5a1200fbe"}, +] [package.dependencies] -astroid = ">=2.12.12,<=2.14.0-dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = ">=0.2" -isort = ">=4.2.5,<6" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -tomlkit = ">=0.10.1" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} +packaging = ">=21.3" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} [package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] +docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "importlib-metadata (>=5.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "virtualenv (>=20.17)", "wheel (>=0.38.4)"] [[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" +name = "pyright" +version = "1.1.296" +description = "Command line wrapper for pyright" category = "dev" optional = false -python-versions = ">=3.6.8" +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.296-py3-none-any.whl", hash = "sha256:51cc5f05807b1fb53f9f0e14736b8f772b500a3ba4e0edeb99727e68e700d9ea"}, + {file = "pyright-1.1.296.tar.gz", hash = "sha256:6c3cd394473e55a516ebe443d02b83e63456ef29f052dcf8e64e7875c1418fa6"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" [package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] [[package]] name = "pytest" -version = "7.2.0" +version = "7.2.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, + {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, +] [package.dependencies] attrs = ">=19.2.0" @@ -234,17 +532,119 @@ 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 = "pytz" -version = "2022.5" +version = "2022.7.1" description = "World timezone definitions, modern and historical" category = "main" optional = false python-versions = "*" +files = [ + {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, + {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, +] + +[[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 = "ruff" +version = "0.0.254" +description = "An extremely fast Python linter, written in Rust." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.0.254-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:dd58c500d039fb381af8d861ef456c3e94fd6855c3d267d6c6718c9a9fe07be0"}, + {file = "ruff-0.0.254-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:688379050ae05394a6f9f9c8471587fd5dcf22149bd4304a4ede233cc4ef89a1"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1429be6d8bd3db0bf5becac3a38bd56f8421447790c50599cd90fd53417ec4"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:059a380c08e849b6f312479b18cc63bba2808cff749ad71555f61dd930e3c9a2"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3f15d5d033fd3dcb85d982d6828ddab94134686fac2c02c13a8822aa03e1321"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8deba44fd563361c488dedec90dc330763ee0c01ba54e17df54ef5820079e7e0"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef20bf798ffe634090ad3dc2e8aa6a055f08c448810a2f800ab716cc18b80107"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0deb1d7226ea9da9b18881736d2d96accfa7f328c67b7410478cc064ad1fa6aa"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27d39d697fdd7df1f2a32c1063756ee269ad8d5345c471ee3ca450636d56e8c6"}, + {file = "ruff-0.0.254-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2fc21d060a3197ac463596a97d9b5db2d429395938b270ded61dd60f0e57eb21"}, + {file = "ruff-0.0.254-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f70dc93bc9db15cccf2ed2a831938919e3e630993eeea6aba5c84bc274237885"}, + {file = "ruff-0.0.254-py3-none-musllinux_1_2_i686.whl", hash = "sha256:09c764bc2bd80c974f7ce1f73a46092c286085355a5711126af351b9ae4bea0c"}, + {file = "ruff-0.0.254-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4385cdd30153b7aa1d8f75dfd1ae30d49c918ead7de07e69b7eadf0d5538a1f"}, + {file = "ruff-0.0.254-py3-none-win32.whl", hash = "sha256:c38291bda4c7b40b659e8952167f386e86ec29053ad2f733968ff1d78b4c7e15"}, + {file = "ruff-0.0.254-py3-none-win_amd64.whl", hash = "sha256:e15742df0f9a3615fbdc1ee9a243467e97e75bf88f86d363eee1ed42cedab1ec"}, + {file = "ruff-0.0.254-py3-none-win_arm64.whl", hash = "sha256:b435afc4d65591399eaf4b2af86e441a71563a2091c386cadf33eaa11064dc09"}, + {file = "ruff-0.0.254.tar.gz", hash = "sha256:0eb66c9520151d3bd950ea43b3a088618a8e4e10a5014a72687881e6f3606312"}, +] + +[[package]] +name = "setuptools" +version = "67.4.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"}, + {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"}, +] + +[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" @@ -253,6 +653,10 @@ 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 = "tomli" @@ -261,298 +665,73 @@ 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 = "tomlkit" -version = "0.11.6" -description = "Style preserving TOML library" +name = "tox" +version = "4.4.6" +description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "tox-4.4.6-py3-none-any.whl", hash = "sha256:e3d4a65852f029e5ba441a01824d2d839d30bb8fb071635ef9cb53952698e6bf"}, + {file = "tox-4.4.6.tar.gz", hash = "sha256:9786671d23b673ace7499c602c5746e2a225d1ecd9d9f624d0461303f40bd93b"}, +] + +[package.dependencies] +cachetools = ">=5.3" +chardet = ">=5.1" +colorama = ">=0.4.6" +filelock = ">=3.9" +packaging = ">=23" +platformdirs = ">=2.6.2" +pluggy = ">=1" +pyproject-api = ">=1.5" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} +virtualenv = ">=20.17.1" + +[package.extras] +docs = ["furo (>=2022.12.7)", "sphinx (>=6.1.3)", "sphinx-argparse-cli (>=1.11)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)", "sphinx-copybutton (>=0.5.1)", "sphinx-inline-tabs (>=2022.1.2b11)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.2.2)", "devpi-process (>=0.3)", "diff-cover (>=7.4)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.12.2)", "psutil (>=5.9.4)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-xdist (>=3.1)", "re-assert (>=1.1)", "time-machine (>=2.9)", "wheel (>=0.38.4)"] [[package]] name = "typing-extensions" -version = "4.4.0" +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 = "wrapt" -version = "1.14.1" -description = "Module for decorators, wrappers and monkey patching." +name = "virtualenv" +version = "20.20.0" +description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.20.0-py3-none-any.whl", hash = "sha256:3c22fa5a7c7aa106ced59934d2c20a2ecb7f49b4130b8bf444178a16b880fa45"}, + {file = "virtualenv-20.20.0.tar.gz", hash = "sha256:a8a4b8ca1e28f864b7514a253f98c1d62b64e31e77325ba279248c65fb4fcef4"}, +] -[metadata] -lock-version = "1.1" -python-versions = ">=3.8" -content-hash = "a506d99fee2556a4ce0bfd5056a9789e8f3b4e5e34a56b39bdbc6896f1aefb23" +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<4" -[metadata.files] -astroid = [ - {file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"}, - {file = "astroid-2.12.12.tar.gz", hash = "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83"}, -] -attrs = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, -] -colorama = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -dill = [ - {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, - {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, -] -exceptiongroup = [ - {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"}, - {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isort = [ - {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, - {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, -] -lazy-object-proxy = [ - {file = "lazy-object-proxy-1.8.0.tar.gz", hash = "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156"}, - {file = "lazy_object_proxy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe"}, - {file = "lazy_object_proxy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25"}, - {file = "lazy_object_proxy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"}, - {file = "lazy_object_proxy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7"}, - {file = "lazy_object_proxy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e"}, - {file = "lazy_object_proxy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d"}, - {file = "lazy_object_proxy-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c"}, - {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd"}, - {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858"}, - {file = "lazy_object_proxy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada"}, - {file = "lazy_object_proxy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f"}, - {file = "lazy_object_proxy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c"}, - {file = "lazy_object_proxy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288"}, - {file = "lazy_object_proxy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f"}, - {file = "lazy_object_proxy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0"}, - {file = "lazy_object_proxy-1.8.0-pp37-pypy37_pp73-any.whl", hash = "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891"}, - {file = "lazy_object_proxy-1.8.0-pp38-pypy38_pp73-any.whl", hash = "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec"}, - {file = "lazy_object_proxy-1.8.0-pp39-pypy39_pp73-any.whl", hash = "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8"}, -] -mccabe = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] -numpy = [ - {file = "numpy-1.23.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:95d79ada05005f6f4f337d3bb9de8a7774f259341c70bc88047a1f7b96a4bcb2"}, - {file = "numpy-1.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:926db372bc4ac1edf81cfb6c59e2a881606b409ddc0d0920b988174b2e2a767f"}, - {file = "numpy-1.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c237129f0e732885c9a6076a537e974160482eab8f10db6292e92154d4c67d71"}, - {file = "numpy-1.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8365b942f9c1a7d0f0dc974747d99dd0a0cdfc5949a33119caf05cb314682d3"}, - {file = "numpy-1.23.4-cp310-cp310-win32.whl", hash = "sha256:2341f4ab6dba0834b685cce16dad5f9b6606ea8a00e6da154f5dbded70fdc4dd"}, - {file = "numpy-1.23.4-cp310-cp310-win_amd64.whl", hash = "sha256:d331afac87c92373826af83d2b2b435f57b17a5c74e6268b79355b970626e329"}, - {file = "numpy-1.23.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:488a66cb667359534bc70028d653ba1cf307bae88eab5929cd707c761ff037db"}, - {file = "numpy-1.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce03305dd694c4873b9429274fd41fc7eb4e0e4dea07e0af97a933b079a5814f"}, - {file = "numpy-1.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8981d9b5619569899666170c7c9748920f4a5005bf79c72c07d08c8a035757b0"}, - {file = "numpy-1.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a70a7d3ce4c0e9284e92285cba91a4a3f5214d87ee0e95928f3614a256a1488"}, - {file = "numpy-1.23.4-cp311-cp311-win32.whl", hash = "sha256:5e13030f8793e9ee42f9c7d5777465a560eb78fa7e11b1c053427f2ccab90c79"}, - {file = "numpy-1.23.4-cp311-cp311-win_amd64.whl", hash = "sha256:7607b598217745cc40f751da38ffd03512d33ec06f3523fb0b5f82e09f6f676d"}, - {file = "numpy-1.23.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7ab46e4e7ec63c8a5e6dbf5c1b9e1c92ba23a7ebecc86c336cb7bf3bd2fb10e5"}, - {file = "numpy-1.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8aae2fb3180940011b4862b2dd3756616841c53db9734b27bb93813cd79fce6"}, - {file = "numpy-1.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c053d7557a8f022ec823196d242464b6955a7e7e5015b719e76003f63f82d0f"}, - {file = "numpy-1.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0882323e0ca4245eb0a3d0a74f88ce581cc33aedcfa396e415e5bba7bf05f68"}, - {file = "numpy-1.23.4-cp38-cp38-win32.whl", hash = "sha256:dada341ebb79619fe00a291185bba370c9803b1e1d7051610e01ed809ef3a4ba"}, - {file = "numpy-1.23.4-cp38-cp38-win_amd64.whl", hash = "sha256:0fe563fc8ed9dc4474cbf70742673fc4391d70f4363f917599a7fa99f042d5a8"}, - {file = "numpy-1.23.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c67b833dbccefe97cdd3f52798d430b9d3430396af7cdb2a0c32954c3ef73894"}, - {file = "numpy-1.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f76025acc8e2114bb664294a07ede0727aa75d63a06d2fae96bf29a81747e4a7"}, - {file = "numpy-1.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12ac457b63ec8ded85d85c1e17d85efd3c2b0967ca39560b307a35a6703a4735"}, - {file = "numpy-1.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95de7dc7dc47a312f6feddd3da2500826defdccbc41608d0031276a24181a2c0"}, - {file = "numpy-1.23.4-cp39-cp39-win32.whl", hash = "sha256:f2f390aa4da44454db40a1f0201401f9036e8d578a25f01a6e237cea238337ef"}, - {file = "numpy-1.23.4-cp39-cp39-win_amd64.whl", hash = "sha256:f260da502d7441a45695199b4e7fd8ca87db659ba1c78f2bbf31f934fe76ae0e"}, - {file = "numpy-1.23.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61be02e3bf810b60ab74e81d6d0d36246dbfb644a462458bb53b595791251911"}, - {file = "numpy-1.23.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:296d17aed51161dbad3c67ed6d164e51fcd18dbcd5dd4f9d0a9c6055dce30810"}, - {file = "numpy-1.23.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4d52914c88b4930dafb6c48ba5115a96cbab40f45740239d9f4159c4ba779962"}, - {file = "numpy-1.23.4.tar.gz", hash = "sha256:ed2cc92af0efad20198638c69bb0fc2870a58dabfba6eb722c933b48556c686c"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pandas = [ - {file = "pandas-1.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0a78e05ec09731c5b3bd7a9805927ea631fe6f6cb06f0e7c63191a9a778d52b4"}, - {file = "pandas-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5b0c970e2215572197b42f1cff58a908d734503ea54b326412c70d4692256391"}, - {file = "pandas-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f340331a3f411910adfb4bbe46c2ed5872d9e473a783d7f14ecf49bc0869c594"}, - {file = "pandas-1.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c709f4700573deb2036d240d140934df7e852520f4a584b2a8d5443b71f54d"}, - {file = "pandas-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32e3d9f65606b3f6e76555bfd1d0b68d94aff0929d82010b791b6254bf5a4b96"}, - {file = "pandas-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a52419d9ba5906db516109660b114faf791136c94c1a636ed6b29cbfff9187ee"}, - {file = "pandas-1.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:66a1ad667b56e679e06ba73bb88c7309b3f48a4c279bd3afea29f65a766e9036"}, - {file = "pandas-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:36aa1f8f680d7584e9b572c3203b20d22d697c31b71189322f16811d4ecfecd3"}, - {file = "pandas-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcf1a82b770b8f8c1e495b19a20d8296f875a796c4fe6e91da5ef107f18c5ecb"}, - {file = "pandas-1.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c25e5c16ee5c0feb6cf9d982b869eec94a22ddfda9aa2fbed00842cbb697624"}, - {file = "pandas-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:932d2d7d3cab44cfa275601c982f30c2d874722ef6396bb539e41e4dc4618ed4"}, - {file = "pandas-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:eb7e8cf2cf11a2580088009b43de84cabbf6f5dae94ceb489f28dba01a17cb77"}, - {file = "pandas-1.5.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cb2a9cf1150302d69bb99861c5cddc9c25aceacb0a4ef5299785d0f5389a3209"}, - {file = "pandas-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:81f0674fa50b38b6793cd84fae5d67f58f74c2d974d2cb4e476d26eee33343d0"}, - {file = "pandas-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:17da7035d9e6f9ea9cdc3a513161f8739b8f8489d31dc932bc5a29a27243f93d"}, - {file = "pandas-1.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:669c8605dba6c798c1863157aefde959c1796671ffb342b80fcb80a4c0bc4c26"}, - {file = "pandas-1.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683779e5728ac9138406c59a11e09cd98c7d2c12f0a5fc2b9c5eecdbb4a00075"}, - {file = "pandas-1.5.1-cp38-cp38-win32.whl", hash = "sha256:ddf46b940ef815af4e542697eaf071f0531449407a7607dd731bf23d156e20a7"}, - {file = "pandas-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:db45b94885000981522fb92349e6b76f5aee0924cc5315881239c7859883117d"}, - {file = "pandas-1.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:927e59c694e039c75d7023465d311277a1fc29ed7236b5746e9dddf180393113"}, - {file = "pandas-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e675f8fe9aa6c418dc8d3aac0087b5294c1a4527f1eacf9fe5ea671685285454"}, - {file = "pandas-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:04e51b01d5192499390c0015630975f57836cc95c7411415b499b599b05c0c96"}, - {file = "pandas-1.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cee0c74e93ed4f9d39007e439debcaadc519d7ea5c0afc3d590a3a7b2edf060"}, - {file = "pandas-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b156a971bc451c68c9e1f97567c94fd44155f073e3bceb1b0d195fd98ed12048"}, - {file = "pandas-1.5.1-cp39-cp39-win32.whl", hash = "sha256:05c527c64ee02a47a24031c880ee0ded05af0623163494173204c5b72ddce658"}, - {file = "pandas-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:6bb391659a747cf4f181a227c3e64b6d197100d53da98dcd766cc158bdd9ec68"}, - {file = "pandas-1.5.1.tar.gz", hash = "sha256:249cec5f2a5b22096440bd85c33106b6102e0672204abd2d5c014106459804ee"}, -] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -pydantic = [ - {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"}, - {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"}, - {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"}, - {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"}, - {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"}, - {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"}, - {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"}, - {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"}, - {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"}, - {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"}, - {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"}, - {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"}, - {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"}, - {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"}, - {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"}, - {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"}, - {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"}, - {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"}, - {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"}, - {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"}, - {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"}, - {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"}, - {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"}, - {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"}, - {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"}, - {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"}, - {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"}, - {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"}, - {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"}, - {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"}, - {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"}, - {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"}, - {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"}, - {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"}, - {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, - {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, -] -pylint = [ - {file = "pylint-2.15.5-py3-none-any.whl", hash = "sha256:c2108037eb074334d9e874dc3c783752cc03d0796c88c9a9af282d0f161a1004"}, - {file = "pylint-2.15.5.tar.gz", hash = "sha256:3b120505e5af1d06a5ad76b55d8660d44bf0f2fc3c59c2bdd94e39188ee3a4df"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pytest = [ - {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, - {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, -] -python-dateutil = [ - {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"}, -] -pytz = [ - {file = "pytz-2022.5-py2.py3-none-any.whl", hash = "sha256:335ab46900b1465e714b4fda4963d87363264eb662aab5e65da039c25f1f5b22"}, - {file = "pytz-2022.5.tar.gz", hash = "sha256:c4d88f472f54d615e9cd582a5004d1e5f624854a6a27a6211591c251f22a6914"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -tomlkit = [ - {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, - {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, -] -typing-extensions = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, -] -wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, -] +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "9be2d7ab67622e4ecd662b63412eb038bbfc45714be0d433861b5413dec91da4" diff --git a/pyproject.toml b/pyproject.toml index 929a6ef..68f7ab4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ [tool.poetry.dependencies] -python = ">=3.8" +python = "^3.8" pydantic = "^1.10.2" pandas = "^1.5.1" python-dateutil = "^2.8.2" @@ -27,9 +27,30 @@ python-dateutil = "^2.8.2" [tool.poetry.group.dev.dependencies] pytest = "^7.2.0" -pylint = "^2.15.5" +black = "^23.1.0" +isort = "^5.12.0" +ruff = "^0.0.254" +tox = "^4.4.6" +pyright = "^1.1.296" +pre-commit = "^3.1.1" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.isort] +profile = "black" +include_trailing_comma = true + +[tool.ruff] +line-length = 88 + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401"] # Ignore unused imports in __init__.py + +[tool.pyright] +pythonVersion = "3.8" + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/quiffen/core/account.py b/quiffen/core/account.py index 8f45645..fcfd570 100644 --- a/quiffen/core/account.py +++ b/quiffen/core/account.py @@ -3,7 +3,7 @@ from datetime import datetime from decimal import Decimal from enum import Enum -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from quiffen import utils from quiffen.core.base import BaseModel, Field @@ -55,15 +55,15 @@ class Account(BaseModel): """ # pylint: enable=line-too-long name: str - desc: str = None - account_type: AccountType = None - credit_limit: Decimal = None - balance: Decimal = None - date_at_balance: datetime = None + desc: Optional[str] = None + account_type: Optional[AccountType] = None + credit_limit: Optional[Decimal] = None + balance: Optional[Decimal] = None + date_at_balance: Optional[datetime] = None transactions: Dict[str, TransactionList] = {} - _last_header: AccountType = None + _last_header: Optional[AccountType] = None - __CUSTOM_FIELDS: List[Field] = [] + __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __eq__(self, other) -> bool: if not isinstance(other, Account): @@ -106,7 +106,7 @@ def set_header(self, header: AccountType) -> None: def add_transaction( self, transaction: TransactionLike, - header: AccountType = None, + header: Optional[AccountType] = None, ) -> None: """Add a transaction to the dict of TransactionList objects. @@ -163,7 +163,7 @@ def merge(self, other: Account) -> None: def to_qif( self, date_format: str = '%Y-%m-%d', - classes: Dict[str, Class] = None + classes: Optional[Dict[str, Class]] = None ) -> str: """Return a QIF-formatted string of this account. diff --git a/quiffen/core/base.py b/quiffen/core/base.py index 5f37fc9..d611667 100644 --- a/quiffen/core/base.py +++ b/quiffen/core/base.py @@ -93,7 +93,7 @@ def from_string(cls, string: str, separator: str = '\n') -> T: """Create a class instance from a string.""" return cls.from_list(string.split(separator)) - def to_dict(self, ignore: Iterable[str] = None) -> Dict[str, Any]: + def to_dict(self, ignore: Optional[Iterable[str]] = None) -> Dict[str, Any]: """Convert the class instance to a dict.""" if ignore is None: ignore = [] diff --git a/quiffen/core/category.py b/quiffen/core/category.py index cbad088..0e8032d 100644 --- a/quiffen/core/category.py +++ b/quiffen/core/category.py @@ -2,7 +2,7 @@ from decimal import Decimal from enum import Enum -from typing import Any, Dict, Iterable, List, Union +from typing import Any, Dict, Iterable, List, Optional, Union from pydantic import validator @@ -89,14 +89,14 @@ class Category(BaseModel): """ # pylint: enable=line-too-long name: str - desc: str = None - tax_related: bool = None + desc: Optional[str] = None + tax_related: Optional[bool] = None category_type: CategoryType = CategoryType.EXPENSE - budget_amount: Decimal = None - tax_schedule_info: str = None - hierarchy: str = None + budget_amount: Optional[Decimal] = None + tax_schedule_info: Optional[str] = None + hierarchy: Optional[str] = None children: List[Category] = [] - parent: Category = None + parent: Optional[Category] = None __CUSTOM_FIELDS: List[Field] = [] @@ -144,7 +144,7 @@ def _refresh_hierarchy(self) -> None: # pylint: disable-next=protected-access child._refresh_hierarchy() - def dict(self, exclude: Iterable[str] = None, **_) -> Dict[str, Any]: + def dict(self, exclude: Optional[Iterable[str]] = None, **_) -> Dict[str, Any]: """Return a representation of the Category object as a dict. Overwrites pydantic.BaseModel.dict or else it will recurse infinitely @@ -174,7 +174,7 @@ def dict(self, exclude: Iterable[str] = None, **_) -> Dict[str, Any]: return res # This is kept for backwards compatibility - def to_dict(self, ignore: Iterable[str] = None, **kwargs) -> Dict[str, Any]: + def to_dict(self, ignore: Optional[Iterable[str]] = None, **kwargs) -> Dict[str, Any]: """Return a representation of the Category object as a dict. Parameters @@ -419,7 +419,7 @@ def from_list(cls, lst: List[str]) -> Category: List of strings containing QIF information about the category. """ kwargs: Dict[str, Any] = {} - new_parent: Union[Category, None] = None + new_parent: Optional[Union[Category, None]] = None for field in lst: line_code, field_info = utils.parse_line_code_and_field_info(field) if not line_code: diff --git a/quiffen/core/class_type.py b/quiffen/core/class_type.py index 9c43c81..5ad9313 100644 --- a/quiffen/core/class_type.py +++ b/quiffen/core/class_type.py @@ -21,7 +21,7 @@ class Class(BaseModel): The description of the class. """ name: str - desc: str = None + desc: Optional[str] = None categories: List[Category] = [] # pylint: disable-next=unused-private-member diff --git a/quiffen/core/investment.py b/quiffen/core/investment.py index cc42701..46591be 100644 --- a/quiffen/core/investment.py +++ b/quiffen/core/investment.py @@ -2,7 +2,7 @@ from datetime import datetime from decimal import Decimal -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from quiffen import utils from quiffen.core.base import BaseModel, Field @@ -46,18 +46,18 @@ class Investment(BaseModel): The line number of the investment in the QIF file. """ date: datetime - action: str = None - security: str = None - price: Decimal = None - quantity: Decimal = None - cleared: str = None - amount: Decimal = None - memo: str = None - first_line: str = None - to_account: str = None - transfer_amount: Decimal = None - commission: Decimal = None - line_number: int = None + action: Optional[str] = None + security: Optional[str] = None + price: Optional[Decimal] = None + quantity: Optional[Decimal] = None + cleared: Optional[str] = None + amount: Optional[Decimal] = None + memo: Optional[str] = None + first_line: Optional[str] = None + to_account: Optional[str] = None + transfer_amount: Optional[Decimal] = None + commission: Optional[Decimal] = None + line_number: Optional[int] = None __CUSTOM_FIELDS: List[Field] = [] @@ -116,7 +116,7 @@ def from_list( cls, lst: List[str], day_first: bool = False, - line_number: int = None, + line_number: Optional[int] = None, ) -> Investment: """Return a class instance from a list of QIF strings. @@ -191,7 +191,7 @@ def from_string( string: str, separator: str = '\n', day_first: bool = False, - line_number: int = None, + line_number: Optional[int] = None, ) -> Investment: """Return a class instance from a QIF string. diff --git a/quiffen/core/qif.py b/quiffen/core/qif.py index fd92cd7..f17cbd1 100644 --- a/quiffen/core/qif.py +++ b/quiffen/core/qif.py @@ -350,7 +350,7 @@ def remove_security(self, security_symbol: str) -> Security: def to_qif( self, - path: Union[FilePath, str, None] = None, + path: Optional[Union[FilePath, str, None]] = None, date_format: str = '%Y-%m-%d', ) -> str: """Convert the Qif object to a QIF file""" @@ -383,7 +383,7 @@ def _get_data_dicts( self, data_type: QifDataType = QifDataType.TRANSACTIONS, date_format: Optional[str] = '%Y-%m-%d', - ignore: List[str] = None, + ignore: Optional[List[str]] = None, ) -> List[Dict[str, Any]]: """Converts specified data from the Qif object to a list of dicts""" if ignore is None: @@ -447,10 +447,10 @@ def _get_data_dicts( def to_csv( self, - path: Union[FilePath, str, None] = None, + path: Optional[Union[FilePath, str, None]] = None, data_type: QifDataType = QifDataType.TRANSACTIONS, date_format: str = '%Y-%m-%d', - ignore: List[str] = None, + ignore: Optional[List[str]] = None, delimiter: str = ',', quote_character: str = '"', ) -> str: @@ -514,7 +514,7 @@ def to_csv( def to_dataframe( self, data_type: QifDataType = QifDataType.TRANSACTIONS, - ignore: List[str] = None, + ignore: Optional[List[str]] = None, ) -> pd.DataFrame: """Convert part of the Qif object to a Pandas DataFrame. The data_type parameter can be used to specify which part of the Qif diff --git a/quiffen/core/security.py b/quiffen/core/security.py index ca80535..9769773 100644 --- a/quiffen/core/security.py +++ b/quiffen/core/security.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from quiffen import utils from quiffen.core.base import BaseModel, Field @@ -22,11 +22,11 @@ class Security(BaseModel): line_number : int The line number of the security in the QIF file """ - name: str = None - symbol: str = None - type: str = None - goal: str = None - line_number: int = None + name: Optional[str] = None + symbol: Optional[str] = None + type: Optional[str] = None + goal: Optional[str] = None + line_number: Optional[int] = None # pylint: disable-next=unused-private-member __CUSTOM_FIELDS: List[Field] = [] @@ -81,7 +81,7 @@ def to_qif(self) -> str: def from_list( cls, lst: List[str], - line_number: int = None, + line_number: Optional[int] = None, ) -> Security: """Return a class instance from a list of QIF strings. @@ -135,7 +135,7 @@ def from_string( cls, string: str, separator: str = '\n', - line_number: int = None, + line_number: Optional[int] = None, ) -> Security: """Return a class instance from a QIF string. diff --git a/quiffen/core/split.py b/quiffen/core/split.py index aa90573..5263f2d 100644 --- a/quiffen/core/split.py +++ b/quiffen/core/split.py @@ -2,7 +2,7 @@ from datetime import datetime from decimal import Decimal -from typing import Dict, List, Union +from typing import Dict, List, Optional, Union from quiffen import utils from quiffen.core.base import BaseModel, Field @@ -42,15 +42,15 @@ class Split(BaseModel): [Split(amount=100.6, category=Category(name='Electrical', expense=True, hierarchy='Electrical'))] """ # pylint: enable=line-too-long - date: datetime = None - amount: Decimal = None - memo: str = None - cleared: str = None - category: Category = None - to_account: str = None - check_number: Union[int, str] = None - percent: Decimal = None - payee_address: str = None + date: Optional[datetime] = None + amount: Optional[Decimal] = None + memo: Optional[str] = None + cleared: Optional[str] = None + category: Optional[Category] = None + to_account: Optional[str] = None + check_number: Optional[Union[int, str]] = None + percent: Optional[Decimal] = None + payee_address: Optional[str] = None __CUSTOM_FIELDS: List[Field] = [] @@ -72,7 +72,7 @@ def __str__(self) -> str: def to_qif( self, date_format: str = '%Y-%m-%d', - classes: Dict[str, Class] = None, + classes: Optional[Dict[str, Class]] = None, ) -> str: """Returns a QIF string representation of the split.""" if classes is None: diff --git a/quiffen/core/transaction.py b/quiffen/core/transaction.py index d8861d7..0b5b03e 100644 --- a/quiffen/core/transaction.py +++ b/quiffen/core/transaction.py @@ -103,26 +103,26 @@ class Transaction(BaseModel): # pylint: enable=line-too-long date: datetime amount: Decimal - memo: str = None - cleared: str = None - payee: str = None - payee_address: str = None - category: Category = None - check_number: Union[int, str] = None - reimbursable_expense: bool = None - small_business_expense: bool = None - to_account: str = None - first_payment_date: datetime = None - loan_length: Decimal = None - num_payments: int = None - periods_per_annum: int = None - interest_rate: Decimal = None - current_loan_balance: Decimal = None - original_loan_amount: Decimal = None - line_number: int = None + memo: Optional[str] = None + cleared: Optional[str] = None + payee: Optional[str] = None + payee_address: Optional[str] = None + category: Optional[Category] = None + check_number: Optional[Union[int, str]] = None + reimbursable_expense: Optional[bool] = None + small_business_expense: Optional[bool] = None + to_account: Optional[str] = None + first_payment_date: Optional[datetime] = None + loan_length: Optional[Decimal] = None + num_payments: Optional[int] = None + periods_per_annum: Optional[int] = None + interest_rate: Optional[Decimal] = None + current_loan_balance: Optional[Decimal] = None + original_loan_amount: Optional[Decimal] = None + line_number: Optional[int] = None splits: List[Split] = [] _split_categories: Dict[str, Category] = {} - _last_split: Split = None + _last_split: Optional[Split] = None __CUSTOM_FIELDS: List[Field] = [] @@ -256,7 +256,7 @@ def _create_class_from_category_string( def to_qif( self, date_format: str = '%Y-%m-%d', - classes: Dict[str, Class] = None, + classes: Optional[Dict[str, Class]] = None, ) -> str: """Converts a Transaction to a QIF string""" if classes is None: @@ -320,7 +320,7 @@ def from_list( cls, lst: List[str], day_first: bool = False, - line_number: int = None, + line_number: Optional[int] = None, ) -> Tuple[Transaction, Dict[str, Class]]: """Return a class instance from a list of QIF strings. @@ -510,7 +510,7 @@ def from_string( string: str, separator: str = '\n', day_first: bool = False, - line_number: int = None, + line_number: Optional[int] = None, ) -> Tuple[Transaction, Dict[str, Class]]: """Return a class instance from a QIF string. diff --git a/quiffen/utils.py b/quiffen/utils.py index 7bbb773..c0db9f3 100644 --- a/quiffen/utils.py +++ b/quiffen/utils.py @@ -40,16 +40,16 @@ def parse_date(date_string: str, day_first: bool = False) -> datetime: date_string = date_string.replace(' ', '0') date_string = date_string.replace('\'', '/') - try: - # QIF files allow some really strange date formats, such as - # %d0%B0%Y (e.g. 0100202022 for 2022-02-01) - # This extends also to month-first dates. The following regex checks - # for this and converts it to a date string that can be parsed by - # dateutil.parser - date_parts = ZERO_SEPARATED_DATE.search(date_string).groups() + # QIF files allow some really strange date formats, such as + # %d0%B0%Y (e.g. 0100202022 for 2022-02-01) + # This extends also to month-first dates. The following regex checks + # for this and converts it to a date string that can be parsed by + # dateutil.parser + date_search = ZERO_SEPARATED_DATE.search(date_string) + + if date_search: + date_parts = date_search.groups() date_string = ' '.join(date_parts) - except AttributeError: - pass return parser.parse(date_string, dayfirst=day_first) @@ -115,26 +115,25 @@ def convert_custom_fields_to_qif_string( def apply_csv_formatting_to_scalar( obj: Any, date_format: str = '%Y-%m-%d', - stringify: bool = False, ) -> Union[str, int, float]: """Apply CSV-friendly formatting to a scalar value""" if isinstance(obj, (datetime, date)) and date_format: return obj.strftime(date_format) + elif isinstance(obj, (datetime, date)): + return obj.isoformat() elif isinstance(obj, Enum): return str(obj.value) elif isinstance(obj, Decimal): if obj % 1: return float(obj) return int(obj) - elif stringify: - return str(obj) - return obj + return str(obj) def apply_csv_formatting_to_container( obj: Union[List[Any], Dict[Any, Any]], date_format: str = '%Y-%m-%d', -) -> Union[List[Any], Dict[Any, Any], str]: +) -> Union[List[Any], Dict[Any, Any], str, int, float]: """Recursively apply CSV-friendly formatting to a container""" if isinstance(obj, list): return [ @@ -143,7 +142,7 @@ def apply_csv_formatting_to_container( ] elif isinstance(obj, dict): return { - apply_csv_formatting_to_scalar(key, date_format, True): + apply_csv_formatting_to_scalar(key, date_format): apply_csv_formatting_to_container(value, date_format) for key, value in obj.items() } diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..fc3748b --- /dev/null +++ b/tox.ini @@ -0,0 +1,46 @@ +[tox] +requires = + tox>=4 +envlist = py{38,39,310,311}, black, ruff, pyright, isort +isolated_build = true + +[gh-actions] +python = + 3.8: py38, black, ruff, pyright, isort + 3.9: py39 + 3.10: py310 + 3.11: py311 + +[testenv] +setenv = + PYTHONPATH = "{toxinidir}" +allowlist_externals = poetry +commands_pre = + poetry install +commands = + poetry run pytest --basetemp="{envtmpdir}" tests + + +[testenv:pyright] +setenv = + PYTHONPATH = "{toxinidir}" +allowlist_externals = poetry +commands_pre = + poetry install -v --no-root --sync +commands = + poetry run pyright --project "{toxinidir}" quiffen + +[testenv:ruff] +basepython = python3.8 +deps = ruff +commands = ruff "{toxinidir}" + +[testenv:black] +basepython = python3.8 +deps = black +commands = black --check --diff "{toxinidir}" + +[testenv:isort] +basepython = python3.8 +deps = isort +commands = isort --check-only --diff "{toxinidir}" From e68223d2d85153f7ebbe0e122de4c7b75e85d3e9 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Sat, 4 Mar 2023 14:07:43 +0000 Subject: [PATCH 2/5] Remove pylint comments --- quiffen/core/account.py | 2 -- quiffen/core/base.py | 1 - quiffen/core/category.py | 7 ------- quiffen/core/class_type.py | 1 - quiffen/core/security.py | 1 - quiffen/core/split.py | 2 -- quiffen/core/transaction.py | 4 ---- 7 files changed, 18 deletions(-) diff --git a/quiffen/core/account.py b/quiffen/core/account.py index fcfd570..1a2ed91 100644 --- a/quiffen/core/account.py +++ b/quiffen/core/account.py @@ -26,7 +26,6 @@ class AccountType(str, Enum): class Account(BaseModel): - # pylint: disable=line-too-long """ A class representing a QIF account. @@ -53,7 +52,6 @@ class Account(BaseModel): >>> acc Account(name='Personal Bank Account', desc='My Personal bank account with Barclays.', ...) """ - # pylint: enable=line-too-long name: str desc: Optional[str] = None account_type: Optional[AccountType] = None diff --git a/quiffen/core/base.py b/quiffen/core/base.py index d611667..a1cded1 100644 --- a/quiffen/core/base.py +++ b/quiffen/core/base.py @@ -37,7 +37,6 @@ def __lt__(self, other) -> bool: ) -# pylint: disable=too-few-public-methods class BaseModel(PydanticBaseModel, Generic[T]): class Config: extra = 'allow' diff --git a/quiffen/core/category.py b/quiffen/core/category.py index 0e8032d..6bfc64a 100644 --- a/quiffen/core/category.py +++ b/quiffen/core/category.py @@ -17,7 +17,6 @@ class CategoryType(str, Enum): class Category(BaseModel): - # pylint: disable=line-too-long """ A node-like class used to represent a category. Can be built into trees to represent category families. @@ -87,7 +86,6 @@ class Category(BaseModel): Food (root) └─ Chicken """ - # pylint: enable=line-too-long name: str desc: Optional[str] = None tax_related: Optional[bool] = None @@ -121,7 +119,6 @@ def __str__(self) -> str: def __lt__(self, other) -> bool: return self.name < other.name - # pylint: disable=no-self-argument @validator('hierarchy', pre=True, always=True) def _set_hierarchy(cls, v: str, values) -> str: if not v: @@ -134,14 +131,12 @@ def _set_hierarchy(cls, v: str, values) -> str: raise ValueError('Hierarchy must end with name.', v, values['name']) return v - # pylint: enable=no-self-argument def _refresh_hierarchy(self) -> None: """Refreshes the hierarchy of the current category and all its children recursively.""" for child in self.children: child.hierarchy = self.hierarchy + ':' + child.name - # pylint: disable-next=protected-access child._refresh_hierarchy() def dict(self, exclude: Optional[Iterable[str]] = None, **_) -> Dict[str, Any]: @@ -251,7 +246,6 @@ def set_parent(self, parent: Union[Category, None]) -> None: if parent: parent.children.append(self) - # pylint: disable-next=protected-access parent._refresh_hierarchy() else: self.hierarchy = self.name @@ -405,7 +399,6 @@ def _to_qif_string(self) -> str: def to_qif(self) -> str: """Return a QIF representation of all the categories in the tree.""" return '^\n'.join( - # pylint: disable-next=protected-access [c._to_qif_string() for c in self.traverse_down()] ) diff --git a/quiffen/core/class_type.py b/quiffen/core/class_type.py index 5ad9313..4e9226e 100644 --- a/quiffen/core/class_type.py +++ b/quiffen/core/class_type.py @@ -24,7 +24,6 @@ class Class(BaseModel): desc: Optional[str] = None categories: List[Category] = [] - # pylint: disable-next=unused-private-member __CUSTOM_FIELDS: List[Field] = [] def __eq__(self, other) -> bool: diff --git a/quiffen/core/security.py b/quiffen/core/security.py index 9769773..3402318 100644 --- a/quiffen/core/security.py +++ b/quiffen/core/security.py @@ -28,7 +28,6 @@ class Security(BaseModel): goal: Optional[str] = None line_number: Optional[int] = None - # pylint: disable-next=unused-private-member __CUSTOM_FIELDS: List[Field] = [] def __str__(self) -> str: diff --git a/quiffen/core/split.py b/quiffen/core/split.py index 5263f2d..90f5319 100644 --- a/quiffen/core/split.py +++ b/quiffen/core/split.py @@ -11,7 +11,6 @@ class Split(BaseModel): - # pylint: disable=line-too-long """ A class used to represent a split in a transaction. @@ -41,7 +40,6 @@ class Split(BaseModel): >>> print(tr.splits) [Split(amount=100.6, category=Category(name='Electrical', expense=True, hierarchy='Electrical'))] """ - # pylint: enable=line-too-long date: Optional[datetime] = None amount: Optional[Decimal] = None memo: Optional[str] = None diff --git a/quiffen/core/transaction.py b/quiffen/core/transaction.py index 0b5b03e..8b0fd08 100644 --- a/quiffen/core/transaction.py +++ b/quiffen/core/transaction.py @@ -22,7 +22,6 @@ class Transaction(BaseModel): - # pylint: disable=line-too-long """ A class used to represent a transaction. @@ -100,7 +99,6 @@ class Transaction(BaseModel): {'amount': 150.6, 'category': {'name': 'Finances', 'expense': True, 'income': False, 'hierarchy': 'Finances', 'children': []}} """ - # pylint: enable=line-too-long date: datetime amount: Decimal memo: Optional[str] = None @@ -148,7 +146,6 @@ def __str__(self) -> str: return 'Transaction:' + properties - # pylint: disable=no-self-argument @root_validator(pre=True) def create_split_categories(cls, values: Dict[str, Any]) -> Dict: if splits := values.get('splits'): @@ -180,7 +177,6 @@ def check_split_percentages_and_amounts( 'Split amounts cannot exceed the amount of the transaction' ) return splits - # pylint: enable=no-self-argument @property def split_categories(self) -> Dict[str, Category]: From 61f08f8268937ce28dad4c6f5a949f78bf713405 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Sat, 4 Mar 2023 14:31:06 +0000 Subject: [PATCH 3/5] Fix typing --- quiffen/core/account.py | 3 +++ quiffen/core/base.py | 4 ++-- quiffen/core/category.py | 19 +++++++++++++------ quiffen/core/class_type.py | 4 ++-- quiffen/core/investment.py | 2 +- quiffen/core/qif.py | 34 ++++++++++++++++++++++++++++++---- quiffen/core/security.py | 2 +- quiffen/core/split.py | 5 +++-- quiffen/core/transaction.py | 30 +++++++++++++++--------------- quiffen/utils.py | 6 +++--- 10 files changed, 73 insertions(+), 36 deletions(-) diff --git a/quiffen/core/account.py b/quiffen/core/account.py index 1a2ed91..cbf656b 100644 --- a/quiffen/core/account.py +++ b/quiffen/core/account.py @@ -134,6 +134,9 @@ def add_transaction( except ValueError as e: raise ValueError('Header must be a valid AccountType.') from e + if not header: + raise RuntimeError('No header provided, and no last header set.') + if header not in self.transactions: self.transactions[header] = [] diff --git a/quiffen/core/base.py b/quiffen/core/base.py index a1cded1..86fb4d7 100644 --- a/quiffen/core/base.py +++ b/quiffen/core/base.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import Any, Dict, Generic, Iterable, List, Type, TypeVar +from typing import Any, Dict, Generic, Iterable, List, Optional, Type, TypeVar from pydantic import BaseModel as PydanticBaseModel @@ -41,7 +41,7 @@ class BaseModel(PydanticBaseModel, Generic[T]): class Config: extra = 'allow' - __CUSTOM_FIELDS: List[Field] = [] + __CUSTOM_FIELDS: List[Field] = [] # type: ignore @classmethod def add_custom_field( diff --git a/quiffen/core/category.py b/quiffen/core/category.py index 6bfc64a..9384771 100644 --- a/quiffen/core/category.py +++ b/quiffen/core/category.py @@ -96,14 +96,16 @@ class Category(BaseModel): children: List[Category] = [] parent: Optional[Category] = None - __CUSTOM_FIELDS: List[Field] = [] + __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __str__(self) -> str: properties = '' for (object_property, value) in self.__dict__.items(): if value: if object_property == 'parent': - properties += f'\n\tParent: {self.parent.name}' + properties += ( + f'\n\tParent: {self.parent.name if self.parent else "None"}' + ) elif object_property == 'children': properties += f'\n\tChildren: {len(self.children)}' elif object_property == 'category_type': @@ -136,7 +138,7 @@ def _refresh_hierarchy(self) -> None: """Refreshes the hierarchy of the current category and all its children recursively.""" for child in self.children: - child.hierarchy = self.hierarchy + ':' + child.name + child.hierarchy = self.hierarchy + ':' + child.name if self.hierarchy else child.name child._refresh_hierarchy() def dict(self, exclude: Optional[Iterable[str]] = None, **_) -> Dict[str, Any]: @@ -195,7 +197,7 @@ def traverse_down(self) -> List[Category]: nodes_to_visit.extend(current_node.children) return all_children - def traverse_up(self) -> List[Category]: + def traverse_up(self: Category) -> List[Category]: """Return a list of all parents, grandparents etc. of the current category. @@ -309,13 +311,18 @@ def remove_child( raise KeyError(f"Category '{child}' not found.") parent = child_category.parent - new_children = [c for c in parent.children if c != child_category] + + if parent: + new_children = [c for c in parent.children if c != child_category] + else: + new_children = [] if keep_children: for grandchild in child_category.children: new_children.append(grandchild) - parent.set_children(new_children) + if parent: + parent.set_children(new_children) child_category.set_parent(None) return child_category diff --git a/quiffen/core/class_type.py b/quiffen/core/class_type.py index 4e9226e..c1b0ce7 100644 --- a/quiffen/core/class_type.py +++ b/quiffen/core/class_type.py @@ -2,7 +2,7 @@ # reserved word in Python. from __future__ import annotations -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from quiffen import utils from quiffen.core.base import BaseModel, Field @@ -24,7 +24,7 @@ class Class(BaseModel): desc: Optional[str] = None categories: List[Category] = [] - __CUSTOM_FIELDS: List[Field] = [] + __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __eq__(self, other) -> bool: if not isinstance(other, Class): diff --git a/quiffen/core/investment.py b/quiffen/core/investment.py index 46591be..ef9a7c3 100644 --- a/quiffen/core/investment.py +++ b/quiffen/core/investment.py @@ -59,7 +59,7 @@ class Investment(BaseModel): commission: Optional[Decimal] = None line_number: Optional[int] = None - __CUSTOM_FIELDS: List[Field] = [] + __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __str__(self) -> str: properties = '' diff --git a/quiffen/core/qif.py b/quiffen/core/qif.py index f17cbd1..3588687 100644 --- a/quiffen/core/qif.py +++ b/quiffen/core/qif.py @@ -68,7 +68,7 @@ class Qif(BaseModel): classes: Dict[str, Class] = {} securities: Dict[str, Security] = {} - __CUSTOM_FIELDS: List[Field] = [] + __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __str__(self) -> str: accounts_str = '\n'.join(str(acc) for acc in self.accounts.values()) @@ -187,7 +187,7 @@ def parse( if '!Type:Cat' in header_line: # Section contains category information new_category = Category.from_list(sanitised_section_lines) - categories = add_categories_to_container( + categories = add_categories_to_container( # type: ignore new_category, categories, ) @@ -211,6 +211,14 @@ def parse( day_first=day_first, line_number=line_number, ) + + if last_account is None: + raise ParserException( + f'Line {line_number}: ' + 'No account found before investment. ' + 'This should not happen.' + ) + accounts[last_account].add_transaction( new_investment, AccountType('Invst'), @@ -221,6 +229,11 @@ def parse( sanitised_section_lines, line_number=line_number, ) + if new_security.symbol is None: + raise ParserException( + f'Line {line_number}: ' + f'No symbol found for security.' + ) securities[new_security.symbol] = new_security elif '!Type' in header_line and not accounts: # Accounts is empty and there's a transaction, so create default @@ -244,6 +257,14 @@ def parse( day_first=day_first, line_number=line_number, ) + + if last_account is None: + raise ParserException( + f'Line {line_number}: ' + 'No account found before transactions. ' + 'This should not happen.' + ) + accounts[last_account].add_transaction( new_transaction, AccountType(header_line.split(':')[1]), @@ -289,7 +310,7 @@ def remove_account(self, account_name: str) -> Account: def add_category(self, new_category: Category) -> None: """Add a new category to the Qif object""" - self.categories = add_categories_to_container( + self.categories = add_categories_to_container( # type: ignore new_category, self.categories, ) @@ -333,6 +354,11 @@ def remove_class(self, class_name: str) -> Class: def add_security(self, new_security: Security) -> None: """Add a new security to the Qif object""" + if not new_security.symbol: + raise ValueError( + 'Cannot add a security without a symbol to the Qif object.' + ) + if new_security.symbol in self.securities: self.securities[new_security.symbol].merge(new_security) else: @@ -434,7 +460,7 @@ def _get_data_dicts( ) # Format and hide private fields - return [ + return [ # type: ignore utils.apply_csv_formatting_to_container( { k: v for k, v in data_dict.items() diff --git a/quiffen/core/security.py b/quiffen/core/security.py index 3402318..6b31038 100644 --- a/quiffen/core/security.py +++ b/quiffen/core/security.py @@ -28,7 +28,7 @@ class Security(BaseModel): goal: Optional[str] = None line_number: Optional[int] = None - __CUSTOM_FIELDS: List[Field] = [] + __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __str__(self) -> str: return_str = 'Security:' diff --git a/quiffen/core/split.py b/quiffen/core/split.py index 90f5319..84d1d07 100644 --- a/quiffen/core/split.py +++ b/quiffen/core/split.py @@ -50,7 +50,7 @@ class Split(BaseModel): percent: Optional[Decimal] = None payee_address: Optional[str] = None - __CUSTOM_FIELDS: List[Field] = [] + __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __str__(self) -> str: properties = '' @@ -86,7 +86,8 @@ def to_qif( parent_class = cls break - qif += self.category.hierarchy + if self.category.hierarchy is not None: + qif += self.category.hierarchy if parent_class: qif += f'/{parent_class.name}' diff --git a/quiffen/core/transaction.py b/quiffen/core/transaction.py index 8b0fd08..2fda5c2 100644 --- a/quiffen/core/transaction.py +++ b/quiffen/core/transaction.py @@ -122,7 +122,7 @@ class Transaction(BaseModel): _split_categories: Dict[str, Category] = {} _last_split: Optional[Split] = None - __CUSTOM_FIELDS: List[Field] = [] + __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __str__(self) -> str: properties = '' @@ -167,7 +167,7 @@ def check_split_percentages_and_amounts( total_percent = sum( split.percent for split in splits if split.percent ) - total_amount = sum(split.amount for split in splits) + total_amount = sum(split.amount for split in splits if split.amount is not None) if total_percent - 100 > 0.01: raise ValueError( 'Split percentages cannot exceed 100% of the transaction' @@ -192,7 +192,7 @@ def add_split(self, split: Split) -> None: """Add a Split to Transaction.""" if ( split.percent - and sum(s.percent for s in self.splits) + split.percent - 100 > 0.01 + and sum(s.percent for s in self.splits if s.percent is not None) + split.percent - 100 > 0.01 ): raise ValueError( 'The sum of the split percentages cannot be greater than 100.' @@ -201,7 +201,7 @@ def add_split(self, split: Split) -> None: if split.amount: abs_sum_of_splits = abs( sum( - s.amount for s in self.splits + s.amount for s in self.splits if s.amount is not None ) + split.amount ) if abs_sum_of_splits - abs(self.amount) > 0.01: @@ -340,7 +340,7 @@ def from_list( kwargs: Dict[str, Any] = {} classes: Dict[str, Class] = {} splits: List[Split] = [] - current_split = None + current_split: Optional[Split] = None for field in lst: line_code, field_info = utils.parse_line_code_and_field_info(field) @@ -370,7 +370,7 @@ def from_list( transaction_date = utils.parse_date(field_info, day_first) if not splits: kwargs['date'] = transaction_date - else: + elif current_split: current_split.date = transaction_date elif line_code == 'E': if current_split is None: @@ -391,24 +391,24 @@ def from_list( amount = field_info.replace(',', '') if not splits: kwargs['amount'] = amount - else: + elif current_split: current_split.amount = Decimal(amount) elif line_code == 'M': if not splits: kwargs['memo'] = field_info - else: + elif current_split: current_split.memo = field_info elif line_code == 'C': if not splits: kwargs['cleared'] = field_info - else: + elif current_split: current_split.cleared = field_info elif line_code == 'P': kwargs['payee'] = field_info elif line_code == 'A': if not splits: kwargs['payee_address'] = field_info - else: + elif current_split: current_split.payee_address = field_info elif line_code == 'L': class_name, field_info, classes = ( @@ -423,7 +423,7 @@ def from_list( if field_info.startswith('['): if not splits: kwargs['to_account'] = field_info[1:-1] - else: + elif current_split: current_split.to_account = field_info[1:-1] else: category = create_categories_from_hierarchy(field_info) @@ -434,7 +434,7 @@ def from_list( if 'category' in kwargs: category_root.set_parent(kwargs['category']) kwargs['category'] = category - else: + elif current_split: category_root.set_parent(current_split.category) current_split.category = category @@ -443,7 +443,7 @@ def from_list( elif line_code == 'N': if not splits: kwargs['check_number'] = field_info - else: + elif current_split: current_split.check_number = field_info elif line_code == 'F': kwargs['reimbursable_expense'] = field_info or True @@ -474,12 +474,12 @@ def from_list( total = Decimal(kwargs.get('amount', 0)) if splits and total: for split in splits: - if split.percent is None: + if split.percent is None and split.amount is not None: split.percent = Decimal( round(split.amount / total * 100, 2) ) # Check if the split percentage is correct - elif split.amount is not None and not ( + elif split.percent is not None and split.amount is not None and not ( Decimal(round(split.percent, 2)) == Decimal( round( diff --git a/quiffen/utils.py b/quiffen/utils.py index c0db9f3..8599a1f 100644 --- a/quiffen/utils.py +++ b/quiffen/utils.py @@ -2,7 +2,7 @@ from datetime import date, datetime from decimal import Decimal from enum import Enum -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union from dateutil import parser from pydantic import ValidationError, parse_obj_as @@ -114,7 +114,7 @@ def convert_custom_fields_to_qif_string( def apply_csv_formatting_to_scalar( obj: Any, - date_format: str = '%Y-%m-%d', + date_format: Optional[str] = '%Y-%m-%d', ) -> Union[str, int, float]: """Apply CSV-friendly formatting to a scalar value""" if isinstance(obj, (datetime, date)) and date_format: @@ -132,7 +132,7 @@ def apply_csv_formatting_to_scalar( def apply_csv_formatting_to_container( obj: Union[List[Any], Dict[Any, Any]], - date_format: str = '%Y-%m-%d', + date_format: Optional[str] = '%Y-%m-%d', ) -> Union[List[Any], Dict[Any, Any], str, int, float]: """Recursively apply CSV-friendly formatting to a container""" if isinstance(obj, list): From 0ec22aeeb6cd9682b5d708f47111493d0a5182ae Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Sat, 4 Mar 2023 14:42:59 +0000 Subject: [PATCH 4/5] Blacken and Ruffle --- docs/conf.py | 25 +- quiffen/__init__.py | 6 +- quiffen/core/account.py | 131 ++++--- quiffen/core/base.py | 13 +- quiffen/core/category.py | 127 ++++--- quiffen/core/class_type.py | 23 +- quiffen/core/investment.py | 91 ++--- quiffen/core/qif.py | 197 +++++----- quiffen/core/security.py | 43 +-- quiffen/core/split.py | 66 ++-- quiffen/core/transaction.py | 271 +++++++------- quiffen/utils.py | 34 +- tests/test_account.py | 268 ++++++------- tests/test_base.py | 41 +- tests/test_category.py | 700 +++++++++++++++++----------------- tests/test_class_type.py | 142 ++++--- tests/test_investment.py | 178 ++++----- tests/test_qif.py | 608 +++++++++++++++--------------- tests/test_security.py | 232 ++++++------ tests/test_split.py | 156 ++++---- tests/test_transaction.py | 724 ++++++++++++++++++------------------ tests/test_utils.py | 144 +++---- 22 files changed, 2114 insertions(+), 2106 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9035da2..f42cc30 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,17 +12,18 @@ # import os import sys -sys.path.insert(0, os.path.abspath('../quiffen')) + +sys.path.insert(0, os.path.abspath("../quiffen")) # -- Project information ----------------------------------------------------- -project = 'Quiffen' -copyright = '2022, Isaac Harris-Holt' -author = 'Isaac Harris-Holt' +project = "Quiffen" +copyright = "2022, Isaac Harris-Holt" +author = "Isaac Harris-Holt" # The full version, including alpha/beta/rc tags -release = '2.0.0' +release = "2.0.0" # -- General configuration --------------------------------------------------- @@ -30,19 +31,15 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ - 'numpydoc', - 'sphinx.ext.autodoc', - 'sphinx.ext.coverage' -] +extensions = ["numpydoc", "sphinx.ext.autodoc", "sphinx.ext.coverage"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- @@ -50,9 +47,9 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] \ No newline at end of file +html_static_path = ["_static"] diff --git a/quiffen/__init__.py b/quiffen/__init__.py index 0dff073..f0f8628 100644 --- a/quiffen/__init__.py +++ b/quiffen/__init__.py @@ -18,8 +18,4 @@ from quiffen.core.qif import ParserException, Qif, QifDataType from quiffen.core.security import Security from quiffen.core.split import Split -from quiffen.core.transaction import ( - Transaction, - TransactionLike, - TransactionList, -) +from quiffen.core.transaction import Transaction, TransactionLike, TransactionList diff --git a/quiffen/core/account.py b/quiffen/core/account.py index ad47d96..9cfbbe2 100644 --- a/quiffen/core/account.py +++ b/quiffen/core/account.py @@ -8,21 +8,19 @@ from quiffen import utils from quiffen.core.base import BaseModel, Field from quiffen.core.class_type import Class -from quiffen.core.transaction import ( - TransactionLike, - TransactionList, -) +from quiffen.core.transaction import TransactionLike, TransactionList class AccountType(str, Enum): """An enum representing the different account types allowed by QIF.""" - CASH = 'Cash' - BANK = 'Bank' - CREDIT_CARD = 'CCard' - OTH_A = 'Oth A' - OTH_L = 'Oth L' - INVOICE = 'Invoice' - INVST = 'Invst' + + CASH = "Cash" + BANK = "Bank" + CREDIT_CARD = "CCard" + OTH_A = "Oth A" + OTH_L = "Oth L" + INVOICE = "Invoice" + INVST = "Invst" class Account(BaseModel): @@ -42,16 +40,31 @@ class Account(BaseModel): >>> acc.set_header(quiffen.AccountType.BANK) >>> acc.add_transaction(tr) >>> acc.transactions - {'Bank': [Transaction(date=datetime.datetime(2021, 7, 2, 18, 31, 47, 817025), amount=150.0)]} + { + 'Bank': [ + Transaction( + date=datetime.datetime(2021, 7, 2, 18, 31, 47, 817025), + amount=150.0, + ), + ], + } Creating an account instance from a section list in a QIF file. >>> import quiffen - >>> string = '!Account\\nNPersonal Bank Account\\nDMy Personal bank account with Barclays.\\n^\\n' + >>> string = ( + ... '!Account\\nNPersonal Bank Account\\n' + ... 'DMy Personal bank account with Barclays.\\n^\\n' + ... ) >>> acc = quiffen.Account.from_string(string) >>> acc - Account(name='Personal Bank Account', desc='My Personal bank account with Barclays.', ...) + Account( + name='Personal Bank Account', + desc='My Personal bank account with Barclays.', + ..., + ) """ + name: str desc: Optional[str] = None account_type: Optional[AccountType] = None @@ -66,30 +79,25 @@ class Account(BaseModel): def __eq__(self, other) -> bool: if not isinstance(other, Account): return False - return ( - self.name == other.name - and self.account_type == other.account_type - ) + return self.name == other.name and self.account_type == other.account_type def __str__(self) -> str: - properties = '' - ignore = ['_last_header'] - for (object_property, value) in self.__dict__.items(): + properties = "" + ignore = ["_last_header"] + for object_property, value in self.__dict__.items(): if value and object_property not in ignore: - if object_property == 'transactions': - num_transactions = sum( - len(value[header]) for header in value - ) - properties += f'\n\tTransactions: {num_transactions}' - elif object_property == 'account_type': - properties += f'\n\tAccount Type: {value.value}' + if object_property == "transactions": + num_transactions = sum(len(value[header]) for header in value) + properties += f"\n\tTransactions: {num_transactions}" + elif object_property == "account_type": + properties += f"\n\tAccount Type: {value.value}" else: properties += ( - f'\n\t' + f"\n\t" f'{object_property.replace("_", " ").strip().title()}: ' - f'{value}' + f"{value}" ) - return 'Account:' + properties + return "Account:" + properties def set_header(self, header: AccountType) -> None: """Set the last header used. @@ -122,19 +130,19 @@ def add_transaction( If there is no header provided, and no ``last_header`` set. """ if not header and not self._last_header: - raise RuntimeError('No header provided, and no last header set.') + raise RuntimeError("No header provided, and no last header set.") if not header: header = self._last_header else: try: - header = AccountType(header.strip().split(':')[-1]) + header = AccountType(header.strip().split(":")[-1]) self._last_header = header except ValueError as e: - raise ValueError('Header must be a valid AccountType.') from e + raise ValueError("Header must be a valid AccountType.") from e if not header: - raise RuntimeError('No header provided, and no last header set.') + raise RuntimeError("No header provided, and no last header set.") if header not in self.transactions: self.transactions[header] = [] @@ -161,9 +169,7 @@ def merge(self, other: Account) -> None: self.transactions[header].extend(other.transactions[header]) def to_qif( - self, - date_format: str = '%Y-%m-%d', - classes: Optional[Dict[str, Class]] = None + self, date_format: str = "%Y-%m-%d", classes: Optional[Dict[str, Class]] = None ) -> str: """Return a QIF-formatted string of this account. @@ -180,18 +186,18 @@ def to_qif( str A QIF-formatted string of this account. """ - qif = '!Account\n' - qif += f'N{self.name}\n' + qif = "!Account\n" + qif += f"N{self.name}\n" if self.desc: - qif += f'D{self.desc}\n' + qif += f"D{self.desc}\n" if self.account_type: - qif += f'T{self.account_type.value}\n' + qif += f"T{self.account_type.value}\n" if self.credit_limit: - qif += f'L{self.credit_limit}\n' + qif += f"L{self.credit_limit}\n" if self.balance: - qif += f'${self.balance}\n' + qif += f"${self.balance}\n" if self.date_at_balance: - qif += f'/{self.date_at_balance}\n' + qif += f"/{self.date_at_balance}\n" qif += utils.convert_custom_fields_to_qif_string( self._get_custom_fields(), @@ -199,12 +205,13 @@ def to_qif( ) for header, header_transactions in self.transactions.items(): - qif += f'^\n!Type:{header.value}\n' - qif += '^\n'.join( + qif += f"^\n!Type:{header.value}\n" + qif += "^\n".join( transaction.to_qif( date_format=date_format, classes=classes, - ) for transaction in header_transactions + ) + for transaction in header_transactions ) return qif @@ -236,23 +243,23 @@ def from_list(cls, lst: List[str], day_first: bool = False) -> Account: custom_fields=cls._get_custom_fields(), object_dict=kwargs, ) - if found or line_code == '!': + if found or line_code == "!": continue - if line_code == 'N': - kwargs['name'] = field_info - elif line_code == 'D': - kwargs['desc'] = field_info - elif line_code == 'T': - kwargs['account_type'] = field_info - elif line_code == 'L': - kwargs['credit_limit'] = field_info.replace(',', '') - elif line_code in {'$', '£'}: - kwargs['balance'] = field_info.replace(',', '') - elif line_code == '/': + if line_code == "N": + kwargs["name"] = field_info + elif line_code == "D": + kwargs["desc"] = field_info + elif line_code == "T": + kwargs["account_type"] = field_info + elif line_code == "L": + kwargs["credit_limit"] = field_info.replace(",", "") + elif line_code in {"$", "£"}: + kwargs["balance"] = field_info.replace(",", "") + elif line_code == "/": balance_date = utils.parse_date(field_info, day_first) - kwargs['date_at_balance'] = balance_date + kwargs["date_at_balance"] = balance_date else: - raise ValueError(f'Unknown line code: {line_code}') + raise ValueError(f"Unknown line code: {line_code}") return cls(**kwargs) diff --git a/quiffen/core/base.py b/quiffen/core/base.py index 86fb4d7..7973bf7 100644 --- a/quiffen/core/base.py +++ b/quiffen/core/base.py @@ -3,7 +3,7 @@ from pydantic import BaseModel as PydanticBaseModel -T = TypeVar('T') +T = TypeVar("T") class Field(PydanticBaseModel): @@ -22,6 +22,7 @@ class Field(PydanticBaseModel): The type of the custom field. This is the type that the value of the custom field will be converted to. """ + line_code: str attr: str type: Type @@ -39,7 +40,7 @@ def __lt__(self, other) -> bool: class BaseModel(PydanticBaseModel, Generic[T]): class Config: - extra = 'allow' + extra = "allow" __CUSTOM_FIELDS: List[Field] = [] # type: ignore @@ -65,7 +66,7 @@ def add_custom_field( The type of the extra field. This is the type that the value of the extra field will be converted to. """ - lst = getattr(cls, '__CUSTOM_FIELDS', []).copy() + lst = getattr(cls, "__CUSTOM_FIELDS", []).copy() new_field = Field(line_code=line_code, attr=attr, type=field_type) if new_field in lst: @@ -74,13 +75,13 @@ def add_custom_field( else: lst.append(new_field) - setattr(cls, '__CUSTOM_FIELDS', lst) + setattr(cls, "__CUSTOM_FIELDS", lst) @classmethod def _get_custom_fields(cls) -> List[Field]: """Return a list of the custom fields for the class, reverse ordered by line code length.""" - return sorted(getattr(cls, '__CUSTOM_FIELDS', []), reverse=True) + return sorted(getattr(cls, "__CUSTOM_FIELDS", []), reverse=True) @classmethod @abstractmethod @@ -88,7 +89,7 @@ def from_list(cls, lst: List[str]) -> T: pass @classmethod - def from_string(cls, string: str, separator: str = '\n') -> T: + def from_string(cls, string: str, separator: str = "\n") -> T: """Create a class instance from a string.""" return cls.from_list(string.split(separator)) diff --git a/quiffen/core/category.py b/quiffen/core/category.py index 9384771..983a92d 100644 --- a/quiffen/core/category.py +++ b/quiffen/core/category.py @@ -12,8 +12,9 @@ class CategoryType(str, Enum): """Enum representing the different types of categories in a QIF file.""" - EXPENSE = 'expense' - INCOME = 'income' + + EXPENSE = "expense" + INCOME = "income" class Category(BaseModel): @@ -74,7 +75,12 @@ class Category(BaseModel): └─ Meat └─ Chicken >>> meat - Category(name='Meat', expense=True, parent=Category(name='Food', expense=True, hierarchy='Food'), hierarchy='Food:Meat') + Category( + name='Meat', + expense=True, + parent=Category(name='Food', expense=True, hierarchy='Food'), + hierarchy='Food:Meat', + ) >>> food.remove_child(meat, keep_children=True) >>> print(food.render_tree()) Food (root) @@ -86,6 +92,7 @@ class Category(BaseModel): Food (root) └─ Chicken """ + name: str desc: Optional[str] = None tax_related: Optional[bool] = None @@ -99,38 +106,38 @@ class Category(BaseModel): __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __str__(self) -> str: - properties = '' - for (object_property, value) in self.__dict__.items(): + properties = "" + for object_property, value in self.__dict__.items(): if value: - if object_property == 'parent': + if object_property == "parent": properties += ( f'\n\tParent: {self.parent.name if self.parent else "None"}' ) - elif object_property == 'children': - properties += f'\n\tChildren: {len(self.children)}' - elif object_property == 'category_type': - properties += f'\n\tCategory Type: {value.value}' + elif object_property == "children": + properties += f"\n\tChildren: {len(self.children)}" + elif object_property == "category_type": + properties += f"\n\tCategory Type: {value.value}" else: properties += ( - f'\n\t' + f"\n\t" f'{object_property.replace("_", " ").strip().title()}: ' - f'{value}' + f"{value}" ) - return 'Category:' + properties + return "Category:" + properties def __lt__(self, other) -> bool: return self.name < other.name - @validator('hierarchy', pre=True, always=True) + @validator("hierarchy", pre=True, always=True) def _set_hierarchy(cls, v: str, values) -> str: if not v: - return values['name'] + return values["name"] - if values.get('parent', None) and v != values['name']: - raise ValueError('Hierarchy must match name if no parent is set.') + if values.get("parent", None) and v != values["name"]: + raise ValueError("Hierarchy must match name if no parent is set.") - if not v.endswith(values['name']): - raise ValueError('Hierarchy must end with name.', v, values['name']) + if not v.endswith(values["name"]): + raise ValueError("Hierarchy must end with name.", v, values["name"]) return v @@ -138,7 +145,9 @@ def _refresh_hierarchy(self) -> None: """Refreshes the hierarchy of the current category and all its children recursively.""" for child in self.children: - child.hierarchy = self.hierarchy + ':' + child.name if self.hierarchy else child.name + child.hierarchy = ( + self.hierarchy + ":" + child.name if self.hierarchy else child.name + ) child._refresh_hierarchy() def dict(self, exclude: Optional[Iterable[str]] = None, **_) -> Dict[str, Any]: @@ -157,21 +166,23 @@ def dict(self, exclude: Optional[Iterable[str]] = None, **_) -> Dict[str, Any]: exclude = [] res = { - key.strip('_'): value + key.strip("_"): value for (key, value) in self.__dict__.items() - if key.strip('_') not in exclude + if key.strip("_") not in exclude } - if self.children and 'children' not in exclude: - res['children'] = [category.name for category in self.children] + if self.children and "children" not in exclude: + res["children"] = [category.name for category in self.children] - if self.parent and 'parent' not in exclude: - res['parent'] = self.parent.name + if self.parent and "parent" not in exclude: + res["parent"] = self.parent.name return res # This is kept for backwards compatibility - def to_dict(self, ignore: Optional[Iterable[str]] = None, **kwargs) -> Dict[str, Any]: + def to_dict( + self, ignore: Optional[Iterable[str]] = None, **kwargs + ) -> Dict[str, Any]: """Return a representation of the Category object as a dict. Parameters @@ -362,16 +373,16 @@ def render_tree(self, _level: int = 0) -> str: return self.name if self.parent is None: - is_root_str = ' (root)' + is_root_str = " (root)" else: - is_root_str = '' + is_root_str = "" return ( self.name - + f'{is_root_str}\n' - + '\n'.join( + + f"{is_root_str}\n" + + "\n".join( [ - ' ' * _level + '└─ ' + child.render_tree(_level + 1) + " " * _level + "└─ " + child.render_tree(_level + 1) for child in self.children ] ) @@ -379,22 +390,22 @@ def render_tree(self, _level: int = 0) -> str: def _to_qif_string(self) -> str: """Return a QIF representation of the individual Category object.""" - qif = f'!Type:Cat\nN{self.hierarchy}\n' + qif = f"!Type:Cat\nN{self.hierarchy}\n" if self.desc: - qif += f'D{self.desc}\n' + qif += f"D{self.desc}\n" if self.tax_related is not None: - qif += f'T{self.tax_related}\n' + qif += f"T{self.tax_related}\n" if self.category_type == CategoryType.INCOME: - qif += 'I\n' + qif += "I\n" elif self.category_type == CategoryType.EXPENSE: - qif += 'E\n' + qif += "E\n" if self.budget_amount: - qif += f'B{self.budget_amount}\n' + qif += f"B{self.budget_amount}\n" if self.tax_schedule_info: - qif += f'R{self.tax_schedule_info}\n' + qif += f"R{self.tax_schedule_info}\n" qif += utils.convert_custom_fields_to_qif_string( self._get_custom_fields(), @@ -405,9 +416,7 @@ def _to_qif_string(self) -> str: def to_qif(self) -> str: """Return a QIF representation of all the categories in the tree.""" - return '^\n'.join( - [c._to_qif_string() for c in self.traverse_down()] - ) + return "^\n".join([c._to_qif_string() for c in self.traverse_down()]) @classmethod def from_list(cls, lst: List[str]) -> Category: @@ -434,27 +443,27 @@ def from_list(cls, lst: List[str]) -> Category: if found: continue - if line_code == 'N': + if line_code == "N": temp_cat = create_categories_from_hierarchy(field_info) - kwargs['name'] = temp_cat.name - kwargs['hierarchy'] = temp_cat.hierarchy + kwargs["name"] = temp_cat.name + kwargs["hierarchy"] = temp_cat.hierarchy new_parent = temp_cat.parent if new_parent: new_parent.remove_child(temp_cat) - elif line_code == 'D': - kwargs['desc'] = field_info - elif line_code == 'T': - kwargs['tax_related'] = field_info or True - elif line_code == 'E': - kwargs['category_type'] = CategoryType.EXPENSE - elif line_code == 'I': - kwargs['category_type'] = CategoryType.INCOME - elif line_code == 'B': - kwargs['budget_amount'] = field_info.replace(',', '') - elif line_code == 'R': - kwargs['tax_schedule_info'] = field_info + elif line_code == "D": + kwargs["desc"] = field_info + elif line_code == "T": + kwargs["tax_related"] = field_info or True + elif line_code == "E": + kwargs["category_type"] = CategoryType.EXPENSE + elif line_code == "I": + kwargs["category_type"] = CategoryType.INCOME + elif line_code == "B": + kwargs["budget_amount"] = field_info.replace(",", "") + elif line_code == "R": + kwargs["tax_schedule_info"] = field_info else: - raise ValueError(f'Unknown line code: {line_code}') + raise ValueError(f"Unknown line code: {line_code}") new_cat = cls(**kwargs) new_cat.set_parent(new_parent) @@ -468,7 +477,7 @@ def create_categories_from_hierarchy(hierarchy: str) -> Category: """Create a Category instance from a QIF hierarchy string. Returns the lowest category of the hierarchy. """ - categories = hierarchy.split(':') + categories = hierarchy.split(":") root_category = Category(name=categories[0]) current_category = root_category diff --git a/quiffen/core/class_type.py b/quiffen/core/class_type.py index c1b0ce7..6d35e2a 100644 --- a/quiffen/core/class_type.py +++ b/quiffen/core/class_type.py @@ -20,6 +20,7 @@ class Class(BaseModel): desc : str, default=None The description of the class. """ + name: str desc: Optional[str] = None categories: List[Category] = [] @@ -32,10 +33,10 @@ def __eq__(self, other) -> bool: return self.name == other.name def __str__(self) -> str: - res = f'Class:\n\tName: {self.name}' + res = f"Class:\n\tName: {self.name}" if self.desc: - res += f'\n\tDescription: {self.desc}' - res += f'\n\tCategories: {len(self.categories)}' + res += f"\n\tDescription: {self.desc}" + res += f"\n\tCategories: {len(self.categories)}" return res def add_category(self, new_category: Category) -> None: @@ -56,10 +57,10 @@ def merge(self, other: Class) -> None: def to_qif(self) -> str: """Return a QIF-formatted string of this class.""" - qif = '!Type:Class\n' - qif += f'N{self.name}\n' + qif = "!Type:Class\n" + qif += f"N{self.name}\n" if self.desc: - qif += f'D{self.desc}\n' + qif += f"D{self.desc}\n" qif += utils.convert_custom_fields_to_qif_string( self._get_custom_fields(), @@ -92,11 +93,11 @@ def from_list(cls, lst: List[str]) -> Class: if found: continue - if line_code == 'N': - kwargs['name'] = field_info - elif line_code == 'D': - kwargs['desc'] = field_info + if line_code == "N": + kwargs["name"] = field_info + elif line_code == "D": + kwargs["desc"] = field_info else: - raise ValueError(f'Unknown line code: {line_code}') + raise ValueError(f"Unknown line code: {line_code}") return cls(**kwargs) diff --git a/quiffen/core/investment.py b/quiffen/core/investment.py index ef9a7c3..f85389d 100644 --- a/quiffen/core/investment.py +++ b/quiffen/core/investment.py @@ -45,6 +45,7 @@ class Investment(BaseModel): line_number : int, default=None The line number of the investment in the QIF file. """ + date: datetime action: Optional[str] = None security: Optional[str] = None @@ -62,47 +63,47 @@ class Investment(BaseModel): __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __str__(self) -> str: - properties = '' - for (object_property, value) in self.__dict__.items(): + properties = "" + for object_property, value in self.__dict__.items(): if value: properties += ( - f'\n\t' + f"\n\t" f'{object_property.replace("_", " ").strip().title()}: ' - f'{value}' + f"{value}" ) - return 'Investment:' + properties + return "Investment:" + properties def to_qif( self, - date_format: str = '%Y-%m-%d', + date_format: str = "%Y-%m-%d", **_, # To keep the same signature as Transaction.to_qif ) -> str: """Converts an Investment to a QIF string""" - qif = f'D{self.date.strftime(date_format)}\n' + qif = f"D{self.date.strftime(date_format)}\n" if self.action: - qif += f'N{self.action}\n' + qif += f"N{self.action}\n" if self.security: - qif += f'Y{self.security}\n' + qif += f"Y{self.security}\n" if self.price: - qif += f'I{self.price}\n' + qif += f"I{self.price}\n" if self.quantity: - qif += f'Q{self.quantity}\n' + qif += f"Q{self.quantity}\n" if self.cleared: - qif += f'C{self.cleared}\n' + qif += f"C{self.cleared}\n" if self.amount: - qif += f'T{self.amount}\n' + qif += f"T{self.amount}\n" if self.memo: - qif += f'M{self.memo}\n' + qif += f"M{self.memo}\n" if self.first_line: - qif += f'P{self.first_line}\n' + qif += f"P{self.first_line}\n" if self.to_account: - qif += f'L{self.to_account}\n' + qif += f"L{self.to_account}\n" if self.transfer_amount: - qif += f'${self.transfer_amount}\n' + qif += f"${self.transfer_amount}\n" if self.commission: - qif += f'O{self.commission}\n' + qif += f"O{self.commission}\n" qif += utils.convert_custom_fields_to_qif_string( self._get_custom_fields(), @@ -152,36 +153,36 @@ def from_list( # Check the QIF line code for banking-related operations, then # append to kwargs. - if line_code == 'D': + if line_code == "D": transaction_date = utils.parse_date(field_info, day_first) - kwargs['date'] = transaction_date - elif line_code == 'N': - kwargs['action'] = field_info - elif line_code == 'Y': - kwargs['security'] = field_info - elif line_code == 'I': - kwargs['price'] = field_info.replace(',', '') - elif line_code == 'Q': - kwargs['quantity'] = field_info.replace(',', '') - elif line_code == 'C': - kwargs['cleared'] = field_info - elif line_code in {'T', 'U'}: - kwargs['amount'] = field_info.replace(',', '') - elif line_code == 'M': - kwargs['memo'] = field_info - elif line_code == 'P': - kwargs['first_line'] = field_info - elif line_code == 'L': - kwargs['to_account'] = field_info - elif line_code == '$': - kwargs['transfer_amount'] = field_info.replace(',', '') - elif line_code == 'O': - kwargs['commission'] = field_info.replace(',', '') + kwargs["date"] = transaction_date + elif line_code == "N": + kwargs["action"] = field_info + elif line_code == "Y": + kwargs["security"] = field_info + elif line_code == "I": + kwargs["price"] = field_info.replace(",", "") + elif line_code == "Q": + kwargs["quantity"] = field_info.replace(",", "") + elif line_code == "C": + kwargs["cleared"] = field_info + elif line_code in {"T", "U"}: + kwargs["amount"] = field_info.replace(",", "") + elif line_code == "M": + kwargs["memo"] = field_info + elif line_code == "P": + kwargs["first_line"] = field_info + elif line_code == "L": + kwargs["to_account"] = field_info + elif line_code == "$": + kwargs["transfer_amount"] = field_info.replace(",", "") + elif line_code == "O": + kwargs["commission"] = field_info.replace(",", "") else: - raise ValueError(f'Unknown line code: {line_code}') + raise ValueError(f"Unknown line code: {line_code}") if line_number is not None: - kwargs['line_number'] = line_number + kwargs["line_number"] = line_number return cls(**kwargs) @@ -189,7 +190,7 @@ def from_list( def from_string( cls, string: str, - separator: str = '\n', + separator: str = "\n", day_first: bool = False, line_number: Optional[int] = None, ) -> Investment: diff --git a/quiffen/core/qif.py b/quiffen/core/qif.py index 3588687..3437a66 100644 --- a/quiffen/core/qif.py +++ b/quiffen/core/qif.py @@ -14,34 +14,36 @@ from quiffen.core.category import Category, add_categories_to_container from quiffen.core.class_type import Class from quiffen.core.investment import Investment -from quiffen.core.transaction import Transaction from quiffen.core.security import Security +from quiffen.core.transaction import Transaction try: import pandas as pd + PANDAS_INSTALLED = True except ModuleNotFoundError: PANDAS_INSTALLED = False VALID_TRANSACTION_ACCOUNT_TYPES = [ - '!type:cash', - '!type:bank', - '!type:ccard', - '!type:otha', - '!type:othl', - '!type:invoice' + "!type:cash", + "!type:bank", + "!type:ccard", + "!type:otha", + "!type:othl", + "!type:invoice", ] class QifDataType(str, Enum): """An Enum representing the different types of data that can be found in a Qif object.""" - TRANSACTIONS = 'transactions' - INVESTMENTS = 'investments' - CLASSES = 'classes' - CATEGORIES = 'categories' - ACCOUNTS = 'accounts' - SECURITIES = 'securities' + + TRANSACTIONS = "transactions" + INVESTMENTS = "investments" + CLASSES = "classes" + CATEGORIES = "categories" + ACCOUNTS = "accounts" + SECURITIES = "securities" class ParserException(Exception): @@ -63,6 +65,7 @@ class Qif(BaseModel): classes : dict, default=None A dict of classes in the form {'Class Name': class_object}. """ + accounts: Dict[str, Account] = {} categories: Dict[str, Category] = {} classes: Dict[str, Class] = {} @@ -71,40 +74,37 @@ class Qif(BaseModel): __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __str__(self) -> str: - accounts_str = '\n'.join(str(acc) for acc in self.accounts.values()) - categories_str = '\n'.join(str(cat) for cat in self.categories.values()) - classes_str = '\n'.join(str(cls) for cls in self.classes.values()) - securities_str = '\n'.join(str(sec) for sec in self.securities.values()) + accounts_str = "\n".join(str(acc) for acc in self.accounts.values()) + categories_str = "\n".join(str(cat) for cat in self.categories.values()) + classes_str = "\n".join(str(cls) for cls in self.classes.values()) + securities_str = "\n".join(str(sec) for sec in self.securities.values()) if not (accounts_str or categories_str or classes_str): - return 'Empty Qif object' + return "Empty Qif object" - return_str = 'QIF\n===\n\n' + return_str = "QIF\n===\n\n" if accounts_str: - return_str += f'Accounts\n--------\n\n{accounts_str}\n\n' + return_str += f"Accounts\n--------\n\n{accounts_str}\n\n" if categories_str: - return_str += f'Categories\n----------\n\n{categories_str}\n\n' + return_str += f"Categories\n----------\n\n{categories_str}\n\n" if classes_str: - return_str += f'Classes\n-------\n\n{classes_str}\n\n' + return_str += f"Classes\n-------\n\n{classes_str}\n\n" if securities_str: - return_str += f'Securities\n----------\n\n{securities_str}\n\n' + return_str += f"Securities\n----------\n\n{securities_str}\n\n" return return_str @classmethod def from_list(cls, lst: List[str]) -> Qif: raise NotImplementedError( - 'This method is not implemented for Qif objects. Use Qif.parse to ' - 'parse a QIF file.' + "This method is not implemented for Qif objects. Use Qif.parse to " + "parse a QIF file." ) @classmethod def parse( - cls, - path: Union[FilePath, str], - separator: str = '\n', - day_first: bool = False + cls, path: Union[FilePath, str], separator: str = "\n", day_first: bool = False ) -> Qif: """Return a class instance from a QIF file. @@ -124,16 +124,16 @@ def parse( A Qif object containing all the data in the QIF file. """ path = Path(path) - if path.suffix.lower() != '.qif': - raise ParserException('The file must be a QIF file.') + if path.suffix.lower() != ".qif": + raise ParserException("The file must be a QIF file.") if not path.exists(): - raise ParserException('The file does not exist.') + raise ParserException("The file does not exist.") - data = path.read_text(encoding='utf-8').strip().strip('\n') + data = path.read_text(encoding="utf-8").strip().strip("\n") if not data: - raise ParserException('The file is empty.') + raise ParserException("The file is empty.") accounts: Dict[str, Account] = {} last_account = None @@ -141,7 +141,7 @@ def parse( classes: Dict[str, Class] = {} securities: Dict[str, Security] = {} - sections = data.split('^') + sections = data.split("^") last_header = None line_number = 1 @@ -156,7 +156,7 @@ def parse( # Allow for comments and blank lines at the top of sections for i, line in enumerate(section_lines): header_line = line - if line.strip() and line[0] != '#': + if line.strip() and line[0] != "#": line_number += i section_lines = section_lines[i:] break @@ -164,19 +164,19 @@ def parse( # Empty section continue - if header_line[0] != '!': + if header_line[0] != "!": if not last_header: raise ParserException( - f'Line {line_number}: ' - f'No header found before transactions.' + f"Line {line_number}: " f"No header found before transactions." ) header_line = last_header last_header = header_line sanitised_section_lines = [ - line for line in section_lines - if line.strip() and line.strip()[0] != '!' + line + for line in section_lines + if line.strip() and line.strip()[0] != "!" ] if not sanitised_section_lines: @@ -184,27 +184,27 @@ def parse( # Check for new categories and accounts first, otherwise it's a # transaction so a default account is created - if '!Type:Cat' in header_line: + if "!Type:Cat" in header_line: # Section contains category information new_category = Category.from_list(sanitised_section_lines) categories = add_categories_to_container( # type: ignore new_category, categories, ) - elif '!Type:Class' in header_line: + elif "!Type:Class" in header_line: new_class = Class.from_list(sanitised_section_lines) if new_class.name in classes: classes[new_class.name].merge(new_class) else: classes[new_class.name] = new_class - elif '!Account' in header_line: + elif "!Account" in header_line: new_account = Account.from_list(sanitised_section_lines) if new_account.name in accounts: accounts[new_account.name].merge(new_account) else: accounts[new_account.name] = new_account last_account = new_account.name - elif '!Type:Invst' in header_line: + elif "!Type:Invst" in header_line: # Investment new_investment = Investment.from_list( sanitised_section_lines, @@ -214,16 +214,16 @@ def parse( if last_account is None: raise ParserException( - f'Line {line_number}: ' - 'No account found before investment. ' - 'This should not happen.' + f"Line {line_number}: " + "No account found before investment. " + "This should not happen." ) accounts[last_account].add_transaction( new_investment, - AccountType('Invst'), + AccountType("Invst"), ) - elif '!Type:Security' in header_line: + elif "!Type:Security" in header_line: # Security new_security = Security.from_list( sanitised_section_lines, @@ -231,24 +231,23 @@ def parse( ) if new_security.symbol is None: raise ParserException( - f'Line {line_number}: ' - f'No symbol found for security.' + f"Line {line_number}: " f"No symbol found for security." ) securities[new_security.symbol] = new_security - elif '!Type' in header_line and not accounts: + elif "!Type" in header_line and not accounts: # Accounts is empty and there's a transaction, so create default # account to put transactions in default_account = Account( - name='Quiffen Default Account', + name="Quiffen Default Account", desc=( - 'The default account created by Quiffen when no other ' - 'accounts were present' - ) + "The default account created by Quiffen when no other " + "accounts were present" + ), ) accounts[default_account.name] = default_account last_account = default_account.name - if header_line.lower().replace(' ', '') in ( + if header_line.lower().replace(" ", "") in ( VALID_TRANSACTION_ACCOUNT_TYPES ): # Other transaction type @@ -260,14 +259,14 @@ def parse( if last_account is None: raise ParserException( - f'Line {line_number}: ' - 'No account found before transactions. ' - 'This should not happen.' + f"Line {line_number}: " + "No account found before transactions. " + "This should not happen." ) accounts[last_account].add_transaction( new_transaction, - AccountType(header_line.split(':')[1]), + AccountType(header_line.split(":")[1]), ) if new_transaction.category: @@ -283,7 +282,7 @@ def parse( else: classes[class_name] = new_class - line_number += len(section.split('\n')) + line_number += len(section.split("\n")) return cls( accounts=accounts, @@ -329,7 +328,7 @@ def remove_category( ) from e if keep_children: - print('Keeping children') + print("Keeping children") for child in category.children: child.set_parent(None) self.add_category(child) @@ -356,7 +355,7 @@ def add_security(self, new_security: Security) -> None: """Add a new security to the Qif object""" if not new_security.symbol: raise ValueError( - 'Cannot add a security without a symbol to the Qif object.' + "Cannot add a security without a symbol to the Qif object." ) if new_security.symbol in self.securities: @@ -370,45 +369,44 @@ def remove_security(self, security_symbol: str) -> Security: return self.securities.pop(security_symbol) except KeyError as e: raise KeyError( - f'Security "{security_symbol}" does not exist in this Qif ' - f'object.' + f'Security "{security_symbol}" does not exist in this Qif ' f"object." ) from e def to_qif( self, path: Optional[Union[FilePath, str, None]] = None, - date_format: str = '%Y-%m-%d', + date_format: str = "%Y-%m-%d", ) -> str: """Convert the Qif object to a QIF file""" - qif = '' + qif = "" if self.categories: - qif += '^\n'.join( - category.to_qif() - for category in self.categories.values() - ) + '^\n' + qif += ( + "^\n".join(category.to_qif() for category in self.categories.values()) + + "^\n" + ) if self.classes: - qif += '^\n'.join( - cls.to_qif() - for cls in self.classes.values() - ) + '^\n' + qif += "^\n".join(cls.to_qif() for cls in self.classes.values()) + "^\n" if self.accounts: - qif += '^\n'.join( - account.to_qif(date_format=date_format, classes=self.classes) - for account in self.accounts.values() - ) + '^\n' + qif += ( + "^\n".join( + account.to_qif(date_format=date_format, classes=self.classes) + for account in self.accounts.values() + ) + + "^\n" + ) if path: - Path(path).write_text(qif, encoding='utf-8') + Path(path).write_text(qif, encoding="utf-8") return qif def _get_data_dicts( self, data_type: QifDataType = QifDataType.TRANSACTIONS, - date_format: Optional[str] = '%Y-%m-%d', + date_format: Optional[str] = "%Y-%m-%d", ignore: Optional[List[str]] = None, ) -> List[Dict[str, Any]]: """Converts specified data from the Qif object to a list of dicts""" @@ -435,36 +433,33 @@ def _get_data_dicts( ] elif data_type == QifDataType.ACCOUNTS: data_dicts = [ - account.to_dict(ignore=ignore) - for account in self.accounts.values() + account.to_dict(ignore=ignore) for account in self.accounts.values() ] elif data_type == QifDataType.CATEGORIES: data_dicts = [ - category.to_dict(ignore=ignore) - for category in self.categories.values() + category.to_dict(ignore=ignore) for category in self.categories.values() ] elif data_type == QifDataType.CLASSES: data_dicts = [ - class_.to_dict(ignore=ignore) - for class_ in self.classes.values() + class_.to_dict(ignore=ignore) for class_ in self.classes.values() ] elif data_type == QifDataType.SECURITIES: data_dicts = [ - security.to_dict(ignore=ignore) - for security in self.securities.values() + security.to_dict(ignore=ignore) for security in self.securities.values() ] else: raise ValueError( - f'Invalid data_type: {data_type}. Must be one of ' - f'{list(QifDataType)}' + f"Invalid data_type: {data_type}. Must be one of " + f"{list(QifDataType)}" ) # Format and hide private fields return [ # type: ignore utils.apply_csv_formatting_to_container( { - k: v for k, v in data_dict.items() - if not k.startswith('_') and k not in ignore + k: v + for k, v in data_dict.items() + if not k.startswith("_") and k not in ignore }, date_format=date_format, ) @@ -475,9 +470,9 @@ def to_csv( self, path: Optional[Union[FilePath, str, None]] = None, data_type: QifDataType = QifDataType.TRANSACTIONS, - date_format: str = '%Y-%m-%d', + date_format: str = "%Y-%m-%d", ignore: Optional[List[str]] = None, - delimiter: str = ',', + delimiter: str = ",", quote_character: str = '"', ) -> str: """Convert part of the Qif object to a CSV file. The data_type @@ -521,8 +516,8 @@ def to_csv( writer = csv.DictWriter( output, fieldnames=headers, - extrasaction='ignore', # Ignore extra fields (e.g. private fields) - dialect='unix', # Use Unix line endings + extrasaction="ignore", # Ignore extra fields (e.g. private fields) + dialect="unix", # Use Unix line endings delimiter=delimiter, quotechar=quote_character, ) @@ -533,7 +528,7 @@ def to_csv( return_value = output.getvalue() if path: - Path(path).write_text(return_value, encoding='utf-8') + Path(path).write_text(return_value, encoding="utf-8") return return_value @@ -563,7 +558,7 @@ def to_dataframe( """ if not PANDAS_INSTALLED: raise ModuleNotFoundError( - 'The pandas module must be installed to use this method' + "The pandas module must be installed to use this method" ) if ignore is None: diff --git a/quiffen/core/security.py b/quiffen/core/security.py index 6b31038..9ce7099 100644 --- a/quiffen/core/security.py +++ b/quiffen/core/security.py @@ -22,6 +22,7 @@ class Security(BaseModel): line_number : int The line number of the security in the QIF file """ + name: Optional[str] = None symbol: Optional[str] = None type: Optional[str] = None @@ -31,15 +32,15 @@ class Security(BaseModel): __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __str__(self) -> str: - return_str = 'Security:' + return_str = "Security:" if self.name: - return_str += f'\n\tName: {self.name}' + return_str += f"\n\tName: {self.name}" if self.symbol: - return_str += f'\n\tSymbol: {self.symbol}' + return_str += f"\n\tSymbol: {self.symbol}" if self.type: - return_str += f'\n\tType: {self.type}' + return_str += f"\n\tType: {self.type}" if self.goal: - return_str += f'\n\tGoal: {self.goal}' + return_str += f"\n\tGoal: {self.goal}" return return_str @@ -58,16 +59,16 @@ def merge(self, other: Security) -> None: def to_qif(self) -> str: """Converts a Security to a QIF string""" - qif = '!Type:Security\n' + qif = "!Type:Security\n" if self.name: - qif += f'N{self.name}\n' + qif += f"N{self.name}\n" if self.symbol: - qif += f'S{self.symbol}\n' + qif += f"S{self.symbol}\n" if self.type: - qif += f'T{self.type}\n' + qif += f"T{self.type}\n" if self.goal: - qif += f'G{self.goal}\n' + qif += f"G{self.goal}\n" qif += utils.convert_custom_fields_to_qif_string( self._get_custom_fields(), @@ -113,19 +114,19 @@ def from_list( if found: continue - if line_code == 'N': - kwargs['name'] = field_info - elif line_code == 'S': - kwargs['symbol'] = field_info - elif line_code == 'T': - kwargs['type'] = field_info - elif line_code == 'G': - kwargs['goal'] = field_info + if line_code == "N": + kwargs["name"] = field_info + elif line_code == "S": + kwargs["symbol"] = field_info + elif line_code == "T": + kwargs["type"] = field_info + elif line_code == "G": + kwargs["goal"] = field_info else: - raise ValueError(f'Unknown line code: {line_code}') + raise ValueError(f"Unknown line code: {line_code}") if line_number is not None: - kwargs['line_number'] = line_number + kwargs["line_number"] = line_number return cls(**kwargs) @@ -133,7 +134,7 @@ def from_list( def from_string( cls, string: str, - separator: str = '\n', + separator: str = "\n", line_number: Optional[int] = None, ) -> Security: """Return a class instance from a QIF string. diff --git a/quiffen/core/split.py b/quiffen/core/split.py index 84d1d07..e8745d1 100644 --- a/quiffen/core/split.py +++ b/quiffen/core/split.py @@ -17,7 +17,8 @@ class Split(BaseModel): Examples -------- - Adding Splits to a transaction to show that there were two categories that represent the transaction. + Adding Splits to a transaction to show that there were two categories that represent + the transaction. >>> import quiffen >>> from datetime import datetime @@ -34,12 +35,27 @@ class Split(BaseModel): Amount: 150.6 Splits: 2 total split(s) >>> print(tr.splits) - [Split(amount=50, category=Category(name='Beauty', expense=True, hierarchy='Beauty')), Split(amount=100.6, - category=Category(name='Electrical', expense=True, hierarchy='Electrical'))] - >>> tr.remove_split(amount=50) + [ + Split( + amount=50, + category=Category(name='Beauty', expense=True, hierarchy='Beauty'), + ), + Split( + amount=100.6, + category=Category(name='Electrical', expense=True, hierarchy='Electrical'), + ), + ] + >>> tr.remove_splits(amount=50) + [...] >>> print(tr.splits) - [Split(amount=100.6, category=Category(name='Electrical', expense=True, hierarchy='Electrical'))] + [ + Split( + amount=100.6, + category=Category(name='Electrical', expense=True, hierarchy='Electrical'), + ), + ] """ + date: Optional[datetime] = None amount: Optional[Decimal] = None memo: Optional[str] = None @@ -53,30 +69,30 @@ class Split(BaseModel): __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __str__(self) -> str: - properties = '' - for (object_property, value) in self.dict().items(): + properties = "" + for object_property, value in self.dict().items(): if value: - if object_property == 'category': + if object_property == "category": properties += f'\n\t\tCategory: {value["name"]}' else: properties += ( - f'\n\t\t' + f"\n\t\t" f'{object_property.replace("_", " ").strip().title()}' - f': {value}' + f": {value}" ) - return '\n\tSplit:' + properties + return "\n\tSplit:" + properties def to_qif( self, - date_format: str = '%Y-%m-%d', + date_format: str = "%Y-%m-%d", classes: Optional[Dict[str, Class]] = None, ) -> str: """Returns a QIF string representation of the split.""" if classes is None: classes = {} - qif = 'S' + qif = "S" if self.category: parent_class = None @@ -90,26 +106,26 @@ def to_qif( qif += self.category.hierarchy if parent_class: - qif += f'/{parent_class.name}' + qif += f"/{parent_class.name}" - qif += '\n' + qif += "\n" if self.date: - qif += f'D{self.date.strftime(date_format)}\n' + qif += f"D{self.date.strftime(date_format)}\n" if self.amount: - qif += f'${self.amount}\n' + qif += f"${self.amount}\n" if self.memo: - qif += f'E{self.memo}\n' + qif += f"E{self.memo}\n" if self.cleared: - qif += f'C{self.cleared}\n' + qif += f"C{self.cleared}\n" if self.to_account: - qif += f'L[{self.to_account}]\n' + qif += f"L[{self.to_account}]\n" if self.check_number: - qif += f'N{self.check_number}\n' + qif += f"N{self.check_number}\n" if self.percent: - qif += f'%{self.percent}%\n' + qif += f"%{self.percent}%\n" if self.payee_address: - qif += f'A{self.payee_address}\n' + qif += f"A{self.payee_address}\n" qif += utils.convert_custom_fields_to_qif_string( self._get_custom_fields(), @@ -120,6 +136,4 @@ def to_qif( @classmethod def from_list(cls, lst: List[str]) -> Split: - raise RuntimeError( - 'Splits can only be created in the context of a transaction' - ) + raise RuntimeError("Splits can only be created in the context of a transaction") diff --git a/quiffen/core/transaction.py b/quiffen/core/transaction.py index 2fda5c2..c68da72 100644 --- a/quiffen/core/transaction.py +++ b/quiffen/core/transaction.py @@ -86,19 +86,35 @@ class Transaction(BaseModel): >>> import decimal >>> from datetime import datetime >>> cat = quiffen.Category('Finances') - >>> tr = quiffen.Transaction(date=datetime.now(), amount=decimal.Decimal(150.60), category=cat) + >>> tr = quiffen.Transaction( + ... date=datetime.now(), + ... amount=decimal.Decimal(150.60), + ... category=cat, + ... ) >>> tr - Transaction(date=datetime.datetime(2021, 7, 5, 10, 45, 40, 48195), amount=150.6, category=Category(name='Finances', - expense=True, hierarchy='Finances')) + Transaction( + date=datetime.datetime(2021, 7, 5, 10, 45, 40, 48195), + amount=150.6, + category=Category(name='Finances',expense=True, hierarchy='Finances'), + ) >>> print(tr) Transaction: Date: 2021-07-05 10:45:40.048195 Amount: 150.6 Category: Finances >>> tr.to_dict(ignore=['date'], dictify_category=True) - {'amount': 150.6, 'category': {'name': 'Finances', 'expense': True, 'income': False, 'hierarchy': 'Finances', - 'children': []}} + { + 'amount': 150.6, + 'category': { + 'name': 'Finances', + 'expense': True, + 'income': False, + 'hierarchy': 'Finances', + 'children': [], + }, + } """ + date: datetime amount: Decimal memo: Optional[str] = None @@ -125,56 +141,50 @@ class Transaction(BaseModel): __CUSTOM_FIELDS: List[Field] = [] # type: ignore def __str__(self) -> str: - properties = '' - ignore = ['_last_split', '_is_split'] - for (object_property, value) in self.__dict__.items(): + properties = "" + ignore = ["_last_split", "_is_split"] + for object_property, value in self.__dict__.items(): if value and object_property not in ignore: - if object_property == 'category': - properties += f'\n\tCategory: {value.name}' - elif object_property == '_split_categories': - properties += ( - f'\n\tSplit Categories: {list(value.keys())}' - ) - elif object_property == 'splits': - properties += f'\n\tSplits: {len(value)}' + if object_property == "category": + properties += f"\n\tCategory: {value.name}" + elif object_property == "_split_categories": + properties += f"\n\tSplit Categories: {list(value.keys())}" + elif object_property == "splits": + properties += f"\n\tSplits: {len(value)}" else: properties += ( - f'\n\t' + f"\n\t" f'{object_property.replace("_", " ").strip().title()}: ' - f'{value}' + f"{value}" ) - return 'Transaction:' + properties + return "Transaction:" + properties @root_validator(pre=True) def create_split_categories(cls, values: Dict[str, Any]) -> Dict: - if splits := values.get('splits'): + if splits := values.get("splits"): for split in splits: if split.category: - values['_split_categories'] = add_categories_to_container( + values["_split_categories"] = add_categories_to_container( split.category, - values.get('_split_categories', {}), + values.get("_split_categories", {}), ) - values['_last_split'] = splits[-1] + values["_last_split"] = splits[-1] return values - @validator('splits') + @validator("splits") def check_split_percentages_and_amounts( cls, splits: List[Split], values: Dict[str, Any], ) -> List[Split]: - total_percent = sum( - split.percent for split in splits if split.percent - ) + total_percent = sum(split.percent for split in splits if split.percent) total_amount = sum(split.amount for split in splits if split.amount is not None) if total_percent - 100 > 0.01: + raise ValueError("Split percentages cannot exceed 100% of the transaction") + if abs(total_amount) - abs(values.get("amount", 0)) > 0.01: raise ValueError( - 'Split percentages cannot exceed 100% of the transaction' - ) - if abs(total_amount) - abs(values.get('amount', 0)) > 0.01: - raise ValueError( - 'Split amounts cannot exceed the amount of the transaction' + "Split amounts cannot exceed the amount of the transaction" ) return splits @@ -192,22 +202,24 @@ def add_split(self, split: Split) -> None: """Add a Split to Transaction.""" if ( split.percent - and sum(s.percent for s in self.splits if s.percent is not None) + split.percent - 100 > 0.01 + and sum(s.percent for s in self.splits if s.percent is not None) + + split.percent + - 100 + > 0.01 ): raise ValueError( - 'The sum of the split percentages cannot be greater than 100.' + "The sum of the split percentages cannot be greater than 100." ) if split.amount: abs_sum_of_splits = abs( - sum( - s.amount for s in self.splits if s.amount is not None - ) + split.amount + sum(s.amount for s in self.splits if s.amount is not None) + + split.amount ) if abs_sum_of_splits - abs(self.amount) > 0.01: raise ValueError( - 'The sum of the split amounts cannot be greater than the ' - 'transaction amount.' + "The sum of the split amounts cannot be greater than the " + "transaction amount." ) self.splits.append(split) @@ -225,15 +237,10 @@ def remove_splits(self, **filters) -> List[Split]: to_remove = [] for split in self.splits: - if all( - getattr(split, attr) == value - for attr, value in filters.items() - ): + if all(getattr(split, attr) == value for attr, value in filters.items()): to_remove.append(split) - self.splits = [ - split for split in self.splits if split not in to_remove - ] + self.splits = [split for split in self.splits if split not in to_remove] return to_remove @@ -242,8 +249,8 @@ def _create_class_from_category_string( category_string: str, classes: Dict[str, Class], ) -> Tuple[Optional[str], str, Dict[str, Class]]: - if '/' in category_string: - field_info, class_name = category_string.split('/') + if "/" in category_string: + field_info, class_name = category_string.split("/") if class_name not in classes: classes[class_name] = Class(name=class_name) return class_name, field_info, classes @@ -251,23 +258,23 @@ def _create_class_from_category_string( def to_qif( self, - date_format: str = '%Y-%m-%d', + date_format: str = "%Y-%m-%d", classes: Optional[Dict[str, Class]] = None, ) -> str: """Converts a Transaction to a QIF string""" if classes is None: classes = {} - qif = f'D{self.date.strftime(date_format)}\n' - qif += f'T{self.amount}\n' + qif = f"D{self.date.strftime(date_format)}\n" + qif += f"T{self.amount}\n" if self.memo: - qif += f'M{self.memo}\n' + qif += f"M{self.memo}\n" if self.cleared: - qif += f'C{self.cleared}\n' + qif += f"C{self.cleared}\n" if self.payee: - qif += f'P{self.payee}\n' + qif += f"P{self.payee}\n" if self.payee_address: - qif += f'A{self.payee_address}\n' + qif += f"A{self.payee_address}\n" if self.category: parent_class = None for cls in classes.values(): @@ -276,30 +283,30 @@ def to_qif( parent_class = cls break - qif += f'L{self.category.hierarchy}' + qif += f"L{self.category.hierarchy}" if parent_class: - qif += f'/{parent_class.name}' - qif += '\n' + qif += f"/{parent_class.name}" + qif += "\n" if self.check_number: - qif += f'N{self.check_number}\n' + qif += f"N{self.check_number}\n" if self.reimbursable_expense: - qif += f'F{self.reimbursable_expense}\n' + qif += f"F{self.reimbursable_expense}\n" if self.to_account: - qif += f'L[{self.to_account}]\n' + qif += f"L[{self.to_account}]\n" if self.first_payment_date: - qif += f'1{self.first_payment_date.strftime(date_format)}\n' + qif += f"1{self.first_payment_date.strftime(date_format)}\n" if self.loan_length: - qif += f'2{self.loan_length}\n' + qif += f"2{self.loan_length}\n" if self.num_payments: - qif += f'3{self.num_payments}\n' + qif += f"3{self.num_payments}\n" if self.periods_per_annum: - qif += f'4{self.periods_per_annum}\n' + qif += f"4{self.periods_per_annum}\n" if self.interest_rate: - qif += f'5{self.interest_rate}\n' + qif += f"5{self.interest_rate}\n" if self.current_loan_balance: - qif += f'6{self.current_loan_balance}\n' + qif += f"6{self.current_loan_balance}\n" if self.original_loan_amount: - qif += f'7{self.original_loan_amount}\n' + qif += f"7{self.original_loan_amount}\n" if self.splits: for split in self.splits: qif += split.to_qif(date_format=date_format, classes=classes) @@ -356,7 +363,7 @@ def from_list( if found: continue - if line_code == 'S': + if line_code == "S": _, field_info, classes = cls._create_class_from_category_string( field_info, classes, @@ -366,63 +373,65 @@ def from_list( new_split = Split(category=split_category) splits.append(new_split) current_split = new_split - elif line_code == 'D': + elif line_code == "D": transaction_date = utils.parse_date(field_info, day_first) if not splits: - kwargs['date'] = transaction_date + kwargs["date"] = transaction_date elif current_split: current_split.date = transaction_date - elif line_code == 'E': + elif line_code == "E": if current_split is None: logger.warning( f"No split yet given for memo '{field_info}', skipping" ) else: current_split.memo = field_info - elif line_code in {'$', '£'}: + elif line_code in {"$", "£"}: if current_split: - current_split.amount = Decimal(field_info.replace(',', '')) - elif line_code == '%': + current_split.amount = Decimal(field_info.replace(",", "")) + elif line_code == "%": if current_split: current_split.percent = Decimal( - field_info.split(' ')[0].replace('%', '') + field_info.split(" ")[0].replace("%", "") ) - elif line_code in {'T', 'U'}: - amount = field_info.replace(',', '') + elif line_code in {"T", "U"}: + amount = field_info.replace(",", "") if not splits: - kwargs['amount'] = amount + kwargs["amount"] = amount elif current_split: current_split.amount = Decimal(amount) - elif line_code == 'M': + elif line_code == "M": if not splits: - kwargs['memo'] = field_info + kwargs["memo"] = field_info elif current_split: current_split.memo = field_info - elif line_code == 'C': + elif line_code == "C": if not splits: - kwargs['cleared'] = field_info + kwargs["cleared"] = field_info elif current_split: current_split.cleared = field_info - elif line_code == 'P': - kwargs['payee'] = field_info - elif line_code == 'A': + elif line_code == "P": + kwargs["payee"] = field_info + elif line_code == "A": if not splits: - kwargs['payee_address'] = field_info + kwargs["payee_address"] = field_info elif current_split: current_split.payee_address = field_info - elif line_code == 'L': - class_name, field_info, classes = ( - cls._create_class_from_category_string( - field_info, - classes, - ) + elif line_code == "L": + ( + class_name, + field_info, + classes, + ) = cls._create_class_from_category_string( + field_info, + classes, ) # 'L' can represent both categories and the 'to' transfer # account. Transfer accounts are denoted by [ ] - if field_info.startswith('['): + if field_info.startswith("["): if not splits: - kwargs['to_account'] = field_info[1:-1] + kwargs["to_account"] = field_info[1:-1] elif current_split: current_split.to_account = field_info[1:-1] else: @@ -431,60 +440,62 @@ def from_list( if not splits: # If there's already a category, add the new category # as a child - if 'category' in kwargs: - category_root.set_parent(kwargs['category']) - kwargs['category'] = category + if "category" in kwargs: + category_root.set_parent(kwargs["category"]) + kwargs["category"] = category elif current_split: category_root.set_parent(current_split.category) current_split.category = category if class_name: classes[class_name].add_category(category) - elif line_code == 'N': + elif line_code == "N": if not splits: - kwargs['check_number'] = field_info + kwargs["check_number"] = field_info elif current_split: current_split.check_number = field_info - elif line_code == 'F': - kwargs['reimbursable_expense'] = field_info or True - elif line_code == '1': - kwargs['first_payment_date'] = utils.parse_date( + elif line_code == "F": + kwargs["reimbursable_expense"] = field_info or True + elif line_code == "1": + kwargs["first_payment_date"] = utils.parse_date( field_info, day_first, ) - elif line_code == '2': - kwargs['loan_length'] = field_info.replace(',', '') - elif line_code == '3': - kwargs['num_payments'] = field_info.replace(',', '') - elif line_code == '4': - kwargs['periods_per_annum'] = field_info.replace(',', '') - elif line_code == '5': - kwargs['interest_rate'] = field_info.replace(',', '') - elif line_code == '6': - kwargs['current_loan_balance'] = field_info.replace(',', '') - elif line_code == '7': - kwargs['original_loan_amount'] = field_info.replace(',', '') + elif line_code == "2": + kwargs["loan_length"] = field_info.replace(",", "") + elif line_code == "3": + kwargs["num_payments"] = field_info.replace(",", "") + elif line_code == "4": + kwargs["periods_per_annum"] = field_info.replace(",", "") + elif line_code == "5": + kwargs["interest_rate"] = field_info.replace(",", "") + elif line_code == "6": + kwargs["current_loan_balance"] = field_info.replace(",", "") + elif line_code == "7": + kwargs["original_loan_amount"] = field_info.replace(",", "") else: - raise ValueError(f'Unknown line code: {line_code}') + raise ValueError(f"Unknown line code: {line_code}") if line_number is not None: - kwargs['line_number'] = line_number + kwargs["line_number"] = line_number # Set splits percentage if they don't already have one - total = Decimal(kwargs.get('amount', 0)) + total = Decimal(kwargs.get("amount", 0)) if splits and total: for split in splits: if split.percent is None and split.amount is not None: - split.percent = Decimal( - round(split.amount / total * 100, 2) - ) + split.percent = Decimal(round(split.amount / total * 100, 2)) # Check if the split percentage is correct - elif split.percent is not None and split.amount is not None and not ( - Decimal(round(split.percent, 2)) - == Decimal( - round( - split.amount / total * 100, - 2, + elif ( + split.percent is not None + and split.amount is not None + and not ( + Decimal(round(split.percent, 2)) + == Decimal( + round( + split.amount / total * 100, + 2, + ) ) ) ): @@ -497,14 +508,14 @@ def from_list( for split in splits: split.percent = None - kwargs['splits'] = splits + kwargs["splits"] = splits return cls(**kwargs), classes @classmethod def from_string( cls, string: str, - separator: str = '\n', + separator: str = "\n", day_first: bool = False, line_number: Optional[int] = None, ) -> Tuple[Transaction, Dict[str, Class]]: diff --git a/quiffen/utils.py b/quiffen/utils.py index 8599a1f..6d3b56d 100644 --- a/quiffen/utils.py +++ b/quiffen/utils.py @@ -10,7 +10,7 @@ from quiffen.core.base import Field ZERO_SEPARATED_DATE = re.compile( - r'^(\d{2}|\d{4}|[a-zA-Z]+)0(\d{2}|[a-zA-Z]+)0(\d{2}|\d{4})$', + r"^(\d{2}|\d{4}|[a-zA-Z]+)0(\d{2}|[a-zA-Z]+)0(\d{2}|\d{4})$", ) @@ -37,8 +37,8 @@ def parse_date(date_string: str, day_first: bool = False) -> datetime: """ # QIF files sometimes use ' ' instead of a 0 or a ' instead of a / - date_string = date_string.replace(' ', '0') - date_string = date_string.replace('\'', '/') + date_string = date_string.replace(" ", "0") + date_string = date_string.replace("'", "/") # QIF files allow some really strange date formats, such as # %d0%B0%Y (e.g. 0100202022 for 2022-02-01) @@ -49,23 +49,23 @@ def parse_date(date_string: str, day_first: bool = False) -> datetime: if date_search: date_parts = date_search.groups() - date_string = ' '.join(date_parts) + date_string = " ".join(date_parts) return parser.parse(date_string, dayfirst=day_first) def parse_line_code_and_field_info(field: str) -> Tuple[str, str]: """Parse a QIF field into a line code and field info.""" - field = field.replace('\n', '') + field = field.replace("\n", "") if not field: - return '', '' + return "", "" line_code = field[0] if len(field) > 1: field_info = field[1:] else: - field_info = '' + field_info = "" return line_code, field_info @@ -90,7 +90,7 @@ def add_custom_field_to_object_dict( try: object_dict[custom_field.attr] = parse_obj_as( custom_field.type, - field[len(custom_field.line_code):], + field[len(custom_field.line_code) :], ) return object_dict, True except ValidationError: @@ -104,17 +104,17 @@ def convert_custom_fields_to_qif_string( obj: Any, ) -> str: """Convert custom fields to a QIF string.""" - qif = '' + qif = "" for custom_field in custom_fields: if (attr := getattr(obj, custom_field.attr)) is not None: - qif += f'{custom_field.line_code}{attr}\n' + qif += f"{custom_field.line_code}{attr}\n" return qif def apply_csv_formatting_to_scalar( obj: Any, - date_format: Optional[str] = '%Y-%m-%d', + date_format: Optional[str] = "%Y-%m-%d", ) -> Union[str, int, float]: """Apply CSV-friendly formatting to a scalar value""" if isinstance(obj, (datetime, date)) and date_format: @@ -132,18 +132,16 @@ def apply_csv_formatting_to_scalar( def apply_csv_formatting_to_container( obj: Union[List[Any], Dict[Any, Any]], - date_format: Optional[str] = '%Y-%m-%d', + date_format: Optional[str] = "%Y-%m-%d", ) -> Union[List[Any], Dict[Any, Any], str, int, float]: """Recursively apply CSV-friendly formatting to a container""" if isinstance(obj, list): - return [ - apply_csv_formatting_to_container(item, date_format) - for item in obj - ] + return [apply_csv_formatting_to_container(item, date_format) for item in obj] elif isinstance(obj, dict): return { - apply_csv_formatting_to_scalar(key, date_format): - apply_csv_formatting_to_container(value, date_format) + apply_csv_formatting_to_scalar( + key, date_format + ): apply_csv_formatting_to_container(value, date_format) for key, value in obj.items() } else: diff --git a/tests/test_account.py b/tests/test_account.py index c60a1a8..6370654 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -8,8 +8,8 @@ def test_create_account(): """Test creating an account""" - account = Account(name='Test Account') - assert account.name == 'Test Account' + account = Account(name="Test Account") + assert account.name == "Test Account" assert account.account_type is None assert account.desc is None assert account.balance is None @@ -18,67 +18,67 @@ def test_create_account(): assert account.credit_limit is None account2 = Account( - name='Test Account', - account_type='Bank', - desc='Test Description', + name="Test Account", + account_type="Bank", + desc="Test Description", balance=100, ) - assert account2.name == 'Test Account' - assert account2.account_type == 'Bank' - assert account2.desc == 'Test Description' + assert account2.name == "Test Account" + assert account2.account_type == "Bank" + assert account2.desc == "Test Description" assert account2.balance == 100 assert not account2.transactions account3 = Account( - name='Test Account', - account_type='Bank', - desc='Test Description', + name="Test Account", + account_type="Bank", + desc="Test Description", balance=100, transactions={ - 'Bank': [Transaction(date=datetime(2022, 2, 1), amount=0)], + "Bank": [Transaction(date=datetime(2022, 2, 1), amount=0)], }, ) - assert account3.name == 'Test Account' - assert account3.account_type == 'Bank' - assert account3.desc == 'Test Description' + assert account3.name == "Test Account" + assert account3.account_type == "Bank" + assert account3.desc == "Test Description" assert account3.balance == 100 assert account3.transactions == { - 'Bank': [Transaction(date=datetime(2022, 2, 1), amount=Decimal(0))], + "Bank": [Transaction(date=datetime(2022, 2, 1), amount=Decimal(0))], } def test_eq_success(): """Test comparing two accounts for equality""" - account1 = Account(name='Test Account') - account2 = Account(name='Test Account') + account1 = Account(name="Test Account") + account2 = Account(name="Test Account") assert account1 == account2 def test_eq_failure(): """Test comparing two accounts for equality""" - account1 = Account(name='Test Account') - account2 = Account(name='Test Account 2') + account1 = Account(name="Test Account") + account2 = Account(name="Test Account 2") assert account1 != account2 - account3 = Account(name='Test Account', account_type='CCard') - account4 = Account(name='Test Account', account_type='Bank') + account3 = Account(name="Test Account", account_type="CCard") + account4 = Account(name="Test Account", account_type="Bank") assert account3 != account4 def test_str_method(): """Test the string representation of an account""" account = Account( - name='Test Account', - account_type='Bank', - desc='Test Description', + name="Test Account", + account_type="Bank", + desc="Test Description", balance=100, transactions={ - 'Bank': [Transaction(date=datetime(2022, 2, 1), amount=0)], + "Bank": [Transaction(date=datetime(2022, 2, 1), amount=0)], }, ) assert str(account) == ( - 'Account:\n\tName: Test Account\n\tDesc: Test Description\n\t' - 'Account Type: Bank\n\tBalance: 100\n\tTransactions: 1' + "Account:\n\tName: Test Account\n\tDesc: Test Description\n\t" + "Account Type: Bank\n\tBalance: 100\n\tTransactions: 1" ) account.add_transaction( @@ -86,8 +86,8 @@ def test_str_method(): header=AccountType.BANK, ) assert str(account) == ( - 'Account:\n\tName: Test Account\n\tDesc: Test Description\n\t' - 'Account Type: Bank\n\tBalance: 100\n\tTransactions: 2' + "Account:\n\tName: Test Account\n\tDesc: Test Description\n\t" + "Account Type: Bank\n\tBalance: 100\n\tTransactions: 2" ) account.add_transaction( @@ -95,42 +95,44 @@ def test_str_method(): header=AccountType.CREDIT_CARD, ) assert str(account) == ( - 'Account:\n\tName: Test Account\n\tDesc: Test Description\n\t' - 'Account Type: Bank\n\tBalance: 100\n\tTransactions: 3' + "Account:\n\tName: Test Account\n\tDesc: Test Description\n\t" + "Account Type: Bank\n\tBalance: 100\n\tTransactions: 3" ) # pylint: disable=protected-access def test_set_header(): """Test setting the header for an account""" - account = Account(name='Test Account') + account = Account(name="Test Account") assert account._last_header is None account.set_header(AccountType.BANK) - assert account._last_header == 'Bank' + assert account._last_header == "Bank" + + # pylint: enable=protected-access def test_add_transaction(): """Test adding a transaction to an account""" - account = Account(name='Test Account') + account = Account(name="Test Account") transaction = Transaction(date=datetime(2022, 2, 1), amount=0) - account.add_transaction(transaction, header='Bank') + account.add_transaction(transaction, header="Bank") assert account.transactions == { - 'Bank': [Transaction(date=datetime(2022, 2, 1), amount=Decimal(0))], + "Bank": [Transaction(date=datetime(2022, 2, 1), amount=Decimal(0))], } def test_add_transaction_invalid_header(): """Test adding a transaction to an account with an invalid header""" - account = Account(name='Test Account') + account = Account(name="Test Account") transaction = Transaction(date=datetime(2022, 2, 1), amount=0) with pytest.raises(ValueError): - account.add_transaction(transaction, header='Invalid') + account.add_transaction(transaction, header="Invalid") def test_add_transaction_no_header(): """Test adding a transaction to an account with no header""" - account = Account(name='Test Account') + account = Account(name="Test Account") transaction = Transaction(date=datetime(2022, 2, 1), amount=0) with pytest.raises(RuntimeError): account.add_transaction(transaction) @@ -139,27 +141,27 @@ def test_add_transaction_no_header(): def test_merge(): """Test merging two accounts""" account1 = Account( - name='Test Account', - account_type='Bank', + name="Test Account", + account_type="Bank", balance=100, transactions={ - 'Bank': [Transaction(date=datetime(2022, 2, 1), amount=0)], + "Bank": [Transaction(date=datetime(2022, 2, 1), amount=0)], }, ) account2 = Account( - name='Test Account', - account_type='Bank', - desc='Test Description', + name="Test Account", + account_type="Bank", + desc="Test Description", balance=100, transactions={ - 'Bank': [Transaction(date=datetime(2022, 2, 1), amount=0)], + "Bank": [Transaction(date=datetime(2022, 2, 1), amount=0)], }, ) account1.merge(account2) - assert account1.name == 'Test Account' - assert account1.desc == 'Test Description' + assert account1.name == "Test Account" + assert account1.desc == "Test Description" assert account1.transactions == { - 'Bank': [ + "Bank": [ Transaction(date=datetime(2022, 2, 1), amount=Decimal(0)), Transaction(date=datetime(2022, 2, 1), amount=Decimal(0)), ], @@ -169,37 +171,37 @@ def test_merge(): def test_to_qif(): """Test converting an account to QIF""" account = Account( - name='Test Account', - account_type='Bank', - desc='Test Description', + name="Test Account", + account_type="Bank", + desc="Test Description", balance=100, transactions={ - 'Bank': [Transaction(date=datetime(2022, 2, 1), amount=0)], + "Bank": [Transaction(date=datetime(2022, 2, 1), amount=0)], }, ) account_qif = account.to_qif() print(repr(account_qif)) assert account_qif == ( - '!Account\nNTest Account\nDTest Description\nTBank\n$100\n' - '^\n!Type:Bank\nD2022-02-01\nT0\n' + "!Account\nNTest Account\nDTest Description\nTBank\n$100\n" + "^\n!Type:Bank\nD2022-02-01\nT0\n" ) def test_from_list_no_custom_fields(): """Test creating an account from a list of sections with no custom fields""" qif_list = [ - '!This should be ignored', - 'NTest Account', - 'DTest Description', - 'TCCard', - 'L1000', - '$100', - '/2022-02-01', + "!This should be ignored", + "NTest Account", + "DTest Description", + "TCCard", + "L1000", + "$100", + "/2022-02-01", ] account = Account.from_list(qif_list) - assert account.name == 'Test Account' - assert account.desc == 'Test Description' - assert account.account_type == 'CCard' + assert account.name == "Test Account" + assert account.desc == "Test Description" + assert account.account_type == "CCard" assert account.credit_limit == 1000 assert account.balance == 100 assert account.date_at_balance == datetime(2022, 2, 1) @@ -208,49 +210,49 @@ def test_from_list_no_custom_fields(): def test_from_list_with_custom_fields(): """Test creating an account from a list of sections with custom fields""" - setattr(Account, '__CUSTOM_FIELDS', []) # Reset custom fields + setattr(Account, "__CUSTOM_FIELDS", []) # Reset custom fields qif_list = [ - '!This should be ignored', - 'NTest Account', - 'DTest Description', - 'TCCard', - 'L1000', - '$100', - '/2022-02-01', - 'XCustom field 1', - 'Y9238479', - 'DT2022-01-01T00:00:00.000001', + "!This should be ignored", + "NTest Account", + "DTest Description", + "TCCard", + "L1000", + "$100", + "/2022-02-01", + "XCustom field 1", + "Y9238479", + "DT2022-01-01T00:00:00.000001", ] # Add custom fields Account.add_custom_field( - line_code='X', - attr='custom_field_1', + line_code="X", + attr="custom_field_1", field_type=str, ) Account.add_custom_field( - line_code='Y', - attr='custom_field_2', + line_code="Y", + attr="custom_field_2", field_type=Decimal, ) Account.add_custom_field( - line_code='DT', # Test multi-character line code - attr='custom_field_3', + line_code="DT", # Test multi-character line code + attr="custom_field_3", field_type=datetime, ) account = Account.from_list(qif_list) - assert account.name == 'Test Account' - assert account.desc == 'Test Description' - assert account.account_type == 'CCard' + assert account.name == "Test Account" + assert account.desc == "Test Description" + assert account.account_type == "CCard" assert account.credit_limit == 1000 assert account.balance == 100 assert account.date_at_balance == datetime(2022, 2, 1) assert not account.transactions - assert account.custom_field_1 == 'Custom field 1' - assert account.custom_field_2 == Decimal('9238479') + assert account.custom_field_1 == "Custom field 1" + assert account.custom_field_2 == Decimal("9238479") assert account.custom_field_3 == datetime(2022, 1, 1, 0, 0, 0, 1) - setattr(Account, '__CUSTOM_FIELDS', []) # Reset custom fields + setattr(Account, "__CUSTOM_FIELDS", []) # Reset custom fields def test_from_list_with_unknown_line_code(): @@ -258,14 +260,14 @@ def test_from_list_with_unknown_line_code(): line code """ qif_list = [ - '!This should be ignored', - 'NTest Account', - 'DTest Description', - 'TCCard', - 'L1000', - '$100', - '/2022-02-01', - 'ZInvalid field', + "!This should be ignored", + "NTest Account", + "DTest Description", + "TCCard", + "L1000", + "$100", + "/2022-02-01", + "ZInvalid field", ] with pytest.raises(ValueError): @@ -275,18 +277,18 @@ def test_from_list_with_unknown_line_code(): def test_from_string_default_separator(): """Test creating an account from a string with the default separator""" qif_string = ( - '!This should be ignored\n' - 'NTest Account\n' - 'DTest Description\n' - 'TCCard\n' - 'L1000\n' - '$100\n' - '/2022-02-01\n' + "!This should be ignored\n" + "NTest Account\n" + "DTest Description\n" + "TCCard\n" + "L1000\n" + "$100\n" + "/2022-02-01\n" ) account = Account.from_string(qif_string) - assert account.name == 'Test Account' - assert account.desc == 'Test Description' - assert account.account_type == 'CCard' + assert account.name == "Test Account" + assert account.desc == "Test Description" + assert account.account_type == "CCard" assert account.credit_limit == 1000 assert account.balance == 100 assert account.date_at_balance == datetime(2022, 2, 1) @@ -296,18 +298,18 @@ def test_from_string_default_separator(): def test_from_string_custom_separator(): """Test creating an account from a string with a custom separator""" qif_string = ( - '!This should be ignored---' - 'NTest Account---' - 'DTest Description---' - 'TCCard---' - 'L1000---' - '$100---' - '/2022-02-01---' + "!This should be ignored---" + "NTest Account---" + "DTest Description---" + "TCCard---" + "L1000---" + "$100---" + "/2022-02-01---" ) - account = Account.from_string(qif_string, separator='---') - assert account.name == 'Test Account' - assert account.desc == 'Test Description' - assert account.account_type == 'CCard' + account = Account.from_string(qif_string, separator="---") + assert account.name == "Test Account" + assert account.desc == "Test Description" + assert account.account_type == "CCard" assert account.credit_limit == 1000 assert account.balance == 100 assert account.date_at_balance == datetime(2022, 2, 1) @@ -316,27 +318,27 @@ def test_from_string_custom_separator(): def test_to_dict(): """Test converting an account to a dictionary""" - account = Account(name='Test Account') + account = Account(name="Test Account") account_dict = account.to_dict() assert account_dict == { - 'name': 'Test Account', - 'desc': None, - 'account_type': None, - 'credit_limit': None, - 'balance': None, - 'date_at_balance': None, - 'transactions': {}, + "name": "Test Account", + "desc": None, + "account_type": None, + "credit_limit": None, + "balance": None, + "date_at_balance": None, + "transactions": {}, } def test_to_dict_with_ignore(): """Test converting an account to a dictionary with ignored fields""" - account = Account(name='Test Account') - account_dict = account.to_dict(ignore=['name', 'desc']) + account = Account(name="Test Account") + account_dict = account.to_dict(ignore=["name", "desc"]) assert account_dict == { - 'account_type': None, - 'credit_limit': None, - 'balance': None, - 'date_at_balance': None, - 'transactions': {}, + "account_type": None, + "credit_limit": None, + "balance": None, + "date_at_balance": None, + "transactions": {}, } diff --git a/tests/test_base.py b/tests/test_base.py index c2037e0..62b2bb1 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -6,82 +6,87 @@ def test_create_field(): """Test creating a field""" - field = Field(line_code='T', attr='test', type=str) - assert field.line_code == 'T' - assert field.attr == 'test' + field = Field(line_code="T", attr="test", type=str) + assert field.line_code == "T" + assert field.attr == "test" assert field.type == str def test_sorting_fields(): """Test sorting fields by line code""" - field1 = Field(line_code='A', attr='a', type=str) - field2 = Field(line_code='B', attr='b', type=str) - field3 = Field(line_code='AA', attr='aa', type=str) + field1 = Field(line_code="A", attr="a", type=str) + field2 = Field(line_code="B", attr="b", type=str) + field3 = Field(line_code="AA", attr="aa", type=str) assert sorted([field1, field2, field3]) == [field1, field3, field2] def test_base_model_allows_extra_fields(): """Test that the BaseModel allows extra fields""" + class TestModel(BaseModel): @classmethod def from_list(cls, lst: List[str]) -> None: pass - test_model = TestModel(test='test') - assert test_model.test == 'test' + test_model = TestModel(test="test") + assert test_model.test == "test" def test_add_custom_field(): """Test adding a custom field to a class""" + class TestModel(BaseModel): pass - TestModel.add_custom_field('T', 'test', str) + TestModel.add_custom_field("T", "test", str) assert TestModel._get_custom_fields() == [ - Field(line_code='T', attr='test', type=str), + Field(line_code="T", attr="test", type=str), ] def test_overwrite_custom_field(): """Test overwriting a custom field in a class""" + class TestModel(BaseModel): pass - TestModel.add_custom_field('T', 'test', str) - TestModel.add_custom_field('T', 'test2', int) + TestModel.add_custom_field("T", "test", str) + TestModel.add_custom_field("T", "test2", int) assert TestModel._get_custom_fields() == [ - Field(line_code='T', attr='test2', type=int), + Field(line_code="T", attr="test2", type=int), ] def test_custom_fields_are_not_shared(): """Test that custom fields are not shared between classes""" + class TestModel1(BaseModel): pass class TestModel2(BaseModel): pass - TestModel1.add_custom_field('T', 'test', str) + TestModel1.add_custom_field("T", "test", str) assert TestModel1._get_custom_fields() == [ - Field(line_code='T', attr='test', type=str), + Field(line_code="T", attr="test", type=str), ] assert not TestModel2._get_custom_fields() def test_custom_fields_are_inherited(): """Test that custom fields are inherited by subclasses""" + class TestModel1(BaseModel): pass class TestModel2(TestModel1): pass - TestModel1.add_custom_field('T', 'test', str) + TestModel1.add_custom_field("T", "test", str) assert TestModel1._get_custom_fields() == [ - Field(line_code='T', attr='test', type=str), + Field(line_code="T", attr="test", type=str), ] assert TestModel2._get_custom_fields() == [ - Field(line_code='T', attr='test', type=str), + Field(line_code="T", attr="test", type=str), ] diff --git a/tests/test_category.py b/tests/test_category.py index 7608845..2c23793 100644 --- a/tests/test_category.py +++ b/tests/test_category.py @@ -5,12 +5,13 @@ import pytest -from quiffen.core.qif import Qif from quiffen.core.category import ( Category, - CategoryType, add_categories_to_container, + CategoryType, + add_categories_to_container, create_categories_from_hierarchy, ) +from quiffen.core.qif import Qif @pytest.fixture @@ -23,11 +24,11 @@ def tree(): └─ Grandchild2 └─ Child2 """ - parent = Category(name='Parent') - child1 = Category(name='Child1') - child2 = Category(name='Child2') - grandchild1 = Category(name='Grandchild1') - grandchild2 = Category(name='Grandchild2') + parent = Category(name="Parent") + child1 = Category(name="Child1") + child2 = Category(name="Child2") + grandchild1 = Category(name="Grandchild1") + grandchild2 = Category(name="Grandchild2") parent.add_child(child1) parent.add_child(child2) child1.add_child(grandchild1) @@ -37,23 +38,23 @@ def tree(): def test_create_category(): """Test creating a category""" - category = Category(name='Test') - assert category.name == 'Test' + category = Category(name="Test") + assert category.name == "Test" assert category.parent is None assert not category.children - assert category.hierarchy == 'Test' - assert category.category_type == 'expense' + assert category.hierarchy == "Test" + assert category.category_type == "expense" def test_eq_success(): """Test equality""" - category1 = Category(name='Test') - category2 = Category(name='Test') + category1 = Category(name="Test") + category2 = Category(name="Test") assert category1 == category2 - parent1 = Category(name='Parent') - parent2 = Category(name='Parent') - child1 = Category(name='Child') + parent1 = Category(name="Parent") + parent2 = Category(name="Parent") + child1 = Category(name="Child") child2 = child1.copy() parent1.add_child(child1) parent2.add_child(child2) @@ -62,13 +63,13 @@ def test_eq_success(): def test_eq_failure(): """Test equality""" - category1 = Category(name='Test1') - category2 = Category(name='Test1', category_type='income') + category1 = Category(name="Test1") + category2 = Category(name="Test1", category_type="income") assert category1 != category2 - parent1 = Category(name='Parent') - parent2 = Category(name='Parent') - child = Category(name='Child') + parent1 = Category(name="Parent") + parent2 = Category(name="Parent") + child = Category(name="Child") parent1.add_child(child) assert parent1 != parent2 @@ -76,117 +77,117 @@ def test_eq_failure(): def test_str_method(): """Test the string method""" category = Category( - name='Test', + name="Test", category_type=CategoryType.INCOME, - desc='Test', - children=[Category(name='Child 1'), Category(name='Child 2')], + desc="Test", + children=[Category(name="Child 1"), Category(name="Child 2")], ) assert str(category) == ( - 'Category:\n\tName: Test\n\tDesc: Test\n\t' - 'Category Type: income\n\tHierarchy: Test\n\tChildren: 2' + "Category:\n\tName: Test\n\tDesc: Test\n\t" + "Category Type: income\n\tHierarchy: Test\n\tChildren: 2" ) def test_refresh_hierarchy(): """Test refreshing the hierarchy of a category""" - parent = Category(name='Parent') - child = Category(name='Child') + parent = Category(name="Parent") + child = Category(name="Child") parent.add_child(child) - child.name = 'New Name' + child.name = "New Name" parent._refresh_hierarchy() - assert child.hierarchy == 'Parent:New Name' + assert child.hierarchy == "Parent:New Name" def test_to_dict(tree): """Test converting a category to a dictionary""" parent, child1, child2, grandchild1, grandchild2 = tree assert parent.to_dict() == { - 'name': 'Parent', - 'category_type': 'expense', - 'children': ['Child1', 'Child2'], - 'hierarchy': 'Parent', - 'budget_amount': None, - 'desc': None, - 'parent': None, - 'tax_related': None, - 'tax_schedule_info': None, + "name": "Parent", + "category_type": "expense", + "children": ["Child1", "Child2"], + "hierarchy": "Parent", + "budget_amount": None, + "desc": None, + "parent": None, + "tax_related": None, + "tax_schedule_info": None, } assert child1.to_dict() == { - 'name': 'Child1', - 'category_type': 'expense', - 'children': ['Grandchild1', 'Grandchild2'], - 'hierarchy': 'Parent:Child1', - 'parent': 'Parent', - 'budget_amount': None, - 'desc': None, - 'tax_related': None, - 'tax_schedule_info': None, + "name": "Child1", + "category_type": "expense", + "children": ["Grandchild1", "Grandchild2"], + "hierarchy": "Parent:Child1", + "parent": "Parent", + "budget_amount": None, + "desc": None, + "tax_related": None, + "tax_schedule_info": None, } assert child2.to_dict() == { - 'name': 'Child2', - 'category_type': 'expense', - 'children': [], - 'hierarchy': 'Parent:Child2', - 'parent': 'Parent', - 'budget_amount': None, - 'desc': None, - 'tax_related': None, - 'tax_schedule_info': None, + "name": "Child2", + "category_type": "expense", + "children": [], + "hierarchy": "Parent:Child2", + "parent": "Parent", + "budget_amount": None, + "desc": None, + "tax_related": None, + "tax_schedule_info": None, } assert grandchild1.to_dict() == { - 'name': 'Grandchild1', - 'category_type': 'expense', - 'children': [], - 'hierarchy': 'Parent:Child1:Grandchild1', - 'parent': 'Child1', - 'budget_amount': None, - 'desc': None, - 'tax_related': None, - 'tax_schedule_info': None, + "name": "Grandchild1", + "category_type": "expense", + "children": [], + "hierarchy": "Parent:Child1:Grandchild1", + "parent": "Child1", + "budget_amount": None, + "desc": None, + "tax_related": None, + "tax_schedule_info": None, } assert grandchild2.to_dict() == { - 'name': 'Grandchild2', - 'category_type': 'expense', - 'children': [], - 'hierarchy': 'Parent:Child1:Grandchild2', - 'parent': 'Child1', - 'budget_amount': None, - 'desc': None, - 'tax_related': None, - 'tax_schedule_info': None, + "name": "Grandchild2", + "category_type": "expense", + "children": [], + "hierarchy": "Parent:Child1:Grandchild2", + "parent": "Child1", + "budget_amount": None, + "desc": None, + "tax_related": None, + "tax_schedule_info": None, } def test_to_dict_with_ignore(): """Test converting a category to a dictionary with ignored fields""" - category = Category(name='Test') - assert category.to_dict(ignore=['name']) == { - 'category_type': 'expense', - 'children': [], - 'hierarchy': 'Test', - 'budget_amount': None, - 'desc': None, - 'parent': None, - 'tax_related': None, - 'tax_schedule_info': None, + category = Category(name="Test") + assert category.to_dict(ignore=["name"]) == { + "category_type": "expense", + "children": [], + "hierarchy": "Test", + "budget_amount": None, + "desc": None, + "parent": None, + "tax_related": None, + "tax_schedule_info": None, } def test_set_parent(): """Test setting a parent category""" - parent = Category(name='Parent') - child = Category(name='Child') + parent = Category(name="Parent") + child = Category(name="Child") child.set_parent(parent) assert child.parent == parent assert parent.children == [child] - assert child.hierarchy == 'Parent:Child' + assert child.hierarchy == "Parent:Child" def test_set_parent_with_children(): """Test setting a parent category that already has children""" - parent = Category(name='Parent') - child1 = Category(name='Child1') - child2 = Category(name='Child2') + parent = Category(name="Parent") + child1 = Category(name="Child1") + child2 = Category(name="Child2") child1.add_child(child2) child2.set_parent(parent) assert child1.parent is None @@ -194,24 +195,24 @@ def test_set_parent_with_children(): assert parent.children == [child2] assert not child1.children assert not child2.children - assert child1.hierarchy == 'Child1' - assert child2.hierarchy == 'Parent:Child2' + assert child1.hierarchy == "Child1" + assert child2.hierarchy == "Parent:Child2" def test_set_parent_to_self(): """Test setting a category as its own parent raises a ValueError""" - category = Category(name='Test') + category = Category(name="Test") with pytest.raises(ValueError): category.set_parent(category) def test_merge_single_level(): """Test merging two categories, one level deep""" - parent1 = Category(name='Parent') - parent2 = Category(name='Parent') - child1 = Category(name='Child1') - child2 = Category(name='Child2') - child3 = Category(name='Child3') + parent1 = Category(name="Parent") + parent2 = Category(name="Parent") + child1 = Category(name="Child1") + child2 = Category(name="Child2") + child3 = Category(name="Child3") parent1.add_child(child1) parent1.add_child(child2) parent2.add_child(child3) @@ -224,14 +225,14 @@ def test_merge_single_level(): def test_merge_multi_level(): """Test merging two categories, multiple levels deep""" - parent1 = Category(name='Parent') - parent2 = Category(name='Parent') - child1 = Category(name='Child1') - child1_2 = Category(name='Child1') - child2 = Category(name='Child2') - grandchild1 = Category(name='Grandchild1') - grandchild2 = Category(name='Grandchild2') - grandchild3 = Category(name='Grandchild3') + parent1 = Category(name="Parent") + parent2 = Category(name="Parent") + child1 = Category(name="Child1") + child1_2 = Category(name="Child1") + child2 = Category(name="Child2") + grandchild1 = Category(name="Grandchild1") + grandchild2 = Category(name="Grandchild2") + grandchild3 = Category(name="Grandchild3") parent1.add_child(child1) parent1.add_child(child2) parent2.add_child(child1_2) @@ -252,47 +253,47 @@ def test_merge_multi_level(): def test_add_child(): """Test adding a child category""" - parent = Category(name='Parent') - child = Category(name='Child') + parent = Category(name="Parent") + child = Category(name="Child") parent.add_child(child) assert child.parent == parent assert parent.children == [child] - assert child.hierarchy == 'Parent:Child' + assert child.hierarchy == "Parent:Child" def test_add_multiple_children(): """Test adding multiple children to a category""" - parent = Category(name='Parent') - child1 = Category(name='Child1') - child2 = Category(name='Child2') + parent = Category(name="Parent") + child1 = Category(name="Child1") + child2 = Category(name="Child2") parent.add_child(child1) parent.add_child(child2) assert child1.parent == parent assert child2.parent == parent assert parent.children == [child1, child2] - assert child1.hierarchy == 'Parent:Child1' - assert child2.hierarchy == 'Parent:Child2' + assert child1.hierarchy == "Parent:Child1" + assert child2.hierarchy == "Parent:Child2" def test_add_child_with_parent(): """Test adding a child category that already has a parent""" - parent1 = Category(name='Parent1') - parent2 = Category(name='Parent2') - child = Category(name='Child') + parent1 = Category(name="Parent1") + parent2 = Category(name="Parent2") + child = Category(name="Child") parent1.add_child(child) parent2.add_child(child) assert child.parent == parent2 assert not parent1.children assert parent2.children == [child] - assert child.hierarchy == 'Parent2:Child' + assert child.hierarchy == "Parent2:Child" def test_add_child_with_children(): """Test adding a child category that already has children""" - parent = Category(name='Parent') - child1 = Category(name='Child1') - child2 = Category(name='Child2') - child3 = Category(name='Child3') + parent = Category(name="Parent") + child1 = Category(name="Child1") + child2 = Category(name="Child2") + child3 = Category(name="Child3") child1.add_child(child2) parent.add_child(child1) parent.add_child(child3) @@ -302,61 +303,61 @@ def test_add_child_with_children(): assert parent.children == [child1, child3] assert child1.children == [child2] assert not child2.children - assert child1.hierarchy == 'Parent:Child1' - assert child2.hierarchy == 'Parent:Child1:Child2' - assert child3.hierarchy == 'Parent:Child3' + assert child1.hierarchy == "Parent:Child1" + assert child2.hierarchy == "Parent:Child1:Child2" + assert child3.hierarchy == "Parent:Child3" def test_add_child_multiple_times(): """Test adding a child category multiple times""" - parent = Category(name='Parent') - child = Category(name='Child') + parent = Category(name="Parent") + child = Category(name="Child") parent.add_child(child) parent.add_child(child) assert child.parent == parent assert parent.children == [child] - assert child.hierarchy == 'Parent:Child' + assert child.hierarchy == "Parent:Child" def test_add_child_multi_level(): """Test adding a child to a category that has a parent""" - parent = Category(name='Parent') - child = Category(name='Child') - grandchild = Category(name='Grandchild') + parent = Category(name="Parent") + child = Category(name="Child") + grandchild = Category(name="Grandchild") parent.add_child(child) child.add_child(grandchild) assert child.parent == parent assert grandchild.parent == child assert parent.children == [child] assert child.children == [grandchild] - assert child.hierarchy == 'Parent:Child' - assert grandchild.hierarchy == 'Parent:Child:Grandchild' + assert child.hierarchy == "Parent:Child" + assert grandchild.hierarchy == "Parent:Child:Grandchild" def test_add_self_as_child(): """Test adding a category as its own child raises a ValueError""" - category = Category(name='Test') + category = Category(name="Test") with pytest.raises(ValueError): category.add_child(category) def test_remove_child(): """Test removing a child category""" - parent = Category(name='Parent') - child = Category(name='Child') + parent = Category(name="Parent") + child = Category(name="Child") parent.add_child(child) parent.remove_child(child) assert child.parent is None assert not parent.children - assert child.hierarchy == 'Child' - assert parent.hierarchy == 'Parent' + assert child.hierarchy == "Child" + assert parent.hierarchy == "Parent" def test_remove_child_with_children_without_keep(): """Test removing a child category that has children""" - parent = Category(name='Parent') - child = Category(name='Child') - grandchild = Category(name='Grandchild') + parent = Category(name="Parent") + child = Category(name="Child") + grandchild = Category(name="Grandchild") parent.add_child(child) child.add_child(grandchild) parent.remove_child(child) @@ -365,16 +366,16 @@ def test_remove_child_with_children_without_keep(): assert not parent.children assert child.children == [grandchild] assert not grandchild.children - assert child.hierarchy == 'Child' - assert grandchild.hierarchy == 'Child:Grandchild' - assert parent.hierarchy == 'Parent' + assert child.hierarchy == "Child" + assert grandchild.hierarchy == "Child:Grandchild" + assert parent.hierarchy == "Parent" def test_remove_child_with_children_with_keep(): """Test removing a child category that has children""" - parent = Category(name='Parent') - child = Category(name='Child') - grandchild = Category(name='Grandchild') + parent = Category(name="Parent") + child = Category(name="Child") + grandchild = Category(name="Grandchild") parent.add_child(child) child.add_child(grandchild) parent.remove_child(child, keep_children=True) @@ -383,17 +384,17 @@ def test_remove_child_with_children_with_keep(): assert parent.children == [grandchild] assert not child.children assert not grandchild.children - assert child.hierarchy == 'Child' - assert grandchild.hierarchy == 'Parent:Grandchild' - assert parent.hierarchy == 'Parent' + assert child.hierarchy == "Child" + assert grandchild.hierarchy == "Parent:Grandchild" + assert parent.hierarchy == "Parent" def test_remove_child_with_children_with_keep_multi_level(): """Test removing a child category that has children""" - parent = Category(name='Parent') - child = Category(name='Child') - grandchild = Category(name='Grandchild') - great_grandchild = Category(name='GreatGrandchild') + parent = Category(name="Parent") + child = Category(name="Child") + grandchild = Category(name="Grandchild") + great_grandchild = Category(name="GreatGrandchild") parent.add_child(child) child.add_child(grandchild) grandchild.add_child(great_grandchild) @@ -405,10 +406,10 @@ def test_remove_child_with_children_with_keep_multi_level(): assert not child.children assert grandchild.children == [great_grandchild] assert not great_grandchild.children - assert child.hierarchy == 'Child' - assert grandchild.hierarchy == 'Parent:Grandchild' - assert great_grandchild.hierarchy == 'Parent:Grandchild:GreatGrandchild' - assert parent.hierarchy == 'Parent' + assert child.hierarchy == "Child" + assert grandchild.hierarchy == "Parent:Grandchild" + assert great_grandchild.hierarchy == "Parent:Grandchild:GreatGrandchild" + assert parent.hierarchy == "Parent" def test_render_tree(tree): @@ -427,9 +428,9 @@ def test_render_tree(tree): def test_render_tree_no_tree(): """Test rendering a tree of categories with no children""" - root = Category(name='Parent') + root = Category(name="Parent") tree = root.render_tree() - assert tree == 'Parent' + assert tree == "Parent" def test_traverse_down(tree): @@ -483,150 +484,149 @@ def test_traverse_up(tree): def test_find_child(tree): """Test finding a child""" parent, child1, child2, grandchild1, grandchild2 = tree - assert parent.find_child('Child1') == child1 - assert parent.find_child('Child2') == child2 - assert parent.find_child('Grandchild1') == grandchild1 - assert parent.find_child('Grandchild2') == grandchild2 - assert child1.find_child('Grandchild1') == grandchild1 - assert child1.find_child('Grandchild2') == grandchild2 + assert parent.find_child("Child1") == child1 + assert parent.find_child("Child2") == child2 + assert parent.find_child("Grandchild1") == grandchild1 + assert parent.find_child("Grandchild2") == grandchild2 + assert child1.find_child("Grandchild1") == grandchild1 + assert child1.find_child("Grandchild2") == grandchild2 - assert child2.find_child('Grandchild1') is None - assert child2.find_child('Grandchild2') is None - assert grandchild1.find_child('Grandchild1') is grandchild1 - assert grandchild1.find_child('Grandchild2') is None - assert grandchild2.find_child('Grandchild1') is None - assert grandchild2.find_child('Grandchild2') is grandchild2 + assert child2.find_child("Grandchild1") is None + assert child2.find_child("Grandchild2") is None + assert grandchild1.find_child("Grandchild1") is grandchild1 + assert grandchild1.find_child("Grandchild2") is None + assert grandchild2.find_child("Grandchild1") is None + assert grandchild2.find_child("Grandchild2") is grandchild2 def test_to_qif(): """Test converting a category to a QIF category""" - root = Category(name='Root') - child = Category(name='Child') + root = Category(name="Root") + child = Category(name="Child") root.add_child(child) assert root.to_qif() == ( # Should show both root and child - '!Type:Cat\nNRoot\nE\n^\n' - '!Type:Cat\nNRoot:Child\nE\n' + "!Type:Cat\nNRoot\nE\n^\n" "!Type:Cat\nNRoot:Child\nE\n" ) - assert child.to_qif() == '!Type:Cat\nNRoot:Child\nE\n' + assert child.to_qif() == "!Type:Cat\nNRoot:Child\nE\n" def test_to_qif_with_custom_fields(): """Test converting a category to a QIF category""" - setattr(Category, '__CUSTOM_FIELDS', []) # Reset custom fields - root = Category(name='Root') - child = Category(name='Child') + setattr(Category, "__CUSTOM_FIELDS", []) # Reset custom fields + root = Category(name="Root") + child = Category(name="Child") root.add_child(child) # Add custom fields Category.add_custom_field( - line_code='X', - attr='custom_field_1', + line_code="X", + attr="custom_field_1", field_type=str, ) Category.add_custom_field( - line_code='Y', - attr='custom_field_2', + line_code="Y", + attr="custom_field_2", field_type=Decimal, ) Category.add_custom_field( - line_code='DT', # Test multi-character line code - attr='custom_field_3', + line_code="DT", # Test multi-character line code + attr="custom_field_3", field_type=datetime, ) - root.custom_field_1 = 'Custom field 1' - root.custom_field_2 = Decimal('9238479') + root.custom_field_1 = "Custom field 1" + root.custom_field_2 = Decimal("9238479") root.custom_field_3 = datetime(2022, 1, 1, 0, 0, 0, 1) - child.custom_field_1 = 'Custom field 1' - child.custom_field_2 = Decimal('9238479') + child.custom_field_1 = "Custom field 1" + child.custom_field_2 = Decimal("9238479") child.custom_field_3 = datetime(2022, 1, 1, 0, 0, 0, 1) assert root.to_qif() == ( # Should show both root and child - '!Type:Cat\n' - 'NRoot\n' - 'E\n' - 'DT2022-01-01 00:00:00.000001\n' - 'Y9238479\n' - 'XCustom field 1\n' - '^\n' - '!Type:Cat\n' - 'NRoot:Child\n' - 'E\n' - 'DT2022-01-01 00:00:00.000001\n' - 'Y9238479\n' - 'XCustom field 1\n' + "!Type:Cat\n" + "NRoot\n" + "E\n" + "DT2022-01-01 00:00:00.000001\n" + "Y9238479\n" + "XCustom field 1\n" + "^\n" + "!Type:Cat\n" + "NRoot:Child\n" + "E\n" + "DT2022-01-01 00:00:00.000001\n" + "Y9238479\n" + "XCustom field 1\n" ) assert child.to_qif() == ( - '!Type:Cat\n' - 'NRoot:Child\n' - 'E\n' - 'DT2022-01-01 00:00:00.000001\n' - 'Y9238479\n' - 'XCustom field 1\n' + "!Type:Cat\n" + "NRoot:Child\n" + "E\n" + "DT2022-01-01 00:00:00.000001\n" + "Y9238479\n" + "XCustom field 1\n" ) - setattr(Category, '__CUSTOM_FIELDS', []) # Reset custom fields + setattr(Category, "__CUSTOM_FIELDS", []) # Reset custom fields def test_from_list_no_custom_fields(): """Test creating a category from a list of QIF strings""" qif_list = [ - 'NParent', - 'DSome category description', - 'E', - 'T', - 'B123.45', - 'RTax schedule info', + "NParent", + "DSome category description", + "E", + "T", + "B123.45", + "RTax schedule info", ] category = Category.from_list(qif_list) - assert category.name == 'Parent' - assert category.desc == 'Some category description' - assert category.category_type == 'expense' - assert category.tax_schedule_info == 'Tax schedule info' - assert category.budget_amount == Decimal('123.45') + assert category.name == "Parent" + assert category.desc == "Some category description" + assert category.category_type == "expense" + assert category.tax_schedule_info == "Tax schedule info" + assert category.budget_amount == Decimal("123.45") def test_from_list_with_custom_fields(): """Test creating a category from a list of QIF strings""" - setattr(Category, '__CUSTOM_FIELDS', []) # Reset custom fields + setattr(Category, "__CUSTOM_FIELDS", []) # Reset custom fields qif_list = [ - 'NParent', - 'DSome category description', - 'E', - 'T', - 'B123.45', - 'RTax schedule info', - 'XCustom field 1', - 'Y9238479', - 'DT2022-01-01T00:00:00.000001', + "NParent", + "DSome category description", + "E", + "T", + "B123.45", + "RTax schedule info", + "XCustom field 1", + "Y9238479", + "DT2022-01-01T00:00:00.000001", ] # Add custom fields Category.add_custom_field( - line_code='X', - attr='custom_field_1', + line_code="X", + attr="custom_field_1", field_type=str, ) Category.add_custom_field( - line_code='Y', - attr='custom_field_2', + line_code="Y", + attr="custom_field_2", field_type=Decimal, ) Category.add_custom_field( - line_code='DT', # Test multi-character line code - attr='custom_field_3', + line_code="DT", # Test multi-character line code + attr="custom_field_3", field_type=datetime, ) category = Category.from_list(qif_list) - assert category.name == 'Parent' - assert category.desc == 'Some category description' - assert category.category_type == 'expense' - assert category.tax_schedule_info == 'Tax schedule info' - assert category.budget_amount == Decimal('123.45') - assert category.custom_field_1 == 'Custom field 1' - assert category.custom_field_2 == Decimal('9238479') + assert category.name == "Parent" + assert category.desc == "Some category description" + assert category.category_type == "expense" + assert category.tax_schedule_info == "Tax schedule info" + assert category.budget_amount == Decimal("123.45") + assert category.custom_field_1 == "Custom field 1" + assert category.custom_field_2 == Decimal("9238479") assert category.custom_field_3 == datetime(2022, 1, 1, 0, 0, 0, 1) - setattr(Category, '__CUSTOM_FIELDS', []) # Reset custom fields + setattr(Category, "__CUSTOM_FIELDS", []) # Reset custom fields def test_from_list_with_unknown_line_code(): @@ -634,13 +634,13 @@ def test_from_list_with_unknown_line_code(): line code """ qif_list = [ - 'NParent', - 'DSome category description', - 'E', - 'T', - 'B123.45', - 'RTax schedule info', - 'ZInvalid field', + "NParent", + "DSome category description", + "E", + "T", + "B123.45", + "RTax schedule info", + "ZInvalid field", ] with pytest.raises(ValueError): @@ -650,58 +650,58 @@ def test_from_list_with_unknown_line_code(): def test_from_string_default_separator(): """Test creating a category from a QIF string""" qif_string = ( - 'NParent\n' - 'DSome category description\n' - 'E\n' - 'T\n' - 'B123.45\n' - 'RTax schedule info\n' + "NParent\n" + "DSome category description\n" + "E\n" + "T\n" + "B123.45\n" + "RTax schedule info\n" ) category = Category.from_string(qif_string) - assert category.name == 'Parent' - assert category.desc == 'Some category description' - assert category.category_type == 'expense' - assert category.tax_schedule_info == 'Tax schedule info' - assert category.budget_amount == Decimal('123.45') + assert category.name == "Parent" + assert category.desc == "Some category description" + assert category.category_type == "expense" + assert category.tax_schedule_info == "Tax schedule info" + assert category.budget_amount == Decimal("123.45") def test_from_string_custom_separator(): """Test creating a category from a QIF string with a custom separator""" qif_string = ( - 'NParent---' - 'DSome category description---' - 'E---' - 'T---' - 'B123.45---' - 'RTax schedule info---' + "NParent---" + "DSome category description---" + "E---" + "T---" + "B123.45---" + "RTax schedule info---" ) - category = Category.from_string(qif_string, separator='---') - assert category.name == 'Parent' - assert category.desc == 'Some category description' - assert category.category_type == 'expense' - assert category.tax_schedule_info == 'Tax schedule info' - assert category.budget_amount == Decimal('123.45') + category = Category.from_string(qif_string, separator="---") + assert category.name == "Parent" + assert category.desc == "Some category description" + assert category.category_type == "expense" + assert category.tax_schedule_info == "Tax schedule info" + assert category.budget_amount == Decimal("123.45") def test_create_categories_from_hierarchy(): """Test creating categories from a hierarchy string""" - hierarchy = 'Parent:Child:Grandchild' + hierarchy = "Parent:Child:Grandchild" grandchild = create_categories_from_hierarchy(hierarchy) - assert grandchild.name == 'Grandchild' - assert grandchild.parent.name == 'Child' - assert grandchild.parent.parent.name == 'Parent' + assert grandchild.name == "Grandchild" + assert grandchild.parent.name == "Child" + assert grandchild.parent.parent.name == "Parent" def test_create_categories_from_hierarchy_single_category(): """Test creating a single category from a hierarchy string""" - hierarchy = 'Parent' + hierarchy = "Parent" parent = create_categories_from_hierarchy(hierarchy) - assert parent == Category(name='Parent') + assert parent == Category(name="Parent") def test_add_categories_to_container_no_existing_categories_list(): """Test that a category is added to an empty list of categories.""" - new_category = Category(name='New Category') + new_category = Category(name="New Category") categories = [] add_categories_to_container(new_category, categories) assert categories == [new_category] @@ -709,29 +709,29 @@ def test_add_categories_to_container_no_existing_categories_list(): def test_add_categories_to_container_with_hierarchy_parent_not_in_list(): """Test that a category is added to a list of categories with hierarchy.""" - child = Category(name='Child') - child.set_parent(Category(name='Parent')) + child = Category(name="Child") + child.set_parent(Category(name="Parent")) categories = [] add_categories_to_container(child, categories) assert categories == [ - Category(name='Parent', children=[Category(name='Child')]), + Category(name="Parent", children=[Category(name="Child")]), ] def test_add_categories_to_container_with_hierarchy_parent_in_list(): """Test that a category is added to a list of categories with hierarchy.""" - parent = Category(name='Parent') - child = Category(name='Child') + parent = Category(name="Parent") + child = Category(name="Child") child.set_parent(parent) categories = [parent] add_categories_to_container(child, categories) - assert categories == [Category(name='Parent', children=[child])] + assert categories == [Category(name="Parent", children=[child])] def test_add_categories_to_container_with_existing_categories_list(): """Test that a category is added to a filled list of categories.""" - new_category = Category(name='New Category') - existing_category = Category(name='Existing Category') + new_category = Category(name="New Category") + existing_category = Category(name="Existing Category") categories = [existing_category] add_categories_to_container(new_category, categories) assert categories == [existing_category, new_category] @@ -739,34 +739,34 @@ def test_add_categories_to_container_with_existing_categories_list(): def test_add_categories_to_container_with_hierarchy_existing_categories_list(): """Test that a category is added to a filled list of categories.""" - child = Category(name='Child') - child.set_parent(Category(name='Parent')) - existing_category = Category(name='Existing Category') + child = Category(name="Child") + child.set_parent(Category(name="Parent")) + existing_category = Category(name="Existing Category") categories = [existing_category] add_categories_to_container(child, categories) assert categories == [ existing_category, - Category(name='Parent', children=[Category(name='Child')]), + Category(name="Parent", children=[Category(name="Child")]), ] def test_add_categories_to_container_complex_hierarchy_list(): """Test that a complex category hierarchy is added to a list of categories.""" - plant = Category(name='Plant') - tree = Category(name='Tree') + plant = Category(name="Plant") + tree = Category(name="Tree") tree.set_parent(plant) - oak = Category(name='Oak') + oak = Category(name="Oak") oak.set_parent(tree) - oak_leaf = Category(name='Oak Leaf') + oak_leaf = Category(name="Oak Leaf") oak_leaf.set_parent(oak) # Create separate list of categories with a root with the same name - tree2 = Category(name='Tree') - birch = Category(name='Birch') + tree2 = Category(name="Tree") + birch = Category(name="Birch") birch.set_parent(tree2) - birch_leaf = Category(name='Birch Leaf') + birch_leaf = Category(name="Birch Leaf") birch_leaf.set_parent(birch) categories = [plant] @@ -778,18 +778,18 @@ def test_add_categories_to_container_complex_hierarchy_list(): plant_render = categories[0].render_tree() assert plant_render == ( - 'Plant (root)\n' - '└─ Tree\n' - ' └─ Oak\n' - ' └─ Oak Leaf\n' - ' └─ Birch\n' - ' └─ Birch Leaf' + "Plant (root)\n" + "└─ Tree\n" + " └─ Oak\n" + " └─ Oak Leaf\n" + " └─ Birch\n" + " └─ Birch Leaf" ) def test_add_categories_to_container_no_existing_categories_dict(): """Test that a category is added to an empty dict of categories.""" - new_category = Category(name='New Category') + new_category = Category(name="New Category") categories = {} add_categories_to_container(new_category, categories) assert categories == {new_category.name: new_category} @@ -797,29 +797,29 @@ def test_add_categories_to_container_no_existing_categories_dict(): def test_add_categories_to_container_with_hierarchy_parent_not_in_dict(): """Test that a category is added to a dict of categories with hierarchy.""" - child = Category(name='Child') - child.set_parent(Category(name='Parent')) + child = Category(name="Child") + child.set_parent(Category(name="Parent")) categories = {} add_categories_to_container(child, categories) assert categories == { - 'Parent': Category(name='Parent', children=[Category(name='Child')]), + "Parent": Category(name="Parent", children=[Category(name="Child")]), } def test_add_categories_to_container_with_hierarchy_parent_in_dict(): """Test that a category is added to a dict of categories with hierarchy.""" - parent = Category(name='Parent') - child = Category(name='Child') + parent = Category(name="Parent") + child = Category(name="Child") child.set_parent(parent) categories = {parent.name: parent} add_categories_to_container(child, categories) - assert categories == {'Parent': Category(name='Parent', children=[child])} + assert categories == {"Parent": Category(name="Parent", children=[child])} def test_add_categories_to_container_with_existing_categories_dict(): """Test that a category is added to a filled dict of categories.""" - new_category = Category(name='New Category') - existing_category = Category(name='Existing Category') + new_category = Category(name="New Category") + existing_category = Category(name="Existing Category") categories = {existing_category.name: existing_category} add_categories_to_container(new_category, categories) assert categories == { @@ -830,34 +830,34 @@ def test_add_categories_to_container_with_existing_categories_dict(): def test_add_categories_to_container_with_hierarchy_existing_categories_dict(): """Test that a category is added to a filled dict of categories.""" - child = Category(name='Child') - child.set_parent(Category(name='Parent')) - existing_category = Category(name='Existing Category') + child = Category(name="Child") + child.set_parent(Category(name="Parent")) + existing_category = Category(name="Existing Category") categories = {existing_category.name: existing_category} add_categories_to_container(child, categories) assert categories == { existing_category.name: existing_category, - 'Parent': Category(name='Parent', children=[Category(name='Child')]), + "Parent": Category(name="Parent", children=[Category(name="Child")]), } def test_add_categories_to_container_complex_hierarchy_dict(): """Test that a complex category hierarchy is added to a dict of categories.""" - plant = Category(name='Plant') - tree = Category(name='Tree') + plant = Category(name="Plant") + tree = Category(name="Tree") tree.set_parent(plant) - oak = Category(name='Oak') + oak = Category(name="Oak") oak.set_parent(tree) - oak_leaf = Category(name='Oak Leaf') + oak_leaf = Category(name="Oak Leaf") oak_leaf.set_parent(oak) # Create separate list of categories with a root with the same name - tree2 = Category(name='Tree') - birch = Category(name='Birch') + tree2 = Category(name="Tree") + birch = Category(name="Birch") birch.set_parent(tree2) - birch_leaf = Category(name='Birch Leaf') + birch_leaf = Category(name="Birch Leaf") birch_leaf.set_parent(birch) categories = {plant.name: plant} @@ -869,12 +869,12 @@ def test_add_categories_to_container_complex_hierarchy_dict(): plant_render = categories[plant.name].render_tree() assert plant_render == ( - 'Plant (root)\n' - '└─ Tree\n' - ' └─ Oak\n' - ' └─ Oak Leaf\n' - ' └─ Birch\n' - ' └─ Birch Leaf' + "Plant (root)\n" + "└─ Tree\n" + " └─ Oak\n" + " └─ Oak Leaf\n" + " └─ Birch\n" + " └─ Birch Leaf" ) @@ -884,30 +884,28 @@ def test_categories_do_not_override_each_other(): Related to issue #23. https://github.com/isaacharrisholt/quiffen/issues/23 """ - test_file = ( - Path(__file__).parent / 'test_files' / 'test_category_override.qif' - ) + test_file = Path(__file__).parent / "test_files" / "test_category_override.qif" qif = Qif.parse(test_file) assert len(qif.categories) == 2 - assert sorted(qif.categories.keys()) == ['Everyday Expenses', 'Income'] + assert sorted(qif.categories.keys()) == ["Everyday Expenses", "Income"] - income = qif.categories['Income'] - assert income.name == 'Income' + income = qif.categories["Income"] + assert income.name == "Income" assert len(income.children) == 2 assert sorted(c.name for c in income.children) == [ - 'Available next month', - 'Available this month', + "Available next month", + "Available this month", ] - everyday_expenses = qif.categories['Everyday Expenses'] - assert everyday_expenses.name == 'Everyday Expenses' + everyday_expenses = qif.categories["Everyday Expenses"] + assert everyday_expenses.name == "Everyday Expenses" assert len(everyday_expenses.children) == 2 assert sorted(c.name for c in everyday_expenses.children) == [ - 'Food Budget', - 'Gousto', + "Food Budget", + "Gousto", ] @@ -917,19 +915,17 @@ def test_child_categories_inherit_category_type(): Related to issue #37. https://github.com/isaacharrisholt/quiffen/issues/37 """ - test_file = ( - Path(__file__).parent / 'test_files' / 'test_category_inherit.qif' - ) + test_file = Path(__file__).parent / "test_files" / "test_category_inherit.qif" qif = Qif.parse(test_file) assert len(qif.categories) == 1 - root = qif.categories['Root'] - assert root.name == 'Root' + root = qif.categories["Root"] + assert root.name == "Root" assert root.category_type == CategoryType.INCOME assert len(root.children) == 1 child = root.children[0] - assert child.name == 'Child' + assert child.name == "Child" assert child.category_type == CategoryType.INCOME diff --git a/tests/test_class_type.py b/tests/test_class_type.py index d193cf0..2e89774 100644 --- a/tests/test_class_type.py +++ b/tests/test_class_type.py @@ -9,76 +9,76 @@ def test_create_class(): """Test creating a class""" - cls = Class(name='Test') - assert cls.name == 'Test' + cls = Class(name="Test") + assert cls.name == "Test" assert cls.desc is None - cls2 = Class(name='Test2', desc='Test Description') - assert cls2.name == 'Test2' - assert cls2.desc == 'Test Description' + cls2 = Class(name="Test2", desc="Test Description") + assert cls2.name == "Test2" + assert cls2.desc == "Test Description" def test_eq_success(): """Test that two classes are equal""" - cls = Class(name='Test') - cls2 = Class(name='Test') + cls = Class(name="Test") + cls2 = Class(name="Test") assert cls == cls2 def test_eq_failure(): """Test that two classes are not equal""" - cls = Class(name='Test') - cls2 = Class(name='Test2') + cls = Class(name="Test") + cls2 = Class(name="Test2") assert cls != cls2 def test_str_method(): """Test the string representation of a class""" - cls = Class(name='Test') - assert str(cls) == 'Class:\n\tName: Test\n\tCategories: 0' + cls = Class(name="Test") + assert str(cls) == "Class:\n\tName: Test\n\tCategories: 0" - cls2 = Class(name='Test2', desc='Test Description') + cls2 = Class(name="Test2", desc="Test Description") assert str(cls2) == ( - 'Class:\n\t' - 'Name: Test2\n\t' - 'Description: Test Description\n\t' - 'Categories: 0' + "Class:\n\t" + "Name: Test2\n\t" + "Description: Test Description\n\t" + "Categories: 0" ) def test_to_dict(): """Test the to_dict method""" - cls = Class(name='Test') - assert cls.to_dict() == {'name': 'Test', 'desc': None, 'categories': []} + cls = Class(name="Test") + assert cls.to_dict() == {"name": "Test", "desc": None, "categories": []} - cls2 = Class(name='Test2', desc='Test Description') + cls2 = Class(name="Test2", desc="Test Description") assert cls2.to_dict() == { - 'name': 'Test2', - 'desc': 'Test Description', - 'categories': [], + "name": "Test2", + "desc": "Test Description", + "categories": [], } def test_to_dict_with_ignore(): """Test the to_dict method with ignore""" - cls = Class(name='Test', desc='Test Description') - assert cls.to_dict(ignore={'desc', 'categories'}) == {'name': 'Test'} + cls = Class(name="Test", desc="Test Description") + assert cls.to_dict(ignore={"desc", "categories"}) == {"name": "Test"} def test_add_category(): """Test adding a category to a class""" - cls = Class(name='Test') + cls = Class(name="Test") assert not cls.categories - category = Category(name='Test Category') + category = Category(name="Test Category") cls.add_category(category) assert cls.categories == [category] def test_add_existing_category(): """Test adding an existing category to a class""" - cls = Class(name='Test') - category = Category(name='Test Category') + cls = Class(name="Test") + category = Category(name="Test Category") cls.add_category(category) assert cls.categories == [category] @@ -88,80 +88,80 @@ def test_add_existing_category(): def test_merge(): """Test merging two classes""" - category1 = Category(name='Category 1') - category2 = Category(name='Category 2') - cls = Class(name='Test', categories=[category1]) - cls2 = Class(name='Test2', desc='Test Description', categories=[category2]) + category1 = Category(name="Category 1") + category2 = Category(name="Category 2") + cls = Class(name="Test", categories=[category1]) + cls2 = Class(name="Test2", desc="Test Description", categories=[category2]) cls.merge(cls2) - assert cls.name == 'Test' - assert cls.desc == 'Test Description' + assert cls.name == "Test" + assert cls.desc == "Test Description" assert cls.categories == [category1, category2] def test_to_qif(): """Test the to_qif method""" cls = Class( - name='Test Class', - desc='Test Description', + name="Test Class", + desc="Test Description", ) - assert cls.to_qif() == '!Type:Class\nNTest Class\nDTest Description\n' + assert cls.to_qif() == "!Type:Class\nNTest Class\nDTest Description\n" def test_from_list_no_custom_fields(): """Test creating a class from a list of QIF strings""" qif_list = [ - 'NTest', - 'DTest Description', + "NTest", + "DTest Description", ] cls = Class.from_list(qif_list) - assert cls.name == 'Test' - assert cls.desc == 'Test Description' + assert cls.name == "Test" + assert cls.desc == "Test Description" def test_from_list_with_custom_fields(): """Test creating a class from a list of QIF strings""" - setattr(Class, '__CUSTOM_FIELDS', []) # Reset custom fields + setattr(Class, "__CUSTOM_FIELDS", []) # Reset custom fields qif_list = [ - 'NTest', - 'DTest Description', - 'XCustom field 1', - 'Y9238479', - 'DT2022-01-01T00:00:00.000001', + "NTest", + "DTest Description", + "XCustom field 1", + "Y9238479", + "DT2022-01-01T00:00:00.000001", ] # Add custom fields Class.add_custom_field( - line_code='X', - attr='custom_field_1', + line_code="X", + attr="custom_field_1", field_type=str, ) Class.add_custom_field( - line_code='Y', - attr='custom_field_2', + line_code="Y", + attr="custom_field_2", field_type=Decimal, ) Class.add_custom_field( - line_code='DT', # Test multi-character line code - attr='custom_field_3', + line_code="DT", # Test multi-character line code + attr="custom_field_3", field_type=datetime, ) cls = Class.from_list(qif_list) - assert cls.name == 'Test' - assert cls.desc == 'Test Description' - assert cls.custom_field_1 == 'Custom field 1' - assert cls.custom_field_2 == Decimal('9238479') + assert cls.name == "Test" + assert cls.desc == "Test Description" + assert cls.custom_field_1 == "Custom field 1" + assert cls.custom_field_2 == Decimal("9238479") assert cls.custom_field_3 == datetime(2022, 1, 1, 0, 0, 0, 1) - setattr(Class, '__CUSTOM_FIELDS', []) # Reset custom fields + setattr(Class, "__CUSTOM_FIELDS", []) # Reset custom fields def test_from_list_with_unknown_line_code(): """Test creating a class from a list of QIF strings with an unknown line code""" qif_list = [ - 'NTest', - 'DTest Description', - 'ZCustom field 1', + "NTest", + "DTest Description", + "ZCustom field 1", ] with pytest.raises(ValueError): @@ -170,21 +170,15 @@ def test_from_list_with_unknown_line_code(): def test_from_string_default_separator(): """Test creating a class from a QIF string""" - qif_string = ( - 'NTest\n' - 'DTest Description\n' - ) + qif_string = "NTest\n" "DTest Description\n" cls = Class.from_string(qif_string) - assert cls.name == 'Test' - assert cls.desc == 'Test Description' + assert cls.name == "Test" + assert cls.desc == "Test Description" def test_from_string_custom_separator(): """Test creating a class from a QIF string with a custom separator""" - qif_string = ( - 'NTest---' - 'DTest Description---' - ) - cls = Class.from_string(qif_string, separator='---') - assert cls.name == 'Test' - assert cls.desc == 'Test Description' + qif_string = "NTest---" "DTest Description---" + cls = Class.from_string(qif_string, separator="---") + assert cls.name == "Test" + assert cls.desc == "Test Description" diff --git a/tests/test_investment.py b/tests/test_investment.py index 57b4208..27f82af 100644 --- a/tests/test_investment.py +++ b/tests/test_investment.py @@ -16,16 +16,16 @@ def test_create_investment(): investment2 = Investment( date=date, - action='Buy', - security='Test Security', + action="Buy", + security="Test Security", price=100, - memo='Test Memo', + memo="Test Memo", commission=10, ) - assert investment2.action == 'Buy' - assert investment2.security == 'Test Security' + assert investment2.action == "Buy" + assert investment2.security == "Test Security" assert investment2.price == 100 - assert investment2.memo == 'Test Memo' + assert investment2.memo == "Test Memo" assert investment2.commission == 10 @@ -34,18 +34,18 @@ def test_eq_success(): date = datetime(2022, 1, 1) investment = Investment( date=date, - action='Buy', - security='Test Security', + action="Buy", + security="Test Security", price=100, - memo='Test Memo', + memo="Test Memo", commission=10, ) investment2 = Investment( date=date, - action='Buy', - security='Test Security', + action="Buy", + security="Test Security", price=100, - memo='Test Memo', + memo="Test Memo", commission=10, ) assert investment == investment2 @@ -56,18 +56,18 @@ def test_eq_failure(): date = datetime(2022, 1, 1) investment = Investment( date=date, - action='Buy', - security='Test Security', + action="Buy", + security="Test Security", price=100, - memo='Test Memo', + memo="Test Memo", commission=10, ) investment2 = Investment( date=date, - action='Buy', - security='Test Security', + action="Buy", + security="Test Security", price=100, - memo='Test Memo', + memo="Test Memo", commission=20, ) assert investment != investment2 @@ -78,15 +78,15 @@ def test_str_method(): date = datetime(2022, 1, 1) investment = Investment( date=date, - action='Buy', - security='Test Security', + action="Buy", + security="Test Security", price=100, - memo='Test Memo', + memo="Test Memo", commission=10, ) assert str(investment) == ( - 'Investment:\n\tDate: 2022-01-01 00:00:00\n\tAction: Buy\n\tSecurity: ' - 'Test Security\n\tPrice: 100\n\tMemo: Test Memo\n\tCommission: 10' + "Investment:\n\tDate: 2022-01-01 00:00:00\n\tAction: Buy\n\tSecurity: " + "Test Security\n\tPrice: 100\n\tMemo: Test Memo\n\tCommission: 10" ) @@ -95,14 +95,14 @@ def test_to_qif(): date = datetime(2022, 1, 1) investment = Investment( date=date, - action='Buy', - security='Test Security', + action="Buy", + security="Test Security", price=100, - memo='Test Memo', + memo="Test Memo", commission=10, ) assert investment.to_qif() == ( - 'D2022-01-01\nNBuy\nYTest Security\nI100\nMTest Memo\nO10\n' + "D2022-01-01\nNBuy\nYTest Security\nI100\nMTest Memo\nO10\n" ) @@ -111,26 +111,26 @@ def test_to_dict(): date = datetime(2022, 1, 1) investment = Investment( date=date, - action='Buy', - security='Test Security', + action="Buy", + security="Test Security", price=100, - memo='Test Memo', + memo="Test Memo", commission=10, ) assert investment.to_dict() == { - 'date': date, - 'action': 'Buy', - 'security': 'Test Security', - 'price': Decimal('100'), - 'memo': 'Test Memo', - 'commission': Decimal('10'), - 'amount': None, - 'cleared': None, - 'first_line': None, - 'line_number': None, - 'quantity': None, - 'to_account': None, - 'transfer_amount': None, + "date": date, + "action": "Buy", + "security": "Test Security", + "price": Decimal("100"), + "memo": "Test Memo", + "commission": Decimal("10"), + "amount": None, + "cleared": None, + "first_line": None, + "line_number": None, + "quantity": None, + "to_account": None, + "transfer_amount": None, } @@ -139,86 +139,86 @@ def test_to_dict_with_ignore(): date = datetime(2022, 1, 1) investment = Investment( date=date, - action='Buy', - security='Test Security', + action="Buy", + security="Test Security", price=100, - memo='Test Memo', + memo="Test Memo", commission=10, ) - assert investment.to_dict(ignore=['action', 'security']) == { - 'date': date, - 'price': Decimal('100'), - 'memo': 'Test Memo', - 'commission': Decimal('10'), - 'amount': None, - 'cleared': None, - 'first_line': None, - 'line_number': None, - 'quantity': None, - 'to_account': None, - 'transfer_amount': None, + assert investment.to_dict(ignore=["action", "security"]) == { + "date": date, + "price": Decimal("100"), + "memo": "Test Memo", + "commission": Decimal("10"), + "amount": None, + "cleared": None, + "first_line": None, + "line_number": None, + "quantity": None, + "to_account": None, + "transfer_amount": None, } def test_from_list_no_custom_fields(): """Test creating an investment from a list with no custom fields""" qif_list = [ - 'D2022-01-01', - 'NBuy', - 'YTest Security', - 'I100', - 'MTest Memo', - 'O10', + "D2022-01-01", + "NBuy", + "YTest Security", + "I100", + "MTest Memo", + "O10", ] investment = Investment.from_list(qif_list) assert investment.date == datetime(2022, 1, 1) - assert investment.action == 'Buy' - assert investment.security == 'Test Security' + assert investment.action == "Buy" + assert investment.security == "Test Security" assert investment.price == 100 - assert investment.memo == 'Test Memo' + assert investment.memo == "Test Memo" assert investment.commission == 10 def test_from_list_with_custom_fields(): """Test creating an investment from a list with custom fields""" - setattr(Investment, '__CUSTOM_FIELDS', []) # Reset custom fields + setattr(Investment, "__CUSTOM_FIELDS", []) # Reset custom fields qif_list = [ - 'D2022-01-01', - 'NBuy', - 'YTest Security', - 'I100', - 'MTest Memo', - 'O10', - 'XCustom field 1', - 'Y9238479', - 'DT2022-01-01T00:00:00.000001', + "D2022-01-01", + "NBuy", + "YTest Security", + "I100", + "MTest Memo", + "O10", + "XCustom field 1", + "Y9238479", + "DT2022-01-01T00:00:00.000001", ] # Add custom fields Investment.add_custom_field( - line_code='X', - attr='custom_field_1', + line_code="X", + attr="custom_field_1", field_type=str, ) Investment.add_custom_field( - line_code='Y', - attr='custom_field_2', + line_code="Y", + attr="custom_field_2", field_type=Decimal, ) Investment.add_custom_field( - line_code='DT', # Test multi-character line code - attr='custom_field_3', + line_code="DT", # Test multi-character line code + attr="custom_field_3", field_type=datetime, ) investment = Investment.from_list(qif_list) assert investment.date == datetime(2022, 1, 1) - assert investment.action == 'Buy' - assert investment.security == 'Test Security' + assert investment.action == "Buy" + assert investment.security == "Test Security" assert investment.price == 100 - assert investment.memo == 'Test Memo' + assert investment.memo == "Test Memo" assert investment.commission == 10 - assert investment.custom_field_1 == 'Custom field 1' - assert investment.custom_field_2 == Decimal('9238479') + assert investment.custom_field_1 == "Custom field 1" + assert investment.custom_field_2 == Decimal("9238479") assert investment.custom_field_3 == datetime(2022, 1, 1, 0, 0, 0, 1) - setattr(Investment, '__CUSTOM_FIELDS', []) # Reset custom fields + setattr(Investment, "__CUSTOM_FIELDS", []) # Reset custom fields diff --git a/tests/test_qif.py b/tests/test_qif.py index 81f9f29..8d44770 100644 --- a/tests/test_qif.py +++ b/tests/test_qif.py @@ -16,27 +16,27 @@ @pytest.fixture def qif_file(): - return Path(__file__).parent / 'test_files' / 'test.qif' + return Path(__file__).parent / "test_files" / "test.qif" @pytest.fixture def qif_file_with_oth_a_account(): - return Path(__file__).parent / 'test_files' / 'test_oth_a.qif' + return Path(__file__).parent / "test_files" / "test_oth_a.qif" @pytest.fixture def empty_file(): - return Path(__file__).parent / 'test_files' / 'empty.qif' + return Path(__file__).parent / "test_files" / "empty.qif" @pytest.fixture def txt_file(): - return Path(__file__).parent / 'test_files' / 'invalid.txt' + return Path(__file__).parent / "test_files" / "invalid.txt" @pytest.fixture def nonexistent_file(): - return Path(__file__).parent / 'test_files' / 'nonexistent.qif' + return Path(__file__).parent / "test_files" / "nonexistent.qif" def test_create_qif(): @@ -57,27 +57,27 @@ def test_eq_success_empty_qif(): def test_eq_success_with_accounts(): """Test that two Qif instances are equal when they have accounts""" qif = Qif() - qif.add_account(Account(name='Test Account')) + qif.add_account(Account(name="Test Account")) qif2 = Qif() - qif2.add_account(Account(name='Test Account')) + qif2.add_account(Account(name="Test Account")) assert qif == qif2 def test_eq_success_with_classes(): """Test that two Qif instances are equal when they have classes""" qif = Qif() - qif.add_class(Class(name='Test Class')) + qif.add_class(Class(name="Test Class")) qif2 = Qif() - qif2.add_class(Class(name='Test Class')) + qif2.add_class(Class(name="Test Class")) assert qif == qif2 def test_eq_success_with_categories(): """Test that two Qif instances are equal when they have categories""" qif = Qif() - qif.add_category(Category(name='Test Category')) + qif.add_category(Category(name="Test Category")) qif2 = Qif() - qif2.add_category(Category(name='Test Category')) + qif2.add_category(Category(name="Test Category")) assert qif == qif2 @@ -85,9 +85,9 @@ def test_eq_failure_with_accounts(): """Test that two Qif instances are not equal when they have different accounts""" qif = Qif() - qif.add_account(Account(name='Test Account')) + qif.add_account(Account(name="Test Account")) qif2 = Qif() - qif2.add_account(Account(name='Test Account 2')) + qif2.add_account(Account(name="Test Account 2")) assert qif != qif2 @@ -95,9 +95,9 @@ def test_eq_failure_with_categories(): """Test that two Qif instances are not equal when they have different categories""" qif = Qif() - qif.add_category(Category(name='Test Category')) + qif.add_category(Category(name="Test Category")) qif2 = Qif() - qif2.add_category(Category(name='Test Category 2')) + qif2.add_category(Category(name="Test Category 2")) assert qif != qif2 @@ -105,88 +105,88 @@ def test_eq_failure_with_classes(): """Test that two Qif instances are not equal when they have different classes""" qif = Qif() - qif.add_class(Class(name='Test Class')) + qif.add_class(Class(name="Test Class")) qif2 = Qif() - qif2.add_class(Class(name='Test Class 2')) + qif2.add_class(Class(name="Test Class 2")) assert qif != qif2 def test_str_method_empty_qif(): """Test the string representation of an empty Qif instance""" qif = Qif() - assert str(qif) == 'Empty Qif object' + assert str(qif) == "Empty Qif object" def test_str_method_with_accounts(): """Test the string representation of a Qif instance with accounts""" qif = Qif() - qif.add_account(Account(name='Test Account')) + qif.add_account(Account(name="Test Account")) assert str(qif) == ( - 'QIF\n' - '===\n\n' - 'Accounts\n' - '--------\n\n' - 'Account:\n\t' - 'Name: Test Account\n\n' + "QIF\n" + "===\n\n" + "Accounts\n" + "--------\n\n" + "Account:\n\t" + "Name: Test Account\n\n" ) def test_str_method_with_categories(): """Test the string representation of a Qif instance with categories""" qif = Qif() - qif.add_category(Category(name='Test Category')) + qif.add_category(Category(name="Test Category")) print(repr(str(qif))) assert str(qif) == ( - 'QIF\n' - '===\n\n' - 'Categories\n' - '----------\n\n' - 'Category:\n\t' - 'Name: Test Category\n\t' - 'Category Type: expense\n\t' - 'Hierarchy: Test Category\n\n' + "QIF\n" + "===\n\n" + "Categories\n" + "----------\n\n" + "Category:\n\t" + "Name: Test Category\n\t" + "Category Type: expense\n\t" + "Hierarchy: Test Category\n\n" ) def test_str_method_with_classes(): """Test the string representation of a Qif instance with classes""" qif = Qif() - qif.add_class(Class(name='Test Class')) + qif.add_class(Class(name="Test Class")) assert str(qif) == ( - 'QIF\n' - '===\n\n' - 'Classes\n' - '-------\n\n' - 'Class:\n\t' - 'Name: Test Class\n\t' - 'Categories: 0\n\n' + "QIF\n" + "===\n\n" + "Classes\n" + "-------\n\n" + "Class:\n\t" + "Name: Test Class\n\t" + "Categories: 0\n\n" ) def test_str_method_with_all(): """Test the string representation of a Qif instance with all data""" qif = Qif() - qif.add_account(Account(name='Test Account')) - qif.add_class(Class(name='Test Class')) - qif.add_category(Category(name='Test Category')) + qif.add_account(Account(name="Test Account")) + qif.add_class(Class(name="Test Class")) + qif.add_category(Category(name="Test Category")) assert str(qif) == ( - 'QIF\n' - '===\n\n' - 'Accounts\n' - '--------\n\n' - 'Account:\n\t' - 'Name: Test Account\n\n' - 'Categories\n' - '----------\n\n' - 'Category:\n\t' - 'Name: Test Category\n\t' - 'Category Type: expense\n\t' - 'Hierarchy: Test Category\n\n' - 'Classes\n' - '-------\n\n' - 'Class:\n\t' - 'Name: Test Class\n\t' - 'Categories: 0\n\n' + "QIF\n" + "===\n\n" + "Accounts\n" + "--------\n\n" + "Account:\n\t" + "Name: Test Account\n\n" + "Categories\n" + "----------\n\n" + "Category:\n\t" + "Name: Test Category\n\t" + "Category Type: expense\n\t" + "Hierarchy: Test Category\n\n" + "Classes\n" + "-------\n\n" + "Class:\n\t" + "Name: Test Class\n\t" + "Categories: 0\n\n" ) @@ -222,7 +222,7 @@ def test_parse_non_qif_file(txt_file): def test_parse_nonexistent_file(): """Test parsing a file that does not exist""" with pytest.raises(ParserException): - Qif.parse('nonexistent_file.qif') + Qif.parse("nonexistent_file.qif") def test_parsed_accounts(qif_file): @@ -231,20 +231,19 @@ def test_parsed_accounts(qif_file): # Validate the account is the default account assert len(qif.accounts) == 1 - account_name = 'Quiffen Default Account' + account_name = "Quiffen Default Account" account = qif.accounts[account_name] assert account.name == account_name assert account.desc == ( - 'The default account created by Quiffen when no other accounts were ' - 'present' + "The default account created by Quiffen when no other accounts were " "present" ) def test_parsed_transactions(qif_file): """Test that the transactions are parsed correctly""" qif = Qif.parse(qif_file) - account = qif.accounts['Quiffen Default Account'] + account = qif.accounts["Quiffen Default Account"] # Validate the transactions transactions = account.transactions @@ -265,32 +264,32 @@ def test_parsed_categories(qif_file): # Validate categories assert len(qif.categories) == 4 - expected_categories = ['Bills', 'Food', 'Investments', 'Miscellaneous'] + expected_categories = ["Bills", "Food", "Investments", "Miscellaneous"] assert sorted(qif.categories.keys()) == expected_categories # Validate bills category - bills = qif.categories['Bills'] + bills = qif.categories["Bills"] assert len(bills.children) == 1 - assert bills.children[0].name == 'Cell Phone' + assert bills.children[0].name == "Cell Phone" assert bills.children[0].parent == bills # Validate food category - food = qif.categories['Food'] + food = qif.categories["Food"] assert len(food.children) == 1 - assert food.children[0].name == 'Groceries' + assert food.children[0].name == "Groceries" assert food.children[0].parent == food # Validate investments category - investments = qif.categories['Investments'] + investments = qif.categories["Investments"] investment_child_categories = sorted(investments.children) assert len(investment_child_categories) == 2 - assert investment_child_categories[0].name == 'Bonds' + assert investment_child_categories[0].name == "Bonds" assert investment_child_categories[0].parent == investments - assert investment_child_categories[1].name == 'Stocks' + assert investment_child_categories[1].name == "Stocks" assert investment_child_categories[1].parent == investments # Validate miscellaneous category - miscellaneous = qif.categories['Miscellaneous'] + miscellaneous = qif.categories["Miscellaneous"] assert len(miscellaneous.children) == 0 @@ -300,26 +299,22 @@ def test_parsed_classes(qif_file): # Validate classes assert len(qif.classes) == 2 - assert 'Test class' in qif.classes - assert 'Test class 2' in qif.classes + assert "Test class" in qif.classes + assert "Test class 2" in qif.classes # Validate test class - test_class = qif.classes['Test class'] - assert test_class.name == 'Test class' - assert test_class.desc == ( - 'This is just a class I added here for test purposes' - ) + test_class = qif.classes["Test class"] + assert test_class.name == "Test class" + assert test_class.desc == ("This is just a class I added here for test purposes") assert len(test_class.categories) == 1 cell_phone = test_class.categories[0] - assert cell_phone.name == 'Cell Phone' - assert cell_phone.parent.name == 'Bills' + assert cell_phone.name == "Cell Phone" + assert cell_phone.parent.name == "Bills" # Validate test class 2 - test_class_2 = qif.classes['Test class 2'] - assert test_class_2.name == 'Test class 2' - assert test_class_2.desc == ( - 'This is just a class I added here for test purposes' - ) + test_class_2 = qif.classes["Test class 2"] + assert test_class_2.name == "Test class 2" + assert test_class_2.desc == ("This is just a class I added here for test purposes") assert len(test_class_2.categories) == 0 @@ -333,53 +328,53 @@ def test_parsed_securities(qif_file): assert len(qif.securities) == 3 assert sorted(qif.securities.keys()) == [ - 'G002864', - 'M039728', - 'USD0000', + "G002864", + "M039728", + "USD0000", ] - assert qif.securities['G002864'].name == '' - assert qif.securities['G002864'].symbol == 'G002864' - assert qif.securities['G002864'].type == 'Stock' - assert qif.securities['G002864'].goal == 'Growth' + assert qif.securities["G002864"].name == "" + assert qif.securities["G002864"].symbol == "G002864" + assert qif.securities["G002864"].type == "Stock" + assert qif.securities["G002864"].goal == "Growth" - assert qif.securities['M039728'].name == '' - assert qif.securities['M039728'].symbol == 'M039728' - assert qif.securities['M039728'].type == 'Stock' - assert qif.securities['M039728'].goal == 'Growth' + assert qif.securities["M039728"].name == "" + assert qif.securities["M039728"].symbol == "M039728" + assert qif.securities["M039728"].type == "Stock" + assert qif.securities["M039728"].goal == "Growth" - assert qif.securities['USD0000'].name == '' - assert qif.securities['USD0000'].symbol == 'USD0000' - assert qif.securities['USD0000'].type == 'Stock' - assert qif.securities['USD0000'].goal == 'Growth' + assert qif.securities["USD0000"].name == "" + assert qif.securities["USD0000"].symbol == "USD0000" + assert qif.securities["USD0000"].type == "Stock" + assert qif.securities["USD0000"].goal == "Growth" def test_add_account(): """Test adding an account""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") qif.add_account(account) assert len(qif.accounts) == 1 - assert qif.accounts['Test Account'] == account + assert qif.accounts["Test Account"] == account def test_add_existing_account(): """Test adding an account that already exists""" qif = Qif() - account = Account(name='Test Account') - account2 = Account(name='Test Account') + account = Account(name="Test Account") + account2 = Account(name="Test Account") qif.add_account(account) qif.add_account(account2) assert len(qif.accounts) == 1 - assert 'Test Account' in qif.accounts + assert "Test Account" in qif.accounts def test_remove_account(): """Test removing an account""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") qif.add_account(account) - removed = qif.remove_account('Test Account') + removed = qif.remove_account("Test Account") assert len(qif.accounts) == 0 assert removed == account @@ -388,35 +383,35 @@ def test_remove_nonexistent_account(): """Test removing an account that does not exist""" qif = Qif() with pytest.raises(KeyError): - qif.remove_account('Test Account') + qif.remove_account("Test Account") def test_add_category(): """Test adding a category""" qif = Qif() - category = Category(name='Test Category') + category = Category(name="Test Category") qif.add_category(category) assert len(qif.categories) == 1 - assert qif.categories['Test Category'] == category + assert qif.categories["Test Category"] == category def test_add_existing_category(): """Test adding a category that already exists""" qif = Qif() - category = Category(name='Test Category') - category2 = Category(name='Test Category') + category = Category(name="Test Category") + category2 = Category(name="Test Category") qif.add_category(category) qif.add_category(category2) assert len(qif.categories) == 1 - assert 'Test Category' in qif.categories + assert "Test Category" in qif.categories def test_remove_category(): """Test removing a category""" qif = Qif() - category = Category(name='Test Category') + category = Category(name="Test Category") qif.add_category(category) - removed = qif.remove_category('Test Category') + removed = qif.remove_category("Test Category") assert len(qif.categories) == 0 assert removed == category @@ -424,13 +419,13 @@ def test_remove_category(): def test_remove_category_keep_children(): """Test removing a category and keeping its children""" qif = Qif() - category = Category(name='Test Category') - child_category = Category(name='Child Category', parent=category) + category = Category(name="Test Category") + child_category = Category(name="Child Category", parent=category) qif.add_category(category) qif.add_category(child_category) - removed = qif.remove_category('Test Category', keep_children=True) + removed = qif.remove_category("Test Category", keep_children=True) assert len(qif.categories) == 1 - assert 'Child Category' in qif.categories + assert "Child Category" in qif.categories assert removed == category @@ -438,35 +433,35 @@ def test_remove_category_nonexistent_category(): """Test removing a category that does not exist""" qif = Qif() with pytest.raises(KeyError): - qif.remove_category('Test Category') + qif.remove_category("Test Category") def test_add_class(): """Test adding a class""" qif = Qif() - cls = Class(name='Test Class') + cls = Class(name="Test Class") qif.add_class(cls) assert len(qif.classes) == 1 - assert qif.classes['Test Class'] == cls + assert qif.classes["Test Class"] == cls def test_add_existing_class(): """Test adding a class that already exists""" qif = Qif() - cls = Class(name='Test Class') - cls2 = Class(name='Test Class') + cls = Class(name="Test Class") + cls2 = Class(name="Test Class") qif.add_class(cls) qif.add_class(cls2) assert len(qif.classes) == 1 - assert 'Test Class' in qif.classes + assert "Test Class" in qif.classes def test_remove_class(): """Test removing a class""" qif = Qif() - cls = Class(name='Test Class') + cls = Class(name="Test Class") qif.add_class(cls) - removed = qif.remove_class('Test Class') + removed = qif.remove_class("Test Class") assert len(qif.classes) == 0 assert removed == cls @@ -475,35 +470,35 @@ def test_remove_nonexistent_class(): """Test removing a class that does not exist""" qif = Qif() with pytest.raises(KeyError): - qif.remove_class('Test Class') + qif.remove_class("Test Class") def test_add_security(): """Test adding a security""" qif = Qif() - security = Security(symbol='TEST') + security = Security(symbol="TEST") qif.add_security(security) assert len(qif.securities) == 1 - assert qif.securities['TEST'] == security + assert qif.securities["TEST"] == security def test_add_existing_security(): """Test adding a security that already exists""" qif = Qif() - security = Security(symbol='TEST') - security2 = Security(symbol='TEST') + security = Security(symbol="TEST") + security2 = Security(symbol="TEST") qif.add_security(security) qif.add_security(security2) assert len(qif.securities) == 1 - assert 'TEST' in qif.securities + assert "TEST" in qif.securities def test_remove_security(): """Test removing a security""" qif = Qif() - security = Security(symbol='TEST') + security = Security(symbol="TEST") qif.add_security(security) - removed = qif.remove_security('TEST') + removed = qif.remove_security("TEST") assert len(qif.securities) == 0 assert removed == security @@ -512,7 +507,7 @@ def test_remove_nonexistent_security(): """Test removing a security that does not exist""" qif = Qif() with pytest.raises(KeyError): - qif.remove_security('TEST') + qif.remove_security("TEST") def test_to_qif(qif_file): @@ -525,7 +520,7 @@ def test_to_qif(qif_file): original file. """ qif = Qif.parse(qif_file) - test_file = qif_file.parent / 'test_output.qif' + test_file = qif_file.parent / "test_output.qif" qif.to_qif(test_file) resulting_qif = Qif.parse(test_file) @@ -539,13 +534,13 @@ def test_to_qif(qif_file): def test_get_data_dicts_transactions(): """Test the get_data_dicts method with transactions""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") transaction = Transaction( date=datetime(2019, 1, 1), - amount=Decimal('100'), - payee='Test Payee', - memo='Test Memo', - category=Category(name='Test Category'), + amount=Decimal("100"), + payee="Test Payee", + memo="Test Memo", + category=Category(name="Test Category"), ) account.set_header(AccountType.BANK) account.add_transaction(transaction) @@ -554,7 +549,7 @@ def test_get_data_dicts_transactions(): assert len(data_dicts) == 1 expected = transaction.to_dict() - expected['date'] = '2019-01-01' # Dates are converted to strings + expected["date"] = "2019-01-01" # Dates are converted to strings assert data_dicts[0] == expected @@ -562,35 +557,35 @@ def test_get_data_dicts_transactions_with_date_format_and_ignore(): """Test the get_data_dicts method with transactions and date format and ignore""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") transaction = Transaction( date=datetime(2019, 1, 1), - amount=Decimal('100'), - payee='Test Payee', - memo='Test Memo', - category=Category(name='Test Category'), + amount=Decimal("100"), + payee="Test Payee", + memo="Test Memo", + category=Category(name="Test Category"), ) account.set_header(AccountType.BANK) account.add_transaction(transaction) qif.add_account(account) data_dicts = qif._get_data_dicts( data_type=QifDataType.TRANSACTIONS, - date_format='%d/%m/%Y', - ignore=['payee', 'memo'], + date_format="%d/%m/%Y", + ignore=["payee", "memo"], ) assert len(data_dicts) == 1 expected = transaction.to_dict() - expected['date'] = '01/01/2019' # Dates are converted to strings - del expected['payee'] - del expected['memo'] + expected["date"] = "01/01/2019" # Dates are converted to strings + del expected["payee"] + del expected["memo"] assert data_dicts[0] == expected def test_get_data_dicts_categories(): """Test the get_data_dicts method with categories""" qif = Qif() - category = Category(name='Test Category') + category = Category(name="Test Category") qif.add_category(category) data_dicts = qif._get_data_dicts(data_type=QifDataType.CATEGORIES) @@ -601,7 +596,7 @@ def test_get_data_dicts_categories(): def test_get_data_dicts_classes(): """Test the get_data_dicts method with classes""" qif = Qif() - cls = Class(name='Test Class') + cls = Class(name="Test Class") qif.add_class(cls) data_dicts = qif._get_data_dicts(data_type=QifDataType.CLASSES) @@ -612,26 +607,26 @@ def test_get_data_dicts_classes(): def test_get_data_dicts_accounts(): """Test the get_data_dicts method with accounts""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") account.set_header(AccountType.BANK) qif.add_account(account) data_dicts = qif._get_data_dicts(data_type=QifDataType.ACCOUNTS) assert len(data_dicts) == 1 expected = account.to_dict() - del expected['_last_header'] + del expected["_last_header"] assert data_dicts[0] == expected def test_get_data_dicts_investments(): """Test the get_data_dicts method with investments""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") investment = Investment( date=datetime(2019, 1, 1), - amount=Decimal('100'), - security='Test Security', - price=Decimal('10'), + amount=Decimal("100"), + security="Test Security", + price=Decimal("10"), ) account.set_header(AccountType.INVST) account.add_transaction(investment) @@ -640,38 +635,40 @@ def test_get_data_dicts_investments(): assert len(data_dicts) == 1 expected = investment.to_dict() - expected['date'] = '2019-01-01' # Dates are converted to strings + expected["date"] = "2019-01-01" # Dates are converted to strings assert data_dicts[0] == expected def test_get_data_dicts_securities(): """Test the get_data_dicts method with securities""" qif = Qif() - security = Security(symbol='TEST') + security = Security(symbol="TEST") qif.add_security(security) data_dicts = qif._get_data_dicts(data_type=QifDataType.SECURITIES) assert len(data_dicts) == 1 assert data_dicts[0] == security.to_dict() + + # pylint: enable=protected-access def test_to_csv_transactions(): """Test the to_csv method with transactions""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") transaction = Transaction( date=datetime(2019, 1, 1), - amount=Decimal('100'), - payee='Test Payee', - memo='Test Memo', - category=Category(name='Test Category'), + amount=Decimal("100"), + payee="Test Payee", + memo="Test Memo", + category=Category(name="Test Category"), ) account.set_header(AccountType.BANK) account.add_transaction(transaction) qif.add_account(account) - csv_file = Path(__file__).parent / 'test_files' / 'test_output.csv' + csv_file = Path(__file__).parent / "test_files" / "test_output.csv" qif.to_csv(path=csv_file, data_type=QifDataType.TRANSACTIONS) assert csv_file.exists() @@ -681,21 +678,21 @@ def test_to_csv_transactions(): results = list(reader) assert len(results) == 1 - assert results[0]['date'] == '2019-01-01' - assert results[0]['amount'] == '100' - assert results[0]['payee'] == 'Test Payee' - assert results[0]['memo'] == 'Test Memo' - assert 'Test Category' in results[0]['category'] + assert results[0]["date"] == "2019-01-01" + assert results[0]["amount"] == "100" + assert results[0]["payee"] == "Test Payee" + assert results[0]["memo"] == "Test Memo" + assert "Test Category" in results[0]["category"] csv_file.unlink() def test_to_csv_categories(): """Test the to_csv method with categories""" qif = Qif() - category = Category(name='Test Category') + category = Category(name="Test Category") qif.add_category(category) - csv_file = Path(__file__).parent / 'test_files' / 'test_output.csv' + csv_file = Path(__file__).parent / "test_files" / "test_output.csv" qif.to_csv(path=csv_file, data_type=QifDataType.CATEGORIES) assert csv_file.exists() @@ -705,17 +702,17 @@ def test_to_csv_categories(): results = list(reader) assert len(results) == 1 - assert results[0]['name'] == 'Test Category' + assert results[0]["name"] == "Test Category" csv_file.unlink() def test_to_csv_classes(): """Test the to_csv method with classes""" qif = Qif() - cls = Class(name='Test Class', desc='Test Description') + cls = Class(name="Test Class", desc="Test Description") qif.add_class(cls) - csv_file = Path(__file__).parent / 'test_files' / 'test_output.csv' + csv_file = Path(__file__).parent / "test_files" / "test_output.csv" qif.to_csv(path=csv_file, data_type=QifDataType.CLASSES) assert csv_file.exists() @@ -725,19 +722,19 @@ def test_to_csv_classes(): results = list(reader) assert len(results) == 1 - assert results[0]['name'] == 'Test Class' - assert results[0]['desc'] == 'Test Description' + assert results[0]["name"] == "Test Class" + assert results[0]["desc"] == "Test Description" csv_file.unlink() def test_to_csv_accounts(): """Test the to_csv method with accounts""" qif = Qif() - account = Account(name='Test Account', account_type='Bank') + account = Account(name="Test Account", account_type="Bank") account.set_header(AccountType.BANK) qif.add_account(account) - csv_file = Path(__file__).parent / 'test_files' / 'test_output.csv' + csv_file = Path(__file__).parent / "test_files" / "test_output.csv" qif.to_csv(path=csv_file, data_type=QifDataType.ACCOUNTS) assert csv_file.exists() @@ -747,26 +744,26 @@ def test_to_csv_accounts(): results = list(reader) assert len(results) == 1 - assert results[0]['name'] == 'Test Account' - assert results[0]['account_type'] == 'Bank' + assert results[0]["name"] == "Test Account" + assert results[0]["account_type"] == "Bank" csv_file.unlink() def test_to_csv_investments(): """Test the to_csv method with investments""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") investment = Investment( date=datetime(2019, 1, 1), - amount=Decimal('100'), - security='Test Security', - price=Decimal('10'), + amount=Decimal("100"), + security="Test Security", + price=Decimal("10"), ) account.set_header(AccountType.INVST) account.add_transaction(investment) qif.add_account(account) - csv_file = Path(__file__).parent / 'test_files' / 'test_output.csv' + csv_file = Path(__file__).parent / "test_files" / "test_output.csv" qif.to_csv(path=csv_file, data_type=QifDataType.INVESTMENTS) assert csv_file.exists() @@ -776,20 +773,20 @@ def test_to_csv_investments(): results = list(reader) assert len(results) == 1 - assert results[0]['date'] == '2019-01-01' - assert results[0]['amount'] == '100' - assert results[0]['security'] == 'Test Security' - assert results[0]['price'] == '10' + assert results[0]["date"] == "2019-01-01" + assert results[0]["amount"] == "100" + assert results[0]["security"] == "Test Security" + assert results[0]["price"] == "10" csv_file.unlink() def test_to_csv_securities(): """Test the to_csv method with securities""" qif = Qif() - security = Security(symbol='TEST') + security = Security(symbol="TEST") qif.add_security(security) - csv_file = Path(__file__).parent / 'test_files' / 'test_output.csv' + csv_file = Path(__file__).parent / "test_files" / "test_output.csv" qif.to_csv(path=csv_file, data_type=QifDataType.SECURITIES) assert csv_file.exists() @@ -799,31 +796,31 @@ def test_to_csv_securities(): results = list(reader) assert len(results) == 1 - assert results[0]['symbol'] == 'TEST' + assert results[0]["symbol"] == "TEST" csv_file.unlink() def test_to_csv_transactions_with_date_format_and_ignore_list(): """Test the to_csv method with transactions with date format and ignore""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") transaction = Transaction( date=datetime(2019, 2, 1), - amount=Decimal('100'), - payee='Test Payee', - memo='Test Memo', - category=Category(name='Test Category'), + amount=Decimal("100"), + payee="Test Payee", + memo="Test Memo", + category=Category(name="Test Category"), ) account.set_header(AccountType.BANK) account.add_transaction(transaction) qif.add_account(account) - csv_file = Path(__file__).parent / 'test_files' / 'test_output.csv' + csv_file = Path(__file__).parent / "test_files" / "test_output.csv" qif.to_csv( path=csv_file, data_type=QifDataType.TRANSACTIONS, - date_format='%m/%d/%Y', - ignore=['payee'], + date_format="%m/%d/%Y", + ignore=["payee"], ) assert csv_file.exists() @@ -833,35 +830,35 @@ def test_to_csv_transactions_with_date_format_and_ignore_list(): results = list(reader) assert len(results) == 1 - assert results[0]['date'] == '02/01/2019' - assert results[0]['amount'] == '100' - assert 'payee' not in results[0] - assert results[0]['memo'] == 'Test Memo' - assert 'Test Category' in results[0]['category'] + assert results[0]["date"] == "02/01/2019" + assert results[0]["amount"] == "100" + assert "payee" not in results[0] + assert results[0]["memo"] == "Test Memo" + assert "Test Category" in results[0]["category"] csv_file.unlink() def test_to_csv_transactions_multiple(): """Test the to_csv method with multiple transactions""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") transaction = Transaction( date=datetime(2019, 1, 1), - amount=Decimal('100'), - payee='Test Payee', - memo='Test Memo', - category=Category(name='Test Category'), + amount=Decimal("100"), + payee="Test Payee", + memo="Test Memo", + category=Category(name="Test Category"), ) account.set_header(AccountType.BANK) account.add_transaction(transaction) transaction2 = transaction.copy() - transaction2.amount = Decimal('200') + transaction2.amount = Decimal("200") account.add_transaction(transaction2) qif.add_account(account) - csv_file = Path(__file__).parent / 'test_files' / 'test_output.csv' + csv_file = Path(__file__).parent / "test_files" / "test_output.csv" qif.to_csv(path=csv_file, data_type=QifDataType.TRANSACTIONS) assert csv_file.exists() @@ -871,29 +868,29 @@ def test_to_csv_transactions_multiple(): results = list(reader) assert len(results) == 2 - assert results[0]['date'] == '2019-01-01' - assert results[0]['amount'] == '100' - assert results[0]['payee'] == 'Test Payee' - assert results[0]['memo'] == 'Test Memo' - assert 'Test Category' in results[0]['category'] - assert results[1]['date'] == '2019-01-01' - assert results[1]['amount'] == '200' - assert results[1]['payee'] == 'Test Payee' - assert results[1]['memo'] == 'Test Memo' - assert 'Test Category' in results[1]['category'] + assert results[0]["date"] == "2019-01-01" + assert results[0]["amount"] == "100" + assert results[0]["payee"] == "Test Payee" + assert results[0]["memo"] == "Test Memo" + assert "Test Category" in results[0]["category"] + assert results[1]["date"] == "2019-01-01" + assert results[1]["amount"] == "200" + assert results[1]["payee"] == "Test Payee" + assert results[1]["memo"] == "Test Memo" + assert "Test Category" in results[1]["category"] csv_file.unlink() def test_to_dataframe_transactions(): """Test the to_dataframe method with transactions""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") transaction = Transaction( date=datetime(2019, 1, 1), - amount=Decimal('100'), - payee='Test Payee', - memo='Test Memo', - category=Category(name='Test Category'), + amount=Decimal("100"), + payee="Test Payee", + memo="Test Memo", + category=Category(name="Test Category"), ) account.set_header(AccountType.BANK) account.add_transaction(transaction) @@ -902,76 +899,76 @@ def test_to_dataframe_transactions(): df = qif.to_dataframe(data_type=QifDataType.TRANSACTIONS) assert df.shape == (1, 20) - assert df['date'][0] == datetime(2019, 1, 1) - assert df['amount'][0] == 100 - assert df['payee'][0] == 'Test Payee' - assert df['memo'][0] == 'Test Memo' - assert df['category'][0]['name'] == 'Test Category' + assert df["date"][0] == datetime(2019, 1, 1) + assert df["amount"][0] == 100 + assert df["payee"][0] == "Test Payee" + assert df["memo"][0] == "Test Memo" + assert df["category"][0]["name"] == "Test Category" def test_to_dataframe_categories(): """Test the to_dataframe method with categories""" qif = Qif() - category = Category(name='Test Category') + category = Category(name="Test Category") qif.add_category(category) df = qif.to_dataframe(data_type=QifDataType.CATEGORIES) assert df.shape == (1, 9) - assert df['name'][0] == 'Test Category' - assert df['parent'][0] is None + assert df["name"][0] == "Test Category" + assert df["parent"][0] is None def test_to_dataframe_classes(): """Test the to_dataframe method with classes""" qif = Qif() - cls = Class(name='Test Class', desc='Test Description') + cls = Class(name="Test Class", desc="Test Description") qif.add_class(cls) df = qif.to_dataframe(data_type=QifDataType.CLASSES) assert df.shape == (1, 3) - assert df['name'][0] == 'Test Class' - assert df['desc'][0] == 'Test Description' + assert df["name"][0] == "Test Class" + assert df["desc"][0] == "Test Description" def test_to_dataframe_securities(): """Test the to_dataframe method with securities""" qif = Qif() - security = Security(symbol='TEST') + security = Security(symbol="TEST") qif.add_security(security) df = qif.to_dataframe(data_type=QifDataType.SECURITIES) assert df.shape == (1, 5) - assert df['symbol'][0] == 'TEST' - assert df['name'][0] is None - assert df['type'][0] is None - assert df['goal'][0] is None + assert df["symbol"][0] == "TEST" + assert df["name"][0] is None + assert df["type"][0] is None + assert df["goal"][0] is None def test_to_dataframe_accounts(): """Test the to_dataframe method with accounts""" qif = Qif() - account = Account(name='Test Account', account_type='Bank') + account = Account(name="Test Account", account_type="Bank") qif.add_account(account) df = qif.to_dataframe(data_type=QifDataType.ACCOUNTS) assert df.shape == (1, 7) - assert df['name'][0] == 'Test Account' - assert df['account_type'][0] == 'Bank' + assert df["name"][0] == "Test Account" + assert df["account_type"][0] == "Bank" def test_to_dataframe_investments(): """Test the to_dataframe method with investments""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") investment = Investment( date=datetime(2019, 1, 1), - amount=Decimal('100'), - security='Test Security', - price=Decimal('10'), + amount=Decimal("100"), + security="Test Security", + price=Decimal("10"), ) account.set_header(AccountType.INVST) account.add_transaction(investment) @@ -980,22 +977,22 @@ def test_to_dataframe_investments(): df = qif.to_dataframe(data_type=QifDataType.INVESTMENTS) assert df.shape == (1, 13) - assert df['date'][0] == datetime(2019, 1, 1) - assert df['amount'][0] == 100 - assert df['security'][0] == 'Test Security' - assert df['price'][0] == 10 + assert df["date"][0] == datetime(2019, 1, 1) + assert df["amount"][0] == 100 + assert df["security"][0] == "Test Security" + assert df["price"][0] == 10 def test_to_dataframe_transactions_with_ignore_list(): """Test the to_dataframe method with transactions and ignore""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") transaction = Transaction( date=datetime(2019, 2, 1), - amount=Decimal('100'), - payee='Test Payee', - memo='Test Memo', - category=Category(name='Test Category'), + amount=Decimal("100"), + payee="Test Payee", + memo="Test Memo", + category=Category(name="Test Category"), ) account.set_header(AccountType.BANK) account.add_transaction(transaction) @@ -1003,33 +1000,33 @@ def test_to_dataframe_transactions_with_ignore_list(): df = qif.to_dataframe( data_type=QifDataType.TRANSACTIONS, - ignore=['payee'], + ignore=["payee"], ) assert df.shape == (1, 19) - assert df['date'][0] == datetime(2019, 2, 1) - assert df['amount'][0] == 100 - assert 'payee' not in df.columns - assert df['memo'][0] == 'Test Memo' - assert df['category'][0]['name'] == 'Test Category' + assert df["date"][0] == datetime(2019, 2, 1) + assert df["amount"][0] == 100 + assert "payee" not in df.columns + assert df["memo"][0] == "Test Memo" + assert df["category"][0]["name"] == "Test Category" def test_to_dataframe_transactions_multiple(): """Test the to_dataframe method with multiple transactions""" qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") transaction = Transaction( date=datetime(2019, 1, 1), - amount=Decimal('100'), - payee='Test Payee', - memo='Test Memo', - category=Category(name='Test Category'), + amount=Decimal("100"), + payee="Test Payee", + memo="Test Memo", + category=Category(name="Test Category"), ) account.set_header(AccountType.BANK) account.add_transaction(transaction) transaction2 = transaction.copy() - transaction2.amount = Decimal('200') + transaction2.amount = Decimal("200") account.add_transaction(transaction2) qif.add_account(account) @@ -1037,16 +1034,16 @@ def test_to_dataframe_transactions_multiple(): df = qif.to_dataframe(data_type=QifDataType.TRANSACTIONS) assert df.shape == (2, 20) - assert df['date'][0] == datetime(2019, 1, 1) - assert df['amount'][0] == 100 - assert df['payee'][0] == 'Test Payee' - assert df['memo'][0] == 'Test Memo' - assert df['category'][0]['name'] == 'Test Category' - assert df['date'][1] == datetime(2019, 1, 1) - assert df['amount'][1] == 200 - assert df['payee'][1] == 'Test Payee' - assert df['memo'][1] == 'Test Memo' - assert df['category'][1]['name'] == 'Test Category' + assert df["date"][0] == datetime(2019, 1, 1) + assert df["amount"][0] == 100 + assert df["payee"][0] == "Test Payee" + assert df["memo"][0] == "Test Memo" + assert df["category"][0]["name"] == "Test Category" + assert df["date"][1] == datetime(2019, 1, 1) + assert df["amount"][1] == 200 + assert df["payee"][1] == "Test Payee" + assert df["memo"][1] == "Test Memo" + assert df["category"][1]["name"] == "Test Category" def test_transaction_before_account_definition_1(qif_file): @@ -1056,14 +1053,12 @@ def test_transaction_before_account_definition_1(qif_file): Relates to pull #32. https://github.com/isaacharrisholt/quiffen/pull/32 """ - test_file = ( - qif_file.parent / 'test_transaction_before_account_definition_1.qif' - ) + test_file = qif_file.parent / "test_transaction_before_account_definition_1.qif" qif = Qif.parse(test_file) - acc = qif.accounts['Quiffen Default Account'] + acc = qif.accounts["Quiffen Default Account"] assert len(acc.transactions) == 1 - assert len(acc.transactions['Bank']) == 2 + assert len(acc.transactions["Bank"]) == 2 def test_transaction_before_account_definition_2(qif_file): @@ -1072,36 +1067,37 @@ def test_transaction_before_account_definition_2(qif_file): Relates to pull #32. """ - test_file = ( - qif_file.parent / 'test_transaction_before_account_definition_2.qif' - ) + test_file = qif_file.parent / "test_transaction_before_account_definition_2.qif" qif = Qif.parse(test_file) - acc = qif.accounts['Quiffen Default Account'] + acc = qif.accounts["Quiffen Default Account"] assert len(acc.transactions) == 1 - assert len(acc.transactions['Bank']) == 2 + assert len(acc.transactions["Bank"]) == 2 def test_empty_qif(): qif = Qif() - assert qif.to_qif() == '' + assert qif.to_qif() == "" def test_transaction_account_type_qif(): qif = Qif() - account = Account(name='Test Account') + account = Account(name="Test Account") qif.add_account(account) transaction = Transaction( date=datetime(2019, 1, 1), - amount=Decimal('100'), + amount=Decimal("100"), ) account.set_header(AccountType.CASH) account.add_transaction(transaction) - assert qif.to_qif() == '''!Account + assert ( + qif.to_qif() + == """!Account NTest Account ^ !Type:Cash D2019-01-01 T100 ^ -''' +""" + ) diff --git a/tests/test_security.py b/tests/test_security.py index 5931cdf..af1bebf 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -14,164 +14,158 @@ def test_create_security(): assert security.type is None assert security.goal is None - security2 = Security(name='Test Security', symbol='Test Symbol') - assert security2.name == 'Test Security' - assert security2.symbol == 'Test Symbol' + security2 = Security(name="Test Security", symbol="Test Symbol") + assert security2.name == "Test Security" + assert security2.symbol == "Test Symbol" assert security2.type is None assert security2.goal is None security3 = Security( - name='Test Security', - symbol='Test Symbol', - type='Test Type', - goal='Test Goal', + name="Test Security", + symbol="Test Symbol", + type="Test Type", + goal="Test Goal", ) - assert security3.name == 'Test Security' - assert security3.symbol == 'Test Symbol' - assert security3.type == 'Test Type' - assert security3.goal == 'Test Goal' + assert security3.name == "Test Security" + assert security3.symbol == "Test Symbol" + assert security3.type == "Test Type" + assert security3.goal == "Test Goal" def test_eq_success(): """Test that two securities are equal""" - security = Security(name='Test Security', symbol='Test Symbol') - security2 = Security(name='Test Security', symbol='Test Symbol') + security = Security(name="Test Security", symbol="Test Symbol") + security2 = Security(name="Test Security", symbol="Test Symbol") assert security == security2 def test_eq_failure(): """Test that two securities are not equal""" - security = Security(name='Test Security', symbol='Test Symbol') - security2 = Security(name='Test Security', symbol='Test Symbol2') + security = Security(name="Test Security", symbol="Test Symbol") + security2 = Security(name="Test Security", symbol="Test Symbol2") assert security != security2 def test_str_method(): """Test the string representation of a security""" - security = Security(name='Test Security', symbol='Test Symbol') - assert str(security) == ( - 'Security:\n\tName: Test Security\n\tSymbol: Test Symbol' - ) + security = Security(name="Test Security", symbol="Test Symbol") + assert str(security) == ("Security:\n\tName: Test Security\n\tSymbol: Test Symbol") security2 = Security( - name='Test Security', - symbol='Test Symbol', - type='Test Type', - goal='Test Goal', + name="Test Security", + symbol="Test Symbol", + type="Test Type", + goal="Test Goal", ) assert str(security2) == ( - 'Security:\n\t' - 'Name: Test Security\n\t' - 'Symbol: Test Symbol\n\t' - 'Type: Test Type\n\t' - 'Goal: Test Goal' + "Security:\n\t" + "Name: Test Security\n\t" + "Symbol: Test Symbol\n\t" + "Type: Test Type\n\t" + "Goal: Test Goal" ) def test_merge(): """Test merging two securities""" - security = Security(name='Test Security', symbol='Test Symbol') + security = Security(name="Test Security", symbol="Test Symbol") security2 = Security( - name='Test Security 2', - symbol='Test Symbol 2', - type='Test Type', - goal='Test Goal', + name="Test Security 2", + symbol="Test Symbol 2", + type="Test Type", + goal="Test Goal", ) security.merge(security2) - assert security.name == 'Test Security' - assert security.symbol == 'Test Symbol' - assert security.type == 'Test Type' - assert security.goal == 'Test Goal' + assert security.name == "Test Security" + assert security.symbol == "Test Symbol" + assert security.type == "Test Type" + assert security.goal == "Test Goal" def test_to_qif(): """Test the to_qif method""" - security = Security(name='Test Security', symbol='Test Symbol') - assert security.to_qif() == ( - '!Type:Security\n' - 'NTest Security\n' - 'STest Symbol\n' - ) + security = Security(name="Test Security", symbol="Test Symbol") + assert security.to_qif() == ("!Type:Security\n" "NTest Security\n" "STest Symbol\n") security2 = Security( - name='Test Security', - symbol='Test Symbol', - type='Test Type', - goal='Test Goal', + name="Test Security", + symbol="Test Symbol", + type="Test Type", + goal="Test Goal", ) assert security2.to_qif() == ( - '!Type:Security\n' - 'NTest Security\n' - 'STest Symbol\n' - 'TTest Type\n' - 'GTest Goal\n' + "!Type:Security\n" + "NTest Security\n" + "STest Symbol\n" + "TTest Type\n" + "GTest Goal\n" ) def test_from_list_no_custom_fields(): """Test creating a security from a list with no custom fields""" qif_list = [ - 'NTest Security', - 'STest Symbol', - 'TTest Type', - 'GTest Goal', + "NTest Security", + "STest Symbol", + "TTest Type", + "GTest Goal", ] security = Security.from_list(qif_list) - assert security.name == 'Test Security' - assert security.symbol == 'Test Symbol' - assert security.type == 'Test Type' - assert security.goal == 'Test Goal' + assert security.name == "Test Security" + assert security.symbol == "Test Symbol" + assert security.type == "Test Type" + assert security.goal == "Test Goal" def test_from_list_with_custom_fields(): """Test creating a security from a list with custom fields""" - setattr(Security, '__CUSTOM_FIELDS', []) # Reset custom fields + setattr(Security, "__CUSTOM_FIELDS", []) # Reset custom fields qif_list = [ - 'NTest Security', - 'STest Symbol', - 'TTest Type', - 'GTest Goal', - 'XCustom field 1', - 'Y9238479', - 'DT2022-01-01T00:00:00.000001', + "NTest Security", + "STest Symbol", + "TTest Type", + "GTest Goal", + "XCustom field 1", + "Y9238479", + "DT2022-01-01T00:00:00.000001", ] # Add custom fields Security.add_custom_field( - line_code='X', - attr='custom_field_1', + line_code="X", + attr="custom_field_1", field_type=str, ) Security.add_custom_field( - line_code='Y', - attr='custom_field_2', + line_code="Y", + attr="custom_field_2", field_type=Decimal, ) Security.add_custom_field( - line_code='DT', # Test multi-character line code - attr='custom_field_3', + line_code="DT", # Test multi-character line code + attr="custom_field_3", field_type=datetime, ) security = Security.from_list(qif_list) - assert security.name == 'Test Security' - assert security.symbol == 'Test Symbol' - assert security.type == 'Test Type' - assert security.goal == 'Test Goal' - assert security.custom_field_1 == 'Custom field 1' - assert security.custom_field_2 == Decimal('9238479') + assert security.name == "Test Security" + assert security.symbol == "Test Symbol" + assert security.type == "Test Type" + assert security.goal == "Test Goal" + assert security.custom_field_1 == "Custom field 1" + assert security.custom_field_2 == Decimal("9238479") assert security.custom_field_3 == datetime(2022, 1, 1, 0, 0, 0, 1) - setattr(Security, '__CUSTOM_FIELDS', []) # Reset custom fields + setattr(Security, "__CUSTOM_FIELDS", []) # Reset custom fields def test_from_list_with_unknown_line_code(): """Test creating a security from a list with an unknown line code""" qif_list = [ - 'NTest Security', - 'STest Symbol', - 'TTest Type', - 'GTest Goal', - 'ZInvalid field', + "NTest Security", + "STest Symbol", + "TTest Type", + "GTest Goal", + "ZInvalid field", ] with pytest.raises(ValueError): @@ -180,61 +174,51 @@ def test_from_list_with_unknown_line_code(): def test_from_string_default_separator(): """Test creating a security from a string with the default separator""" - qif_string = ( - 'NTest Security\n' - 'STest Symbol\n' - 'TTest Type\n' - 'GTest Goal\n' - ) + qif_string = "NTest Security\n" "STest Symbol\n" "TTest Type\n" "GTest Goal\n" security = Security.from_string(qif_string) - assert security.name == 'Test Security' - assert security.symbol == 'Test Symbol' - assert security.type == 'Test Type' - assert security.goal == 'Test Goal' + assert security.name == "Test Security" + assert security.symbol == "Test Symbol" + assert security.type == "Test Type" + assert security.goal == "Test Goal" def test_from_string_custom_separator(): """Test creating a security from a string with a custom separator""" - qif_string = ( - 'NTest Security---' - 'STest Symbol---' - 'TTest Type---' - 'GTest Goal---' - ) - security = Security.from_string(qif_string, separator='---') - assert security.name == 'Test Security' - assert security.symbol == 'Test Symbol' - assert security.type == 'Test Type' - assert security.goal == 'Test Goal' + qif_string = "NTest Security---" "STest Symbol---" "TTest Type---" "GTest Goal---" + security = Security.from_string(qif_string, separator="---") + assert security.name == "Test Security" + assert security.symbol == "Test Symbol" + assert security.type == "Test Type" + assert security.goal == "Test Goal" def test_to_dict(): """Test the to_dict method""" security = Security( - name='Test Security', - symbol='Test Symbol', - type='Test Type', - goal='Test Goal', + name="Test Security", + symbol="Test Symbol", + type="Test Type", + goal="Test Goal", ) assert security.to_dict() == { - 'name': 'Test Security', - 'symbol': 'Test Symbol', - 'type': 'Test Type', - 'goal': 'Test Goal', - 'line_number': None, + "name": "Test Security", + "symbol": "Test Symbol", + "type": "Test Type", + "goal": "Test Goal", + "line_number": None, } def test_to_dict_with_ignore(): """Test the to_dict method with ignored attributes""" security = Security( - name='Test Security', - symbol='Test Symbol', - type='Test Type', - goal='Test Goal', + name="Test Security", + symbol="Test Symbol", + type="Test Type", + goal="Test Goal", ) - assert security.to_dict(ignore=['type', 'line_number']) == { - 'name': 'Test Security', - 'symbol': 'Test Symbol', - 'goal': 'Test Goal', + assert security.to_dict(ignore=["type", "line_number"]) == { + "name": "Test Security", + "symbol": "Test Symbol", + "goal": "Test Goal", } diff --git a/tests/test_split.py b/tests/test_split.py index 491c564..ae1466e 100644 --- a/tests/test_split.py +++ b/tests/test_split.py @@ -1,7 +1,7 @@ from datetime import datetime -from quiffen.core.split import Split from quiffen.core.category import Category +from quiffen.core.split import Split def test_create_split(): @@ -11,150 +11,146 @@ def test_create_split(): assert split.memo is None assert split.category is None - split2 = Split(amount=100, memo='Test Memo') + split2 = Split(amount=100, memo="Test Memo") assert split2.amount == 100 - assert split2.memo == 'Test Memo' + assert split2.memo == "Test Memo" assert split2.category is None split3 = Split( amount=100, - memo='Test Memo', - category=Category(name='Test Category'), + memo="Test Memo", + category=Category(name="Test Category"), ) assert split3.amount == 100 - assert split3.memo == 'Test Memo' - assert split3.category.name == 'Test Category' + assert split3.memo == "Test Memo" + assert split3.category.name == "Test Category" def test_eq_success(): """Test that two splits are equal""" - split = Split(amount=100, memo='Test Memo') - split2 = Split(amount=100, memo='Test Memo') + split = Split(amount=100, memo="Test Memo") + split2 = Split(amount=100, memo="Test Memo") assert split == split2 def test_eq_failure(): """Test that two splits are not equal""" - split = Split(amount=100, memo='Test Memo') - split2 = Split(amount=100, memo='Test Memo2') + split = Split(amount=100, memo="Test Memo") + split2 = Split(amount=100, memo="Test Memo2") assert split != split2 def test_str_method(): """Test the string representation of a split""" - split = Split(amount=100, memo='Test Memo') - assert str(split) == '\n\tSplit:\n\t\tAmount: 100\n\t\tMemo: Test Memo' + split = Split(amount=100, memo="Test Memo") + assert str(split) == "\n\tSplit:\n\t\tAmount: 100\n\t\tMemo: Test Memo" split2 = Split( amount=100, - memo='Test Memo', - category=Category(name='Test Category'), + memo="Test Memo", + category=Category(name="Test Category"), ) assert str(split2) == ( - '\n\tSplit:\n\t\t' - 'Amount: 100\n\t\t' - 'Memo: Test Memo\n\t\t' - 'Category: Test Category' + "\n\tSplit:\n\t\t" + "Amount: 100\n\t\t" + "Memo: Test Memo\n\t\t" + "Category: Test Category" ) def test_to_qif(): """Test the to_qif method""" - split = Split(amount=100, memo='Test Memo') - assert split.to_qif() == ( - 'S\n' - '$100\n' - 'ETest Memo\n' - ) + split = Split(amount=100, memo="Test Memo") + assert split.to_qif() == ("S\n" "$100\n" "ETest Memo\n") - test_category = Category(name='Test Category') + test_category = Category(name="Test Category") split2 = Split( amount=100, - memo='Test Memo', + memo="Test Memo", category=test_category, date=datetime(2019, 1, 1), - cleared='True', + cleared="True", check_number=123, percent=50, - to_account='Test Account', - payee_address='Test Address', + to_account="Test Account", + payee_address="Test Address", ) assert split2.to_qif() == ( - 'STest Category\n' - 'D2019-01-01\n' - '$100\n' - 'ETest Memo\n' - 'CTrue\n' - 'L[Test Account]\n' - 'N123\n' - '%50%\n' - 'ATest Address\n' + "STest Category\n" + "D2019-01-01\n" + "$100\n" + "ETest Memo\n" + "CTrue\n" + "L[Test Account]\n" + "N123\n" + "%50%\n" + "ATest Address\n" ) def test_to_dict(): """Test the to_dict method""" - split = Split(amount=100, memo='Test Memo') + split = Split(amount=100, memo="Test Memo") assert split.to_dict() == { - 'amount': 100, - 'memo': 'Test Memo', - 'category': None, - 'check_number': None, - 'cleared': None, - 'date': None, - 'percent': None, - 'to_account': None, - 'payee_address': None, + "amount": 100, + "memo": "Test Memo", + "category": None, + "check_number": None, + "cleared": None, + "date": None, + "percent": None, + "to_account": None, + "payee_address": None, } - test_category = Category(name='Test Category') + test_category = Category(name="Test Category") split2 = Split( amount=100, - memo='Test Memo', + memo="Test Memo", category=test_category, ) assert split2.to_dict() == { - 'amount': 100, - 'memo': 'Test Memo', - 'category': test_category.to_dict(), - 'check_number': None, - 'cleared': None, - 'date': None, - 'percent': None, - 'to_account': None, - 'payee_address': None, + "amount": 100, + "memo": "Test Memo", + "category": test_category.to_dict(), + "check_number": None, + "cleared": None, + "date": None, + "percent": None, + "to_account": None, + "payee_address": None, } def test_to_dict_with_ignore(): """Test the to_dict method with ignore""" - split = Split(amount=100, memo='Test Memo') - assert split.to_dict(ignore={'memo'}) == { - 'amount': 100, - 'category': None, - 'check_number': None, - 'cleared': None, - 'date': None, - 'percent': None, - 'to_account': None, - 'payee_address': None, + split = Split(amount=100, memo="Test Memo") + assert split.to_dict(ignore={"memo"}) == { + "amount": 100, + "category": None, + "check_number": None, + "cleared": None, + "date": None, + "percent": None, + "to_account": None, + "payee_address": None, } - test_category = Category(name='Test Category') + test_category = Category(name="Test Category") split2 = Split( amount=100, - memo='Test Memo', + memo="Test Memo", category=test_category, ) - assert split2.to_dict(ignore={'memo', 'category'}) == { - 'amount': 100, - 'check_number': None, - 'cleared': None, - 'date': None, - 'percent': None, - 'to_account': None, - 'payee_address': None, + assert split2.to_dict(ignore={"memo", "category"}) == { + "amount": 100, + "check_number": None, + "cleared": None, + "date": None, + "percent": None, + "to_account": None, + "payee_address": None, } diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 4fae419..666cb00 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -28,16 +28,16 @@ def test_create_transaction_more_fields(): date=datetime(2022, 2, 1), amount=100, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", ) assert transaction.date == datetime(2022, 2, 1) assert transaction.amount == 100 assert transaction.check_number == 1 - assert transaction.payee == 'Test Payee' - assert transaction.memo == 'Test Memo' - assert transaction.payee_address == 'Test Address' + assert transaction.payee == "Test Payee" + assert transaction.memo == "Test Memo" + assert transaction.payee_address == "Test Address" assert transaction.category is None assert not transaction.splits @@ -47,19 +47,19 @@ def test_create_transaction_with_splits(): date=datetime(2022, 2, 1), amount=100, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), splits=[Split(amount=100)], ) assert transaction.date == datetime(2022, 2, 1) assert transaction.amount == 100 assert transaction.check_number == 1 - assert transaction.payee == 'Test Payee' - assert transaction.memo == 'Test Memo' - assert transaction.payee_address == 'Test Address' - assert transaction.category == Category(name='Test Category') + assert transaction.payee == "Test Payee" + assert transaction.memo == "Test Memo" + assert transaction.payee_address == "Test Address" + assert transaction.category == Category(name="Test Category") assert transaction.splits == [Split(amount=100)] @@ -83,19 +83,19 @@ def test_create_transaction_with_splits_exactly_100_percent(): https://github.com/isaacharrisholt/quiffen/issues/39 """ transaction_list = [ - 'D12/1/22', - 'PPayee', - 'T5,382.39', - 'NDEP', - 'LSplit', - 'SCategory A', - '$-120.83', - 'SCategory B', - '$-2,100.96', - 'SCategory C', - '$-729.15', - 'SCategory D', - '$8,333.33', + "D12/1/22", + "PPayee", + "T5,382.39", + "NDEP", + "LSplit", + "SCategory A", + "$-120.83", + "SCategory B", + "$-2,100.96", + "SCategory C", + "$-729.15", + "SCategory D", + "$8,333.33", ] # Should not raise an error Transaction.from_list(transaction_list) @@ -127,20 +127,20 @@ def test_eq_success(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), splits=[Split(amount=100), Split(amount=200)], ) transaction4 = Transaction( date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), splits=[Split(amount=100), Split(amount=200)], ) assert transaction3 == transaction4 @@ -162,20 +162,20 @@ def test_eq_failure(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), splits=[Split(amount=100), Split(amount=200)], ) transaction4 = Transaction( date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), splits=[Split(amount=100), Split(amount=200), Split(amount=300)], ) assert transaction3 != transaction4 @@ -184,10 +184,10 @@ def test_eq_failure(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), splits=[Split(amount=100), Split(amount=200)], ) transaction5.splits[0].amount = 300 @@ -197,10 +197,10 @@ def test_eq_failure(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo 2', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo 2", + payee_address="Test Address", + category=Category(name="Test Category"), splits=[Split(amount=100), Split(amount=200)], ) assert transaction3 != transaction6 @@ -212,22 +212,22 @@ def test_str_method(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), splits=[Split(amount=100), Split(amount=200)], ) assert str(transaction) == ( - 'Transaction:\n\t' - 'Date: 2022-02-01 00:00:00\n\t' - 'Amount: 1000\n\t' - 'Memo: Test Memo\n\t' - 'Payee: Test Payee\n\t' - 'Payee Address: Test Address\n\t' - 'Category: Test Category\n\t' - 'Check Number: 1\n\t' - 'Splits: 2' + "Transaction:\n\t" + "Date: 2022-02-01 00:00:00\n\t" + "Amount: 1000\n\t" + "Memo: Test Memo\n\t" + "Payee: Test Payee\n\t" + "Payee Address: Test Address\n\t" + "Category: Test Category\n\t" + "Check Number: 1\n\t" + "Splits: 2" ) @@ -237,10 +237,10 @@ def test_is_split(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), splits=[Split(amount=100), Split(amount=200)], ) assert transaction.is_split @@ -249,10 +249,10 @@ def test_is_split(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), ) assert not transaction.is_split @@ -263,10 +263,10 @@ def test_add_split(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), ) transaction.add_split(Split(amount=100)) assert transaction.splits == [Split(amount=100)] @@ -280,11 +280,11 @@ def test_add_split_percent_too_high(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), - splits=[Split(amount=100, percent=60)] + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), + splits=[Split(amount=100, percent=60)], ) with pytest.raises(ValueError): transaction.add_split(Split(amount=100, percent=60)) @@ -296,11 +296,11 @@ def test_add_split_amount_too_high(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), - splits=[Split(amount=100)] + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), + splits=[Split(amount=100)], ) with pytest.raises(ValueError): transaction.add_split(Split(amount=950)) @@ -314,14 +314,14 @@ def test_add_splits_exactly_100_percent(): """ transaction = Transaction( date=datetime(2022, 2, 1), - amount=Decimal('5382.39'), + amount=Decimal("5382.39"), ) splits = [ - Split(amount=Decimal('-121.83')), - Split(amount=Decimal('-2101.96')), - Split(amount=Decimal('-730.15')), - Split(amount=Decimal('8332.33')), + Split(amount=Decimal("-121.83")), + Split(amount=Decimal("-2101.96")), + Split(amount=Decimal("-730.15")), + Split(amount=Decimal("8332.33")), ] for split in splits: @@ -335,13 +335,13 @@ def test_remove_splits_one_filter(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), - splits=[Split(amount=100), Split(amount=200, memo='Test Memo')] + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), + splits=[Split(amount=100), Split(amount=200, memo="Test Memo")], ) - transaction.remove_splits(memo='Test Memo') + transaction.remove_splits(memo="Test Memo") assert transaction.splits == [Split(amount=100)] @@ -351,13 +351,13 @@ def test_remove_splits_multiple_filters(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), - splits=[Split(amount=100), Split(amount=100, memo='Test Memo')] + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), + splits=[Split(amount=100), Split(amount=100, memo="Test Memo")], ) - transaction.remove_splits(memo='Test Memo', amount=100) + transaction.remove_splits(memo="Test Memo", amount=100) assert transaction.splits == [Split(amount=100)] @@ -367,16 +367,16 @@ def test_remove_splits_no_match(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), - splits=[Split(amount=100), Split(amount=100, memo='Test Memo')] + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), + splits=[Split(amount=100), Split(amount=100, memo="Test Memo")], ) - transaction.remove_splits(memo='Test Memo', amount=100, check_number=1) + transaction.remove_splits(memo="Test Memo", amount=100, check_number=1) assert transaction.splits == [ Split(amount=100), - Split(amount=100, memo='Test Memo'), + Split(amount=100, memo="Test Memo"), ] @@ -386,10 +386,10 @@ def test_remove_splits_no_filters(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), splits=[Split(amount=100), Split(amount=200)], ) transaction.remove_splits() @@ -403,49 +403,49 @@ def test_to_qif_no_splits_no_classes(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), ) print(repr(transaction.to_qif())) assert transaction.to_qif() == ( - 'D2022-02-01\n' - 'T1000\n' - 'MTest Memo\n' - 'PTest Payee\n' - 'ATest Address\n' - 'LTest Category\n' - 'N1\n' + "D2022-02-01\n" + "T1000\n" + "MTest Memo\n" + "PTest Payee\n" + "ATest Address\n" + "LTest Category\n" + "N1\n" ) def test_to_qif_no_split_with_class(): """Test the to_qif method with no splits and a class""" - parent = Category(name='Test Parent') - child = Category(name='Test Child') + parent = Category(name="Test Parent") + child = Category(name="Test Child") parent.add_child(child) - cls = Class(name='Test Class') + cls = Class(name="Test Class") cls.add_category(parent) transaction = Transaction( date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", category=child, ) - assert transaction.to_qif(classes={'Test Class': cls}) == ( - 'D2022-02-01\n' - 'T1000\n' - 'MTest Memo\n' - 'PTest Payee\n' - 'ATest Address\n' - 'LTest Parent:Test Child/Test Class\n' - 'N1\n' + assert transaction.to_qif(classes={"Test Class": cls}) == ( + "D2022-02-01\n" + "T1000\n" + "MTest Memo\n" + "PTest Payee\n" + "ATest Address\n" + "LTest Parent:Test Child/Test Class\n" + "N1\n" ) @@ -455,119 +455,119 @@ def test_to_qif_with_splits_no_classes(): date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', - category=Category(name='Test Category'), + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", + category=Category(name="Test Category"), splits=[ Split( amount=100, - memo='Test Memo', - category=Category(name='Test Split Category'), + memo="Test Memo", + category=Category(name="Test Split Category"), ), - Split(amount=200, memo='Test Memo 2'), + Split(amount=200, memo="Test Memo 2"), ], ) assert transaction.to_qif() == ( - 'D2022-02-01\n' - 'T1000\n' - 'MTest Memo\n' - 'PTest Payee\n' - 'ATest Address\n' - 'LTest Category\n' - 'N1\n' - 'STest Split Category\n' - '$100\n' - 'ETest Memo\n' - 'S\n' - '$200\n' - 'ETest Memo 2\n' + "D2022-02-01\n" + "T1000\n" + "MTest Memo\n" + "PTest Payee\n" + "ATest Address\n" + "LTest Category\n" + "N1\n" + "STest Split Category\n" + "$100\n" + "ETest Memo\n" + "S\n" + "$200\n" + "ETest Memo 2\n" ) def test_to_qif_with_splits_with_classes(): """Test the to_qif method with splits and classes""" - parent = Category(name='Test Parent') - child = Category(name='Test Child') + parent = Category(name="Test Parent") + child = Category(name="Test Child") parent.add_child(child) - cls = Class(name='Test Class') + cls = Class(name="Test Class") cls.add_category(parent) - split_category = Category(name='Test Split Category') + split_category = Category(name="Test Split Category") cls.add_category(split_category) transaction = Transaction( date=datetime(2022, 2, 1), amount=1000, check_number=1, - payee='Test Payee', - memo='Test Memo', - payee_address='Test Address', + payee="Test Payee", + memo="Test Memo", + payee_address="Test Address", category=child, splits=[ Split( amount=100, - memo='Test Memo', - category=Category(name='Test Split Category'), + memo="Test Memo", + category=Category(name="Test Split Category"), ), - Split(amount=200, memo='Test Memo 2'), + Split(amount=200, memo="Test Memo 2"), ], ) - assert transaction.to_qif(classes={'Test Class': cls}) == ( - 'D2022-02-01\n' - 'T1000\n' - 'MTest Memo\n' - 'PTest Payee\n' - 'ATest Address\n' - 'LTest Parent:Test Child/Test Class\n' - 'N1\n' - 'STest Split Category/Test Class\n' - '$100\n' - 'ETest Memo\n' - 'S\n' - '$200\n' - 'ETest Memo 2\n' + assert transaction.to_qif(classes={"Test Class": cls}) == ( + "D2022-02-01\n" + "T1000\n" + "MTest Memo\n" + "PTest Payee\n" + "ATest Address\n" + "LTest Parent:Test Child/Test Class\n" + "N1\n" + "STest Split Category/Test Class\n" + "$100\n" + "ETest Memo\n" + "S\n" + "$200\n" + "ETest Memo 2\n" ) def test_from_list_no_splits_no_classes(): """Test creating a transaction from a list of QIF strings""" qif_list = [ - 'D2022-02-01', - 'T1000', - 'MTest Memo', - 'CTest Cleared', - 'PTest Payee', - 'ATest Address', - 'L[Test To Account]', # Brackets denote to account - 'LTest Category', # No brackets denote category - 'N1', - 'FFalse', - '12022-03-01', # First payment date - '234', # Loan length - '312', # Number of payments - '42', # Periods per year - '51.23', # Interest rate - '61000', # Current loan balance - '710000', # Original loan amount + "D2022-02-01", + "T1000", + "MTest Memo", + "CTest Cleared", + "PTest Payee", + "ATest Address", + "L[Test To Account]", # Brackets denote to account + "LTest Category", # No brackets denote category + "N1", + "FFalse", + "12022-03-01", # First payment date + "234", # Loan length + "312", # Number of payments + "42", # Periods per year + "51.23", # Interest rate + "61000", # Current loan balance + "710000", # Original loan amount ] transaction, _ = Transaction.from_list(qif_list) assert transaction.date == datetime(2022, 2, 1) assert transaction.amount == 1000 - assert transaction.memo == 'Test Memo' - assert transaction.cleared == 'Test Cleared' - assert transaction.payee == 'Test Payee' - assert transaction.payee_address == 'Test Address' - assert transaction.to_account == 'Test To Account' - assert transaction.category == Category(name='Test Category') + assert transaction.memo == "Test Memo" + assert transaction.cleared == "Test Cleared" + assert transaction.payee == "Test Payee" + assert transaction.payee_address == "Test Address" + assert transaction.to_account == "Test To Account" + assert transaction.category == Category(name="Test Category") assert transaction.check_number == 1 assert not transaction.is_split assert transaction.first_payment_date == datetime(2022, 3, 1) assert transaction.loan_length == 34 assert transaction.num_payments == 12 assert transaction.periods_per_annum == 2 - assert transaction.interest_rate == Decimal('1.23') + assert transaction.interest_rate == Decimal("1.23") assert transaction.current_loan_balance == 1000 assert transaction.original_loan_amount == 10000 @@ -576,21 +576,21 @@ def test_from_list_no_split_with_class(): """Test creating a transaction from a list of QIF strings with no splits but that does define a QIF class""" qif_list = [ - 'D2022-02-01', - 'T1000', - 'L[Test To Account]', # Brackets denote to account - 'LTest Category/Class Name', # / denotes a class after the category + "D2022-02-01", + "T1000", + "L[Test To Account]", # Brackets denote to account + "LTest Category/Class Name", # / denotes a class after the category ] transaction, classes = Transaction.from_list(qif_list) assert transaction.date == datetime(2022, 2, 1) assert transaction.amount == 1000 - assert transaction.to_account == 'Test To Account' - assert transaction.category == Category(name='Test Category') + assert transaction.to_account == "Test To Account" + assert transaction.category == Category(name="Test Category") assert classes == { - 'Class Name': Class( - name='Class Name', - categories=[Category(name='Test Category')], + "Class Name": Class( + name="Class Name", + categories=[Category(name="Test Category")], ), } @@ -598,36 +598,36 @@ def test_from_list_no_split_with_class(): def test_from_list_with_splits_no_classes(): """Test creating a transaction from a list of QIF strings with splits""" qif_list = [ - 'D2022-02-01', - 'T1000', - 'L[Test To Account]', # Brackets denote to account - 'LTest Category', # No brackets denote category - 'STest Split Category 1', - 'ETest Split Memo', - '$100', - 'STest Split Category 2', - 'EMemo', - '$100', - '%10', + "D2022-02-01", + "T1000", + "L[Test To Account]", # Brackets denote to account + "LTest Category", # No brackets denote category + "STest Split Category 1", + "ETest Split Memo", + "$100", + "STest Split Category 2", + "EMemo", + "$100", + "%10", ] transaction, _ = Transaction.from_list(qif_list) assert transaction.date == datetime(2022, 2, 1) assert transaction.amount == 1000 - assert transaction.to_account == 'Test To Account' - assert transaction.category == Category(name='Test Category') + assert transaction.to_account == "Test To Account" + assert transaction.category == Category(name="Test Category") assert transaction.is_split assert transaction.splits == [ Split( - category=Category(name='Test Split Category 1'), - memo='Test Split Memo', + category=Category(name="Test Split Category 1"), + memo="Test Split Memo", amount=100, - percent=Decimal('10'), + percent=Decimal("10"), ), Split( - category=Category(name='Test Split Category 2'), - memo='Memo', + category=Category(name="Test Split Category 2"), + memo="Memo", amount=100, - percent=Decimal('10'), + percent=Decimal("10"), ), ] @@ -636,45 +636,45 @@ def test_from_list_with_splits_with_classes(): """Test creating a transaction from a list of QIF strings with splits and classes""" qif_list = [ - 'D2022-02-01', - 'T1000', - 'L[Test To Account]', # Brackets denote to account - 'LTest Category', # No brackets denote category - 'STest Split Category 1/Class Name', - 'ETest Split Memo', - 'T100', - 'STest Split Category 2/Class Name', - 'EMemo', - '$100', - '%10', + "D2022-02-01", + "T1000", + "L[Test To Account]", # Brackets denote to account + "LTest Category", # No brackets denote category + "STest Split Category 1/Class Name", + "ETest Split Memo", + "T100", + "STest Split Category 2/Class Name", + "EMemo", + "$100", + "%10", ] transaction, classes = Transaction.from_list(qif_list) assert transaction.date == datetime(2022, 2, 1) assert transaction.amount == 1000 - assert transaction.to_account == 'Test To Account' - assert transaction.category == Category(name='Test Category') + assert transaction.to_account == "Test To Account" + assert transaction.category == Category(name="Test Category") assert transaction.is_split assert transaction.splits == [ Split( - category=Category(name='Test Split Category 1'), - memo='Test Split Memo', + category=Category(name="Test Split Category 1"), + memo="Test Split Memo", amount=100, - percent=Decimal('10'), + percent=Decimal("10"), ), Split( - category=Category(name='Test Split Category 2'), - memo='Memo', + category=Category(name="Test Split Category 2"), + memo="Memo", amount=100, - percent=Decimal('10'), + percent=Decimal("10"), ), ] assert classes == { - 'Class Name': Class( - name='Class Name', + "Class Name": Class( + name="Class Name", categories=[ - Category(name='Test Split Category 1'), - Category(name='Test Split Category 2'), + Category(name="Test Split Category 1"), + Category(name="Test Split Category 2"), ], ), } @@ -684,48 +684,48 @@ def test_from_list_multiple_categories(): """Test creating a transaction from a list of QIF strings with multiple categories""" qif_list = [ - 'D2022-02-01', - 'T1000', - 'L[Test To Account]', # Brackets denote to account - 'LTest Category 1', - 'LTest Category 2', + "D2022-02-01", + "T1000", + "L[Test To Account]", # Brackets denote to account + "LTest Category 1", + "LTest Category 2", ] transaction, _ = Transaction.from_list(qif_list) assert transaction.date == datetime(2022, 2, 1) assert transaction.amount == 1000 - assert transaction.to_account == 'Test To Account' - assert transaction.category.name == 'Test Category 2' - assert transaction.category.parent.name == 'Test Category 1' + assert transaction.to_account == "Test To Account" + assert transaction.category.name == "Test Category 2" + assert transaction.category.parent.name == "Test Category 1" def test_from_string_default_separator(): """Test creating a transaction from a string with the default separator""" qif_string = ( - 'D2022-02-01\n' - 'T1000\n' - 'L[Test To Account]\n' # Brackets denote to account - 'LTest Category' + "D2022-02-01\n" + "T1000\n" + "L[Test To Account]\n" # Brackets denote to account + "LTest Category" ) transaction, _ = Transaction.from_string(qif_string) assert transaction.date == datetime(2022, 2, 1) assert transaction.amount == 1000 - assert transaction.to_account == 'Test To Account' - assert transaction.category == Category(name='Test Category') + assert transaction.to_account == "Test To Account" + assert transaction.category == Category(name="Test Category") def test_from_string_custom_separator(): """Test creating a transaction from a string with a custom separator""" qif_string = ( - 'D2022-02-01---' - 'T1000---' - 'L[Test To Account]---' # Brackets denote to account - 'LTest Category' + "D2022-02-01---" + "T1000---" + "L[Test To Account]---" # Brackets denote to account + "LTest Category" ) - transaction, _ = Transaction.from_string(qif_string, separator='---') + transaction, _ = Transaction.from_string(qif_string, separator="---") assert transaction.date == datetime(2022, 2, 1) assert transaction.amount == 1000 - assert transaction.to_account == 'Test To Account' - assert transaction.category == Category(name='Test Category') + assert transaction.to_account == "Test To Account" + assert transaction.category == Category(name="Test Category") def test_to_dict(): @@ -733,40 +733,40 @@ def test_to_dict(): transaction = Transaction( date=datetime(2022, 2, 1), amount=1000, - to_account='Test To Account', - category=Category(name='Test Category'), + to_account="Test To Account", + category=Category(name="Test Category"), ) assert transaction.to_dict() == { - 'date': datetime(2022, 2, 1, 0, 0), - 'amount': Decimal('1000'), - 'memo': None, - 'cleared': None, - 'payee': None, - 'payee_address': None, - 'category': { - 'name': 'Test Category', - 'desc': None, - 'tax_related': None, - 'category_type': 'expense', - 'budget_amount': None, - 'tax_schedule_info': None, - 'hierarchy': 'Test Category', - 'children': [], - 'parent': None + "date": datetime(2022, 2, 1, 0, 0), + "amount": Decimal("1000"), + "memo": None, + "cleared": None, + "payee": None, + "payee_address": None, + "category": { + "name": "Test Category", + "desc": None, + "tax_related": None, + "category_type": "expense", + "budget_amount": None, + "tax_schedule_info": None, + "hierarchy": "Test Category", + "children": [], + "parent": None, }, - 'check_number': None, - 'reimbursable_expense': None, - 'small_business_expense': None, - 'to_account': 'Test To Account', - 'first_payment_date': None, - 'loan_length': None, - 'num_payments': None, - 'periods_per_annum': None, - 'interest_rate': None, - 'current_loan_balance': None, - 'original_loan_amount': None, - 'line_number': None, - 'splits': [], + "check_number": None, + "reimbursable_expense": None, + "small_business_expense": None, + "to_account": "Test To Account", + "first_payment_date": None, + "loan_length": None, + "num_payments": None, + "periods_per_annum": None, + "interest_rate": None, + "current_loan_balance": None, + "original_loan_amount": None, + "line_number": None, + "splits": [], } @@ -775,39 +775,39 @@ def test_to_dict_with_ignore(): transaction = Transaction( date=datetime(2022, 2, 1), amount=1000, - to_account='Test To Account', - category=Category(name='Test Category'), - ) - assert transaction.to_dict(ignore=['to_account']) == { - 'date': datetime(2022, 2, 1, 0, 0), - 'amount': Decimal('1000'), - 'memo': None, - 'cleared': None, - 'payee': None, - 'payee_address': None, - 'category': { - 'name': 'Test Category', - 'desc': None, - 'tax_related': None, - 'category_type': 'expense', - 'budget_amount': None, - 'tax_schedule_info': None, - 'hierarchy': 'Test Category', - 'children': [], - 'parent': None + to_account="Test To Account", + category=Category(name="Test Category"), + ) + assert transaction.to_dict(ignore=["to_account"]) == { + "date": datetime(2022, 2, 1, 0, 0), + "amount": Decimal("1000"), + "memo": None, + "cleared": None, + "payee": None, + "payee_address": None, + "category": { + "name": "Test Category", + "desc": None, + "tax_related": None, + "category_type": "expense", + "budget_amount": None, + "tax_schedule_info": None, + "hierarchy": "Test Category", + "children": [], + "parent": None, }, - 'check_number': None, - 'reimbursable_expense': None, - 'small_business_expense': None, - 'first_payment_date': None, - 'loan_length': None, - 'num_payments': None, - 'periods_per_annum': None, - 'interest_rate': None, - 'current_loan_balance': None, - 'original_loan_amount': None, - 'line_number': None, - 'splits': [], + "check_number": None, + "reimbursable_expense": None, + "small_business_expense": None, + "first_payment_date": None, + "loan_length": None, + "num_payments": None, + "periods_per_annum": None, + "interest_rate": None, + "current_loan_balance": None, + "original_loan_amount": None, + "line_number": None, + "splits": [], } @@ -819,33 +819,33 @@ def test_from_list_zero_value_with_splits(): https://github.com/isaacharrisholt/quiffen/issues/31 """ qif_list = [ - 'D2022-02-01', - 'T0', - 'L[Test To Account]', # Brackets denote to account - 'LTest Category', # No brackets denote category - 'STest Split Category 1', - 'ETest Split Memo', - 'T0', - 'STest Split Category 2', - 'EMemo', - '$0', + "D2022-02-01", + "T0", + "L[Test To Account]", # Brackets denote to account + "LTest Category", # No brackets denote category + "STest Split Category 1", + "ETest Split Memo", + "T0", + "STest Split Category 2", + "EMemo", + "$0", ] transaction, _ = Transaction.from_list(qif_list) assert transaction.date == datetime(2022, 2, 1) assert transaction.amount == 0 - assert transaction.to_account == 'Test To Account' - assert transaction.category == Category(name='Test Category') + assert transaction.to_account == "Test To Account" + assert transaction.category == Category(name="Test Category") assert transaction.is_split assert transaction.splits == [ Split( - category=Category(name='Test Split Category 1'), - memo='Test Split Memo', + category=Category(name="Test Split Category 1"), + memo="Test Split Memo", amount=0, percent=None, ), Split( - category=Category(name='Test Split Category 2'), - memo='Memo', + category=Category(name="Test Split Category 2"), + memo="Memo", amount=0, percent=None, ), @@ -861,8 +861,8 @@ def test_check_number_allows_strings(): transaction = Transaction( date=datetime(2022, 2, 1), amount=1000, - to_account='Test To Account', - category=Category(name='Test Category'), - check_number='Transfer', + to_account="Test To Account", + category=Category(name="Test Category"), + check_number="Transfer", ) - assert transaction.check_number == 'Transfer' + assert transaction.check_number == "Transfer" diff --git a/tests/test_utils.py b/tests/test_utils.py index aeb8677..911d0ce 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -6,33 +6,31 @@ @pytest.mark.parametrize( - 'date_pattern,day_first', + "date_pattern,day_first", [ # Day first patterns - ('%d/%m/%Y', True), - ('%d-%m-%Y', True), - ('%d/%m/%y', True), - ('%d-%m-%y', True), - ('%d0%B0%Y', True), - ('%d0%B0%y', True), - ('%d0%b0%Y', True), - ('%d0%b0%y', True), - + ("%d/%m/%Y", True), + ("%d-%m-%Y", True), + ("%d/%m/%y", True), + ("%d-%m-%y", True), + ("%d0%B0%Y", True), + ("%d0%B0%y", True), + ("%d0%b0%Y", True), + ("%d0%b0%y", True), # Month first patterns - ('%m/%d/%Y', False), - ('%m-%d-%Y', False), - ('%m/%d/%y', False), - ('%m-%d-%y', False), - ('%B0%d0%Y', False), - ('%B0%d0%y', False), - ('%b0%d0%Y', False), - ('%b0%d0%y', False), - + ("%m/%d/%Y", False), + ("%m-%d-%Y", False), + ("%m/%d/%y", False), + ("%m-%d-%y", False), + ("%B0%d0%Y", False), + ("%B0%d0%y", False), + ("%b0%d0%Y", False), + ("%b0%d0%y", False), # Year first patterns - ('%Y/%m/%d', False), - ('%Y-%m-%d', False), - ('%Y0%B0%d', False), - ('%Y0%b0%d', False), + ("%Y/%m/%d", False), + ("%Y-%m-%d", False), + ("%Y0%B0%d", False), + ("%Y0%b0%d", False), ], ) def test_parse_date(date_pattern, day_first): @@ -43,16 +41,16 @@ def test_parse_date(date_pattern, day_first): @pytest.mark.parametrize( - 'field,expected_line_code,expected_field_info', + "field,expected_line_code,expected_field_info", [ - ('DDate\n\n', 'D', 'Date'), - ('PPayee', 'P', 'Payee'), - ('MMemo', 'M', 'Memo'), - ('TAmount', 'T', 'Amount'), - ('LCategory', 'L', 'Category'), - ('SSplit', 'S', 'Split'), - ('E', 'E', ''), - ('', '', ''), + ("DDate\n\n", "D", "Date"), + ("PPayee", "P", "Payee"), + ("MMemo", "M", "Memo"), + ("TAmount", "T", "Amount"), + ("LCategory", "L", "Category"), + ("SSplit", "S", "Split"), + ("E", "E", ""), + ("", "", ""), ], ) def test_parse_line_code_and_field_info( @@ -66,54 +64,60 @@ def test_parse_line_code_and_field_info( def test_apply_csv_formatting_scalar(): - assert utils.apply_csv_formatting_to_scalar('foo') == 'foo' + assert utils.apply_csv_formatting_to_scalar("foo") == "foo" assert utils.apply_csv_formatting_to_scalar(123) == 123 assert utils.apply_csv_formatting_to_scalar(123.45) == 123.45 assert utils.apply_csv_formatting_to_scalar(123.0) == 123 - assert utils.apply_csv_formatting_to_scalar( - datetime(2022, 1, 2), - ) == '2022-01-02' - assert utils.apply_csv_formatting_to_scalar( - datetime(2022, 1, 2).date(), - ) == '2022-01-02' + assert ( + utils.apply_csv_formatting_to_scalar( + datetime(2022, 1, 2), + ) + == "2022-01-02" + ) + assert ( + utils.apply_csv_formatting_to_scalar( + datetime(2022, 1, 2).date(), + ) + == "2022-01-02" + ) def test_apply_csv_formatting_to_container_list(): assert utils.apply_csv_formatting_to_container( - ['foo', 123, 123.45, 123.0, datetime(2022, 1, 2)], - ) == ['foo', 123, 123.45, 123, '2022-01-02'] + ["foo", 123, 123.45, 123.0, datetime(2022, 1, 2)], + ) == ["foo", 123, 123.45, 123, "2022-01-02"] def test_apply_csv_formatting_to_container_dict(): assert utils.apply_csv_formatting_to_container( { - 'foo': 123, - 'bar': 123.45, - 'baz': 123.0, - 'qux': datetime(2022, 1, 2), + "foo": 123, + "bar": 123.45, + "baz": 123.0, + "qux": datetime(2022, 1, 2), }, ) == { - 'foo': 123, - 'bar': 123.45, - 'baz': 123, - 'qux': '2022-01-02', + "foo": 123, + "bar": 123.45, + "baz": 123, + "qux": "2022-01-02", } def test_apply_csv_formatting_to_container_dict_with_dicts_and_lists(): assert utils.apply_csv_formatting_to_container( { - 'foo': 123, - 'bar': 123.45, - 'baz': 123.0, - 'qux': datetime(2022, 1, 2), - 'quux': { - 'foo': 123, - 'bar': 123.45, - 'baz': 123.0, - 'qux': datetime(2022, 1, 2), + "foo": 123, + "bar": 123.45, + "baz": 123.0, + "qux": datetime(2022, 1, 2), + "quux": { + "foo": 123, + "bar": 123.45, + "baz": 123.0, + "qux": datetime(2022, 1, 2), }, - 'quuz': [ + "quuz": [ 123, 123.45, 123.0, @@ -121,20 +125,20 @@ def test_apply_csv_formatting_to_container_dict_with_dicts_and_lists(): ], }, ) == { - 'foo': 123, - 'bar': 123.45, - 'baz': 123, - 'qux': '2022-01-02', - 'quux': { - 'foo': 123, - 'bar': 123.45, - 'baz': 123, - 'qux': '2022-01-02', + "foo": 123, + "bar": 123.45, + "baz": 123, + "qux": "2022-01-02", + "quux": { + "foo": 123, + "bar": 123.45, + "baz": 123, + "qux": "2022-01-02", }, - 'quuz': [ + "quuz": [ 123, 123.45, 123, - '2022-01-02', + "2022-01-02", ], } From 5bf8146f50047eadab7024839a87f4d40976fb6c Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Sat, 4 Mar 2023 14:48:53 +0000 Subject: [PATCH 5/5] Fix tests --- quiffen/utils.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/quiffen/utils.py b/quiffen/utils.py index 6d3b56d..062e15f 100644 --- a/quiffen/utils.py +++ b/quiffen/utils.py @@ -115,32 +115,36 @@ def convert_custom_fields_to_qif_string( def apply_csv_formatting_to_scalar( obj: Any, date_format: Optional[str] = "%Y-%m-%d", -) -> Union[str, int, float]: + stringify: bool = False, +) -> Any: """Apply CSV-friendly formatting to a scalar value""" if isinstance(obj, (datetime, date)) and date_format: return obj.strftime(date_format) - elif isinstance(obj, (datetime, date)): - return obj.isoformat() elif isinstance(obj, Enum): return str(obj.value) elif isinstance(obj, Decimal): if obj % 1: return float(obj) return int(obj) - return str(obj) + elif stringify: + return str(obj) + + return obj def apply_csv_formatting_to_container( obj: Union[List[Any], Dict[Any, Any]], date_format: Optional[str] = "%Y-%m-%d", -) -> Union[List[Any], Dict[Any, Any], str, int, float]: +) -> Union[List[Any], Dict[Any, Any], Any]: """Recursively apply CSV-friendly formatting to a container""" if isinstance(obj, list): return [apply_csv_formatting_to_container(item, date_format) for item in obj] elif isinstance(obj, dict): return { apply_csv_formatting_to_scalar( - key, date_format + key, + date_format, + stringify=True, ): apply_csv_formatting_to_container(value, date_format) for key, value in obj.items() }