├── .gitignore ├── .idea ├── .gitignore ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── pyprika.iml └── vcs.xml ├── .pylintrc ├── LICENSE ├── README.md ├── pyprika-client ├── __init__.py ├── const.py ├── data │ ├── __init__.py │ ├── local │ │ ├── __init__.py │ │ └── domain_data_store.py │ └── remote │ │ ├── __init__.py │ │ └── paprika_client.py ├── domain │ ├── __init__.py │ ├── specifications.py │ └── work_units │ │ ├── __init__.py │ │ ├── backgroud_refresh_data.py │ │ ├── create_filter_specification.py │ │ ├── fetch_data.py │ │ ├── filter_recipes.py │ │ ├── link_models.py │ │ ├── store_models.py │ │ └── transform_models.py └── framework │ ├── __init__.py │ ├── containers │ ├── __init__.py │ ├── data_container.py │ ├── model_container.py │ └── work_unit_container.py │ ├── models │ ├── __init__.py │ ├── base_model.py │ ├── bookmark.py │ ├── category.py │ ├── grocery_item.py │ ├── meal.py │ ├── menu.py │ ├── menu_item.py │ ├── pantry_item.py │ ├── recipe.py │ ├── recipe_item.py │ └── status.py │ ├── specifications.py │ └── work_unit_base.py ├── requirements.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea/ 132 | /.idea/ 133 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml 3 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/pyprika.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 21 | # number of processors available to use. 22 | jobs=1 23 | 24 | # Control the amount of potential inferred values when inferring a single 25 | # object. This can help the performance when dealing with large functions or 26 | # complex, nested conditions. 27 | limit-inference-results=100 28 | 29 | # List of plugins (as comma separated values of python module names) to load, 30 | # usually to register additional checkers. 31 | load-plugins= 32 | 33 | # Pickle collected data for later comparisons. 34 | persistent=yes 35 | 36 | # Specify a configuration file. 37 | #rcfile= 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | file-ignored, 77 | suppressed-message, 78 | useless-suppression, 79 | deprecated-pragma, 80 | use-symbolic-message-instead, 81 | apply-builtin, 82 | basestring-builtin, 83 | buffer-builtin, 84 | cmp-builtin, 85 | coerce-builtin, 86 | execfile-builtin, 87 | file-builtin, 88 | long-builtin, 89 | raw_input-builtin, 90 | reduce-builtin, 91 | standarderror-builtin, 92 | unicode-builtin, 93 | xrange-builtin, 94 | coerce-method, 95 | delslice-method, 96 | getslice-method, 97 | setslice-method, 98 | no-absolute-import, 99 | old-division, 100 | dict-iter-method, 101 | dict-view-method, 102 | next-method-called, 103 | metaclass-assignment, 104 | indexing-exception, 105 | raising-string, 106 | reload-builtin, 107 | oct-method, 108 | hex-method, 109 | nonzero-method, 110 | cmp-method, 111 | input-builtin, 112 | round-builtin, 113 | intern-builtin, 114 | unichr-builtin, 115 | map-builtin-not-iterating, 116 | zip-builtin-not-iterating, 117 | range-builtin-not-iterating, 118 | filter-builtin-not-iterating, 119 | using-cmp-argument, 120 | eq-without-hash, 121 | div-method, 122 | idiv-method, 123 | rdiv-method, 124 | exception-message-attribute, 125 | invalid-str-codec, 126 | sys-max-int, 127 | bad-python3-import, 128 | deprecated-string-function, 129 | deprecated-str-translate-call, 130 | deprecated-itertools-function, 131 | deprecated-types-field, 132 | next-method-defined, 133 | dict-items-not-iterating, 134 | dict-keys-not-iterating, 135 | dict-values-not-iterating, 136 | deprecated-operator-function, 137 | deprecated-urllib-function, 138 | xreadlines-attribute, 139 | deprecated-sys-function, 140 | exception-escape, 141 | comprehension-escape, 142 | unused-argument, 143 | no-member, 144 | too-few-public-methods, 145 | too-few-arguments, 146 | arguments-differ 147 | 148 | # Enable the message, report, category or checker with the given id(s). You can 149 | # either give multiple identifier separated by comma (,) or put this option 150 | # multiple time (only on the command line, not in the configuration file where 151 | # it should appear only once). See also the "--disable" option for examples. 152 | enable=c-extension-no-member 153 | 154 | 155 | [REPORTS] 156 | 157 | # Python expression which should return a score less than or equal to 10. You 158 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 159 | # which contain the number of messages in each category, as well as 'statement' 160 | # which is the total number of statements analyzed. This score is used by the 161 | # global evaluation report (RP0004). 162 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 163 | 164 | # Template used to display messages. This is a python new-style format string 165 | # used to format the message information. See doc for all details. 166 | #msg-template= 167 | 168 | # Set the output format. Available formats are text, parseable, colorized, json 169 | # and msvs (visual studio). You can also give a reporter class, e.g. 170 | # mypackage.mymodule.MyReporterClass. 171 | output-format=text 172 | 173 | # Tells whether to display a full report or only the messages. 174 | reports=no 175 | 176 | # Activate the evaluation score. 177 | score=yes 178 | 179 | 180 | [REFACTORING] 181 | 182 | # Maximum number of nested blocks for function / method body 183 | max-nested-blocks=5 184 | 185 | # Complete name of functions that never returns. When checking for 186 | # inconsistent-return-statements if a never returning function is called then 187 | # it will be considered as an explicit return statement and no message will be 188 | # printed. 189 | never-returning-functions=sys.exit 190 | 191 | 192 | [LOGGING] 193 | 194 | # Format style used to check logging format string. `old` means using % 195 | # formatting, `new` is for `{}` formatting,and `fstr` is for f-strings. 196 | logging-format-style=old 197 | 198 | # Logging modules to check that the string format arguments are in logging 199 | # function parameter format. 200 | logging-modules=logging 201 | 202 | 203 | [SPELLING] 204 | 205 | # Limits count of emitted suggestions for spelling mistakes. 206 | max-spelling-suggestions=4 207 | 208 | # Spelling dictionary name. Available dictionaries: none. To make it work, 209 | # install the python-enchant package. 210 | spelling-dict= 211 | 212 | # List of comma separated words that should not be checked. 213 | spelling-ignore-words= 214 | 215 | # A path to a file that contains the private dictionary; one word per line. 216 | spelling-private-dict-file= 217 | 218 | # Tells whether to store unknown words to the private dictionary (see the 219 | # --spelling-private-dict-file option) instead of raising a message. 220 | spelling-store-unknown-words=no 221 | 222 | 223 | [MISCELLANEOUS] 224 | 225 | # List of note tags to take in consideration, separated by a comma. 226 | notes=FIXME, 227 | XXX, 228 | TODO 229 | 230 | 231 | [TYPECHECK] 232 | 233 | # List of decorators that produce context managers, such as 234 | # contextlib.contextmanager. Add to this list to register other decorators that 235 | # produce valid context managers. 236 | contextmanager-decorators=contextlib.contextmanager 237 | 238 | # List of members which are set dynamically and missed by pylint inference 239 | # system, and so shouldn't trigger E1101 when accessed. Python regular 240 | # expressions are accepted. 241 | generated-members= 242 | 243 | # Tells whether missing members accessed in mixin class should be ignored. A 244 | # mixin class is detected if its name ends with "mixin" (case insensitive). 245 | ignore-mixin-members=yes 246 | 247 | # Tells whether to warn about missing members when the owner of the attribute 248 | # is inferred to be None. 249 | ignore-none=yes 250 | 251 | # This flag controls whether pylint should warn about no-member and similar 252 | # checks whenever an opaque object is returned when inferring. The inference 253 | # can return multiple potential results while evaluating a Python object, but 254 | # some branches might not be evaluated, which results in partial inference. In 255 | # that case, it might be useful to still emit no-member and other checks for 256 | # the rest of the inferred objects. 257 | ignore-on-opaque-inference=yes 258 | 259 | # List of class names for which member attributes should not be checked (useful 260 | # for classes with dynamically set attributes). This supports the use of 261 | # qualified names. 262 | ignored-classes=optparse.Values,thread._local,_thread._local 263 | 264 | # List of module names for which member attributes should not be checked 265 | # (useful for modules/projects where namespaces are manipulated during runtime 266 | # and thus existing member attributes cannot be deduced by static analysis). It 267 | # supports qualified module names, as well as Unix pattern matching. 268 | ignored-modules= 269 | 270 | # Show a hint with possible names when a member name was not found. The aspect 271 | # of finding the hint is based on edit distance. 272 | missing-member-hint=yes 273 | 274 | # The minimum edit distance a name should have in order to be considered a 275 | # similar match for a missing member name. 276 | missing-member-hint-distance=1 277 | 278 | # The total number of similar names that should be taken in consideration when 279 | # showing a hint for a missing member. 280 | missing-member-max-choices=1 281 | 282 | # List of decorators that change the signature of a decorated function. 283 | signature-mutators= 284 | 285 | 286 | [VARIABLES] 287 | 288 | # List of additional names supposed to be defined in builtins. Remember that 289 | # you should avoid defining new builtins when possible. 290 | additional-builtins= 291 | 292 | # Tells whether unused global variables should be treated as a violation. 293 | allow-global-unused-variables=yes 294 | 295 | # List of strings which can identify a callback function by name. A callback 296 | # name must start or end with one of those strings. 297 | callbacks=cb_, 298 | _cb 299 | 300 | # A regular expression matching the name of dummy variables (i.e. expected to 301 | # not be used). 302 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 303 | 304 | # Argument names that match this expression will be ignored. Default to name 305 | # with leading underscore. 306 | ignored-argument-names=_.*|^ignored_|^unused_ 307 | 308 | # Tells whether we should check for unused import in __init__ files. 309 | init-import=no 310 | 311 | # List of qualified module names which can have objects that can redefine 312 | # builtins. 313 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 314 | 315 | 316 | [FORMAT] 317 | 318 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 319 | expected-line-ending-format= 320 | 321 | # Regexp for a line that is allowed to be longer than the limit. 322 | ignore-long-lines=^\s*(# )??$ 323 | 324 | # Number of spaces of indent required inside a hanging or continued line. 325 | indent-after-paren=4 326 | 327 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 328 | # tab). 329 | indent-string=' ' 330 | 331 | # Maximum number of characters on a single line. 332 | max-line-length=100 333 | 334 | # Maximum number of lines in a module. 335 | max-module-lines=1000 336 | 337 | # List of optional constructs for which whitespace checking is disabled. `dict- 338 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 339 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 340 | # `empty-line` allows space-only lines. 341 | no-space-check=trailing-comma, 342 | dict-separator 343 | 344 | # Allow the body of a class to be on the same line as the declaration if body 345 | # contains single statement. 346 | single-line-class-stmt=no 347 | 348 | # Allow the body of an if to be on the same line as the test if there is no 349 | # else. 350 | single-line-if-stmt=no 351 | 352 | 353 | [SIMILARITIES] 354 | 355 | # Ignore comments when computing similarities. 356 | ignore-comments=yes 357 | 358 | # Ignore docstrings when computing similarities. 359 | ignore-docstrings=yes 360 | 361 | # Ignore imports when computing similarities. 362 | ignore-imports=no 363 | 364 | # Minimum lines number of a similarity. 365 | min-similarity-lines=4 366 | 367 | 368 | [BASIC] 369 | 370 | # Naming style matching correct argument names. 371 | argument-naming-style=snake_case 372 | 373 | # Regular expression matching correct argument names. Overrides argument- 374 | # naming-style. 375 | #argument-rgx= 376 | 377 | # Naming style matching correct attribute names. 378 | attr-naming-style=snake_case 379 | 380 | # Regular expression matching correct attribute names. Overrides attr-naming- 381 | # style. 382 | #attr-rgx= 383 | 384 | # Bad variable names which should always be refused, separated by a comma. 385 | bad-names=foo, 386 | bar, 387 | baz, 388 | toto, 389 | tutu, 390 | tata 391 | 392 | # Naming style matching correct class attribute names. 393 | class-attribute-naming-style=any 394 | 395 | # Regular expression matching correct class attribute names. Overrides class- 396 | # attribute-naming-style. 397 | #class-attribute-rgx= 398 | 399 | # Naming style matching correct class names. 400 | class-naming-style=PascalCase 401 | 402 | # Regular expression matching correct class names. Overrides class-naming- 403 | # style. 404 | #class-rgx= 405 | 406 | # Naming style matching correct constant names. 407 | const-naming-style=UPPER_CASE 408 | 409 | # Regular expression matching correct constant names. Overrides const-naming- 410 | # style. 411 | #const-rgx= 412 | 413 | # Minimum line length for functions/classes that require docstrings, shorter 414 | # ones are exempt. 415 | docstring-min-length=-1 416 | 417 | # Naming style matching correct function names. 418 | function-naming-style=snake_case 419 | 420 | # Regular expression matching correct function names. Overrides function- 421 | # naming-style. 422 | #function-rgx= 423 | 424 | # Good variable names which should always be accepted, separated by a comma. 425 | good-names=i, 426 | j, 427 | k, 428 | ex, 429 | Run, 430 | _ 431 | 432 | # Include a hint for the correct naming format with invalid-name. 433 | include-naming-hint=no 434 | 435 | # Naming style matching correct inline iteration names. 436 | inlinevar-naming-style=any 437 | 438 | # Regular expression matching correct inline iteration names. Overrides 439 | # inlinevar-naming-style. 440 | #inlinevar-rgx= 441 | 442 | # Naming style matching correct method names. 443 | method-naming-style=snake_case 444 | 445 | # Regular expression matching correct method names. Overrides method-naming- 446 | # style. 447 | #method-rgx= 448 | 449 | # Naming style matching correct module names. 450 | module-naming-style=snake_case 451 | 452 | # Regular expression matching correct module names. Overrides module-naming- 453 | # style. 454 | #module-rgx= 455 | 456 | # Colon-delimited sets of names that determine each other's naming style when 457 | # the name regexes allow several styles. 458 | name-group= 459 | 460 | # Regular expression which should only match function or class names that do 461 | # not require a docstring. 462 | no-docstring-rgx=^_ 463 | 464 | # List of decorators that produce properties, such as abc.abstractproperty. Add 465 | # to this list to register other decorators that produce valid properties. 466 | # These decorators are taken in consideration only for invalid-name. 467 | property-classes=abc.abstractproperty 468 | 469 | # Naming style matching correct variable names. 470 | variable-naming-style=snake_case 471 | 472 | # Regular expression matching correct variable names. Overrides variable- 473 | # naming-style. 474 | #variable-rgx= 475 | 476 | 477 | [STRING] 478 | 479 | # This flag controls whether the implicit-str-concat-in-sequence should 480 | # generate a warning on implicit string concatenation in sequences defined over 481 | # several lines. 482 | check-str-concat-over-line-jumps=no 483 | 484 | 485 | [IMPORTS] 486 | 487 | # List of modules that can be imported at any level, not just the top level 488 | # one. 489 | allow-any-import-level= 490 | 491 | # Allow wildcard imports from modules that define __all__. 492 | allow-wildcard-with-all=no 493 | 494 | # Analyse import fallback blocks. This can be used to support both Python 2 and 495 | # 3 compatible code, which means that the block might have code that exists 496 | # only in one or another interpreter, leading to false positives when analysed. 497 | analyse-fallback-blocks=no 498 | 499 | # Deprecated modules which should not be used, separated by a comma. 500 | deprecated-modules=optparse,tkinter.tix 501 | 502 | # Create a graph of external dependencies in the given file (report RP0402 must 503 | # not be disabled). 504 | ext-import-graph= 505 | 506 | # Create a graph of every (i.e. internal and external) dependencies in the 507 | # given file (report RP0402 must not be disabled). 508 | import-graph= 509 | 510 | # Create a graph of internal dependencies in the given file (report RP0402 must 511 | # not be disabled). 512 | int-import-graph= 513 | 514 | # Force import order to recognize a module as part of the standard 515 | # compatibility libraries. 516 | known-standard-library= 517 | 518 | # Force import order to recognize a module as part of a third party library. 519 | known-third-party=enchant 520 | 521 | # Couples of modules and preferred modules, separated by a comma. 522 | preferred-modules= 523 | 524 | 525 | [CLASSES] 526 | 527 | # List of method names used to declare (i.e. assign) instance attributes. 528 | defining-attr-methods=__init__, 529 | __new__, 530 | setUp, 531 | __post_init__ 532 | 533 | # List of member names, which should be excluded from the protected access 534 | # warning. 535 | exclude-protected=_asdict, 536 | _fields, 537 | _replace, 538 | _source, 539 | _make 540 | 541 | # List of valid names for the first argument in a class method. 542 | valid-classmethod-first-arg=cls 543 | 544 | # List of valid names for the first argument in a metaclass class method. 545 | valid-metaclass-classmethod-first-arg=cls 546 | 547 | 548 | [DESIGN] 549 | 550 | # Maximum number of arguments for function / method. 551 | max-args=5 552 | 553 | # Maximum number of attributes for a class (see R0902). 554 | max-attributes=7 555 | 556 | # Maximum number of boolean expressions in an if statement (see R0916). 557 | max-bool-expr=5 558 | 559 | # Maximum number of branch for function / method body. 560 | max-branches=12 561 | 562 | # Maximum number of locals for function / method body. 563 | max-locals=15 564 | 565 | # Maximum number of parents for a class (see R0901). 566 | max-parents=7 567 | 568 | # Maximum number of public methods for a class (see R0904). 569 | max-public-methods=20 570 | 571 | # Maximum number of return / yield for function / method body. 572 | max-returns=6 573 | 574 | # Maximum number of statements in function / method body. 575 | max-statements=50 576 | 577 | # Minimum number of public methods for a class (see R0903). 578 | min-public-methods=2 579 | 580 | 581 | [EXCEPTIONS] 582 | 583 | # Exceptions that will emit a warning when being caught. Defaults to 584 | # "BaseException, Exception". 585 | overgeneral-exceptions=BaseException, 586 | Exception 587 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Teagan Glenn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyprika 2 | Python Package to talk to Paprika's backend server. 3 | 4 | ## Features 5 | * Configurable periodic retrieval of data 6 | * Recipes, Categories, Meals, Menus are all linked via relational id 7 | * Ability to filter recipes that include categories, exclude categories, total cook/prep duration, recipe difficulty and recipe names. 8 | 9 | ## Usage 10 | ### Initialize 11 | Initialize `Pyprika` with your username and password from your mobile app. If you so choose, you can also tell it to auto fetch after a certain delay: 12 | 13 | ```python 14 | pyprika = Pyprika(username, password) 15 | ``` 16 | 17 | ```python 18 | pyprika = Pyprika(username, password, fetch_delay=timedelta(hours=2), auto_fetch=True) 19 | ``` 20 | 21 | ### Get all data 22 | 23 | ```python 24 | recipe_book = pyprika.get_all() 25 | ``` 26 | 27 | ### Filter recipes 28 | 29 | ```python 30 | recipes = pyprika.get_recipes( 31 | categories=None, 32 | not_categories=None, 33 | difficulty=None, 34 | duration=None, 35 | name_like=None, 36 | name_not_like=None 37 | ) 38 | ``` 39 | **NOTE** All arguments here are optional. Passing no arguments will return every recipe. 40 | 41 | ### Enable/disable auto fetch 42 | 43 | ```python 44 | pyprika.set_auto_fetch(True) #Enable auto-fetch after delay 45 | pyprika.set_auto_fetch(False) #Disable auto-fetch immediately 46 | ``` -------------------------------------------------------------------------------- /pyprika-client/__init__.py: -------------------------------------------------------------------------------- 1 | """Library for communicating with Paprika backend servers.""" 2 | 3 | from datetime import timedelta 4 | 5 | from pyprika.data.local.domain_data_store import DomainDataStore 6 | from pyprika.data.remote.paprika_client import PaprikaClient 7 | from pyprika.domain.work_units.backgroud_refresh_data import BackgroundRefreshData 8 | from pyprika.domain.work_units.create_filter_specification import CreateFilterSpecification 9 | from pyprika.domain.work_units.fetch_data import FetchData 10 | from pyprika.domain.work_units.filter_recipes import FilterRecipes 11 | from pyprika.domain.work_units.link_models import LinkModels 12 | from pyprika.domain.work_units.store_models import StoreModels 13 | from pyprika.domain.work_units.transform_models import TransformModels 14 | from pyprika.framework.containers.data_container import DataContainer 15 | from pyprika.framework.containers.work_unit_container import WorkUnitContainer 16 | 17 | 18 | class Pyprika: 19 | """Main entry point to library.""" 20 | 21 | def __init__(self, username, password, fetch_delay=timedelta(hours=2)): 22 | """Initialize library.""" 23 | self._data_container = DataContainer( 24 | PaprikaClient(username, password), 25 | DomainDataStore() 26 | ) 27 | 28 | store_models = StoreModels(self._data_container.domain_data_store) 29 | link_models = LinkModels(store_models) 30 | transform_models = TransformModels(link_models) 31 | fetch_data = FetchData( 32 | self._data_container.client, 33 | transform_models, 34 | self._data_container.domain_data_store 35 | ) 36 | background_refresh = BackgroundRefreshData(fetch_data, fetch_delay.total_seconds()) 37 | filter_recipes = FilterRecipes(self._data_container.domain_data_store) 38 | create_filter_specifications = CreateFilterSpecification(filter_recipes) 39 | 40 | self._work_unit_container = WorkUnitContainer( 41 | background_refresh, 42 | fetch_data, 43 | transform_models, 44 | link_models, 45 | store_models, 46 | filter_recipes, 47 | create_filter_specifications 48 | ) 49 | 50 | def get_all(self): 51 | return self._data_container.domain_data_store.data 52 | 53 | def get_recipes( 54 | self, 55 | categories=None, 56 | not_categories=None, 57 | difficulty=None, 58 | duration=None, 59 | name_like=None, 60 | name_not_like=None): 61 | return self._work_unit_container.create_filter_specifications.perform_work( 62 | categories=categories, 63 | not_categories=not_categories, 64 | difficulty=difficulty, 65 | duration=duration, 66 | name_like=name_like, 67 | name_not_like=name_not_like, 68 | ) 69 | -------------------------------------------------------------------------------- /pyprika-client/const.py: -------------------------------------------------------------------------------- 1 | """Constants for use throughout package.""" 2 | 3 | BASE_URL = 'https://www.paprikaapp.com/api/v1/sync/' 4 | 5 | CLIENT_USER_AGENT = 'Pyprika Python Library' 6 | APPLICATION_JSON = 'application/json' 7 | -------------------------------------------------------------------------------- /pyprika-client/data/__init__.py: -------------------------------------------------------------------------------- 1 | """API integration to Paprika backend.""" 2 | -------------------------------------------------------------------------------- /pyprika-client/data/local/__init__.py: -------------------------------------------------------------------------------- 1 | """Local data service and storage.""" 2 | -------------------------------------------------------------------------------- /pyprika-client/data/local/domain_data_store.py: -------------------------------------------------------------------------------- 1 | """Local in-memory data store.""" 2 | 3 | 4 | class DomainDataStore: 5 | """Data store for domain.""" 6 | 7 | def __init__(self): 8 | """Initialize the data store.""" 9 | self._data = None 10 | 11 | @property 12 | def data(self): 13 | """Get current domain data.""" 14 | return self._data 15 | 16 | @data.setter 17 | def data(self, value): 18 | """Update the data store and last fetch time.""" 19 | self._data = value 20 | -------------------------------------------------------------------------------- /pyprika-client/data/remote/__init__.py: -------------------------------------------------------------------------------- 1 | """Remote data providers.""" 2 | -------------------------------------------------------------------------------- /pyprika-client/data/remote/paprika_client.py: -------------------------------------------------------------------------------- 1 | """Client for communicating with the Paprika servers.""" 2 | import asyncio 3 | import json 4 | import logging 5 | from timeit import default_timer 6 | 7 | import async_timeout 8 | from aiohttp import BasicAuth, ClientSession 9 | from aiohttp.hdrs import USER_AGENT, ACCEPT, AUTHORIZATION 10 | 11 | from pyprika.const import CLIENT_USER_AGENT, APPLICATION_JSON, BASE_URL 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | KEY_RESPONSE = 'resp' 16 | KEY_ATTR = 'url' 17 | KEY_ELAPSED = 'elapsed' 18 | 19 | ATTR_RECIPES = 'recipes' 20 | ATTR_RECIPE_ITEMS = 'recipe_items' 21 | RECIPE_ENDPOINT = 'recipe/%s' 22 | 23 | ENDPOINTS = [ 24 | 'bookmarks', 25 | 'categories', 26 | 'groceries', 27 | 'meals', 28 | 'menus', 29 | 'menuitems', 30 | 'pantry', 31 | ATTR_RECIPES, 32 | 'status' 33 | ] 34 | 35 | 36 | async def _fetch(auth, headers, url, session, attr_override=None): 37 | """Fetch a single URL """ 38 | end_point = url if url.endswith('/') else (url + '/') 39 | uri = "%s%s" % (BASE_URL, end_point) 40 | with async_timeout.timeout(10): 41 | async with session.get( 42 | uri, 43 | auth=auth, 44 | headers=headers, 45 | allow_redirects=True) as response: 46 | before_request = default_timer() 47 | resp = await response.read() 48 | elapsed = default_timer() - before_request 49 | 50 | return { 51 | KEY_RESPONSE: json.loads(resp).get("result"), 52 | KEY_ATTR: url if not attr_override else attr_override, 53 | KEY_ELAPSED: elapsed 54 | } 55 | 56 | 57 | class PaprikaClient: 58 | """Client to connect to Paprika backend servers.""" 59 | 60 | def __init__(self, username, password): 61 | """Initialize the client.""" 62 | self._auth = BasicAuth(username, password) 63 | self._headers = { 64 | USER_AGENT: CLIENT_USER_AGENT, 65 | ACCEPT: APPLICATION_JSON, 66 | } 67 | 68 | def _process_responses(self, results): 69 | """Process the responses from fetch_all.""" 70 | for result in results: 71 | url = result[KEY_ATTR] 72 | response = result[KEY_RESPONSE] 73 | 74 | self.__setattr__(url, response) 75 | 76 | async def fetch_all(self): 77 | """Fetch all data from the backend servers.""" 78 | tasks = [] 79 | async with ClientSession(auth=self._auth, headers=self._headers) as session: 80 | for url in ENDPOINTS: 81 | attr_override = ATTR_RECIPE_ITEMS if url == ATTR_RECIPES else None 82 | task = asyncio.ensure_future( 83 | _fetch( 84 | self._auth, 85 | self._headers, 86 | url, 87 | session, 88 | attr_override 89 | ) 90 | ) 91 | tasks.append(task) 92 | fetch_results = await asyncio.gather(*tasks) 93 | self._process_responses(fetch_results) 94 | 95 | self.__setattr__(ATTR_RECIPES, []) 96 | try: 97 | recipe_items = self.__getattribute__(ATTR_RECIPE_ITEMS) 98 | if not recipe_items: 99 | return 100 | tasks = [asyncio.ensure_future( 101 | _fetch( 102 | self._auth, 103 | self._headers, 104 | RECIPE_ENDPOINT % recipe_item['uid'], 105 | session, 106 | ATTR_RECIPES 107 | )) for recipe_item in recipe_items if recipe_item.get('uid', None)] 108 | fetch_results = await asyncio.gather(*tasks) 109 | self.__setattr__(ATTR_RECIPES, [result[KEY_RESPONSE] for result in fetch_results]) 110 | except Exception as err: 111 | _LOGGER.error(str(err)) 112 | 113 | def get_bookmarks(self): 114 | """Get bookmark json resources.""" 115 | return self.__getattribute__('bookmarks') or [] 116 | 117 | def get_categories(self): 118 | """Get category json resources.""" 119 | return self.__getattribute__('categories') or [] 120 | 121 | def get_groceries(self): 122 | """Get grocery json resources.""" 123 | return self.__getattribute__('groceries') or [] 124 | 125 | def get_meals(self): 126 | """Get meal json resources.""" 127 | return self.__getattribute__('meals') or [] 128 | 129 | def get_menus(self): 130 | """Get menu json resources.""" 131 | return self.__getattribute__('menus') or [] 132 | 133 | def get_menu_items(self): 134 | """Get menu item json resources.""" 135 | return self.__getattribute__('menuitems') or [] 136 | 137 | def get_pantry_items(self): 138 | """Get pantry item json resources.""" 139 | return self.__getattribute__('pantry') or [] 140 | 141 | def get_recipes(self): 142 | """Get recipes json resources.""" 143 | return self.__getattribute__('recipes') or [] 144 | 145 | def get_status(self): 146 | """Get recipe book status json resources.""" 147 | return self.__getattribute__('status') or {} 148 | -------------------------------------------------------------------------------- /pyprika-client/domain/__init__.py: -------------------------------------------------------------------------------- 1 | """Internal library logic.""" 2 | -------------------------------------------------------------------------------- /pyprika-client/domain/specifications.py: -------------------------------------------------------------------------------- 1 | """Specifiations for filtering ittems.""" 2 | import re 3 | 4 | from pyprika.framework.specifications import Specification 5 | 6 | REGEX_HOURS = re.compile(r"(\d*\.\d+|\d+) *(?:\bhour\w?\b)|(?:\bhr\b)", re.RegexFlag.I) 7 | REGEX_MINUTES = re.compile(r"(\d*\.\d+|\d+) +(?:\bmin.*?\b)", re.RegexFlag.I) 8 | 9 | 10 | def _get_cook_minutes(candidate): 11 | """Get the cook time in minutes.""" 12 | hours = 0.0 13 | minutes = 0.0 14 | for recipe_time in [candidate.cook_time, candidate.prep_time]: 15 | if recipe_time: 16 | hour_match = REGEX_HOURS.match(recipe_time) 17 | minute_match = REGEX_MINUTES.match(recipe_time) 18 | if hour_match and hour_match.group(1): 19 | hours += float(hour_match.group(1)) 20 | if minute_match and minute_match.group(1): 21 | minutes += float(minute_match.group(1)) 22 | 23 | return hours * 60 + minutes 24 | 25 | 26 | class CategorySpecification(Specification): 27 | """Specification on category.""" 28 | 29 | __slots__ = ['category'] 30 | 31 | def __init__(self, category): 32 | """Initialize specifications.""" 33 | self.category = category 34 | 35 | def is_satisfied_by(self, candidate): 36 | """Checks if candidate satisfies condition.""" 37 | return str(self.category).lower() in [str(name).lower() for name in 38 | candidate.category_names] 39 | 40 | 41 | class DifficultySpecification(Specification): 42 | """Specification on difficulty.""" 43 | 44 | __slots__ = ['difficulty'] 45 | 46 | def __init__(self, difficulty): 47 | """Initialize specifications.""" 48 | self.difficulty = difficulty 49 | 50 | def is_satisfied_by(self, candidate): 51 | """Checks if candidate satisfies condition.""" 52 | return str(self.difficulty).lower() in str(candidate.difficulty).lower() 53 | 54 | 55 | class NameSpecification(Specification): 56 | """Specification on name.""" 57 | 58 | __slots__ = ['name'] 59 | 60 | def __init__(self, name): 61 | """Initialize specifications.""" 62 | self.name = name 63 | 64 | def is_satisfied_by(self, candidate): 65 | """Checks if candidate satisfies condition.""" 66 | return str(self.name).lower() in str(candidate.name).lower() 67 | 68 | 69 | class DurationSpecification(Specification): 70 | """Specification on total cook time.""" 71 | 72 | __slots__ = ['duration'] 73 | 74 | def __init__(self, duration): 75 | """Initialize specifications.""" 76 | self.duration = duration 77 | 78 | def is_satisfied_by(self, candidate): 79 | """Checks if candidate satisfies condition.""" 80 | return _get_cook_minutes(candidate) <= float(self.duration) 81 | -------------------------------------------------------------------------------- /pyprika-client/domain/work_units/__init__.py: -------------------------------------------------------------------------------- 1 | """Individual units of work.""" 2 | -------------------------------------------------------------------------------- /pyprika-client/domain/work_units/backgroud_refresh_data.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import atexit 4 | import threading 5 | 6 | from pyprika.framework.work_unit_base import AsyncWorkUnit 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | class BackgroundRefreshData(AsyncWorkUnit): 12 | """Unit of work to refresh data in background.""" 13 | 14 | __slots__ = ['fetch_data', 'interval_minutes'] 15 | 16 | def __init__(self, fetch_data, interval_seconds): 17 | """Initialize the unit of work.""" 18 | atexit.register(self._exit_handler) 19 | 20 | self.fetch_data = fetch_data 21 | self.interval_seconds = interval_seconds 22 | self._running = True 23 | self._loop = asyncio.get_event_loop() 24 | self._thread = threading.Thread(target=self._loop_in_thread) 25 | self._thread.start() 26 | 27 | def _loop_in_thread(self): 28 | asyncio.set_event_loop(self._loop) 29 | try: 30 | self._loop.run_until_complete(asyncio.ensure_future(self.perform_work())) 31 | except asyncio.CancelledError: 32 | pass 33 | 34 | def _exit_handler(self): 35 | self._running = False 36 | 37 | async def perform_work(self): 38 | while self._running: 39 | await self.fetch_data.perform_work() 40 | await asyncio.sleep(self.interval_seconds) 41 | -------------------------------------------------------------------------------- /pyprika-client/domain/work_units/create_filter_specification.py: -------------------------------------------------------------------------------- 1 | """Create filter specifications based on provided inputts.""" 2 | import logging 3 | 4 | from pyprika.domain.specifications import * 5 | from pyprika.framework.specifications import TrueSpecification 6 | from pyprika.framework.work_unit_base import WorkUnit 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | def _build_specification(values, spec_type, invert=False): 12 | if values is None: 13 | return 14 | if not isinstance(values, list): 15 | values = [values] 16 | specification = TrueSpecification() 17 | for value in values: 18 | if invert: 19 | specification = specification or ~spec_type(value) 20 | else: 21 | specification = specification and spec_type(value) 22 | 23 | return specification 24 | 25 | 26 | class CreateFilterSpecification(WorkUnit): 27 | """Filter recipes unit of work.""" 28 | 29 | __slots__ = ['filter_recipes'] 30 | 31 | def __init__(self, filter_recipes): 32 | self.filter_recipes = filter_recipes 33 | 34 | def perform_work(self, 35 | categories=None, 36 | not_categories=None, 37 | difficulty=None, 38 | duration=None, 39 | name_like=None, 40 | name_not_like=None, 41 | limit=10): 42 | """Perform unit of work.""" 43 | specification = TrueSpecification() 44 | if categories: 45 | _LOGGER.error("CATEGORIES {}".format(categories)) 46 | specification = specification and _build_specification(categories, CategorySpecification) 47 | if not_categories: 48 | specification = specification and _build_specification(not_categories, CategorySpecification, True) 49 | if difficulty: 50 | specification = specification and _build_specification(difficulty, DifficultySpecification) 51 | if name_like: 52 | specification = specification and _build_specification(name_like, NameSpecification) 53 | if name_not_like: 54 | specification = specification and _build_specification(name_not_like, NameSpecification, True) 55 | 56 | if duration: 57 | try: 58 | float_duration = float(duration) 59 | specification = specification and _build_specification([float_duration], DurationSpecification) 60 | except ValueError: 61 | _LOGGER.error("Duration is not a float") 62 | 63 | return self.filter_recipes.perform_work(specification) 64 | -------------------------------------------------------------------------------- /pyprika-client/domain/work_units/fetch_data.py: -------------------------------------------------------------------------------- 1 | """Unit of work that fetches data from backend servers.""" 2 | import logging 3 | 4 | from pyprika.framework.work_unit_base import AsyncWorkUnit 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | 8 | 9 | class FetchData(AsyncWorkUnit): 10 | """Retrieve models unit of work.""" 11 | 12 | __slots__ = ['client', 'transform_models', 'domain_data_store'] 13 | 14 | def __init__(self, client, transform_models, domain_data_store): 15 | """Initialize unit of work.""" 16 | self.client = client 17 | self.transform_models = transform_models 18 | self.domain_data_store = domain_data_store 19 | 20 | async def perform_work(self): 21 | """Perform work unit.""" 22 | _LOGGER.warning("Invoking client fetch all") 23 | await self.client.fetch_all() 24 | return await self.transform_models.perform_work( 25 | bookmarks=self.client.get_bookmarks(), 26 | categories=self.client.get_categories(), 27 | groceries=self.client.get_groceries(), 28 | meals=self.client.get_meals(), 29 | menus=self.client.get_menus(), 30 | menu_items=self.client.get_menu_items(), 31 | pantry_items=self.client.get_pantry_items(), 32 | recipes=self.client.get_recipes(), 33 | status=self.client.get_status() 34 | ) 35 | -------------------------------------------------------------------------------- /pyprika-client/domain/work_units/filter_recipes.py: -------------------------------------------------------------------------------- 1 | """Unit of work that filters recipes by a given specifications.""" 2 | import logging 3 | 4 | from pyprika.framework.work_unit_base import WorkUnit 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | 8 | 9 | class FilterRecipes(WorkUnit): 10 | """Filter recipes unit of work.""" 11 | 12 | __slots__ = ['domain_data_store'] 13 | 14 | def __init__(self, domain_data_store): 15 | self.domain_data_store = domain_data_store 16 | 17 | def perform_work(self, specification): 18 | """Perform the unit of work.""" 19 | _LOGGER.warning("Recipes: {}".format(len(self.domain_data_store.data.recipes))) 20 | return [recipe for recipe in self.domain_data_store.data.recipes if 21 | specification.is_satisfied_by(recipe)] 22 | -------------------------------------------------------------------------------- /pyprika-client/domain/work_units/link_models.py: -------------------------------------------------------------------------------- 1 | """Unit of work to link related models via relational identifier.""" 2 | from pyprika.framework.work_unit_base import AsyncWorkUnit 3 | 4 | 5 | class LinkModels(AsyncWorkUnit): 6 | """Unit of work linking models via relational ids.""" 7 | 8 | __slots__ = ['store_models'] 9 | 10 | def __init__(self, store_models): 11 | """Initialize unit of work.""" 12 | self.store_models = store_models 13 | 14 | async def perform_work(self, model_container): 15 | """Perform work unit.""" 16 | for category in model_container.categories: 17 | await category.link_to(model_container.categories) 18 | 19 | for menu_item in model_container.menu_items: 20 | await menu_item.link_to(model_container.menus, model_container.recipes) 21 | 22 | for meal in model_container.meals: 23 | await meal.link_to(model_container.recipes) 24 | 25 | for grocery_item in model_container.groceries: 26 | await grocery_item.link_to(model_container.recipes) 27 | 28 | for recipe in model_container.recipes: 29 | await recipe.link_to(model_container.categories) 30 | 31 | return await self.store_models.perform_work(model_container) 32 | -------------------------------------------------------------------------------- /pyprika-client/domain/work_units/store_models.py: -------------------------------------------------------------------------------- 1 | """Unit of work to store retrieved data in local data store.""" 2 | from pyprika.framework.work_unit_base import AsyncWorkUnit 3 | 4 | 5 | class StoreModels(AsyncWorkUnit): 6 | """Performs data store update unit of work.""" 7 | 8 | __slots__ = ['domain_data_store'] 9 | 10 | def __init__(self, domain_data_store): 11 | """Initialize the unit of work.""" 12 | self.domain_data_store = domain_data_store 13 | 14 | async def perform_work(self, model_container): 15 | """Perform unit of work.""" 16 | self.domain_data_store.data = model_container 17 | return model_container 18 | -------------------------------------------------------------------------------- /pyprika-client/domain/work_units/transform_models.py: -------------------------------------------------------------------------------- 1 | """Uni of work that transforms JSON data to data models.""" 2 | from pyprika.framework.containers.model_container import ModelContainer 3 | from pyprika.framework.models.bookmark import Bookmark 4 | from pyprika.framework.models.category import Category 5 | from pyprika.framework.models.grocery_item import GroceryItem 6 | from pyprika.framework.models.meal import Meal 7 | from pyprika.framework.models.menu import Menu 8 | from pyprika.framework.models.menu_item import MenuItem 9 | from pyprika.framework.models.pantry_item import PantryItem 10 | from pyprika.framework.models.recipe import Recipe 11 | from pyprika.framework.models.status import Status 12 | from pyprika.framework.work_unit_base import AsyncWorkUnit 13 | 14 | 15 | class TransformModels(AsyncWorkUnit): 16 | """Unit of work to create domain models.""" 17 | 18 | __slots__ = ['link_models'] 19 | 20 | def __init__(self, link_models): 21 | """Initialize unit of work.""" 22 | self.link_models = link_models 23 | 24 | async def perform_work(self, bookmarks, categories, groceries, meals, menus, menu_items, 25 | pantry_items, recipes, 26 | status): 27 | """Perform unit of work.""" 28 | model_container = ModelContainer( 29 | [Bookmark.from_json(bookmark) for bookmark in bookmarks], 30 | [Category.from_json(category) for category in categories], 31 | [GroceryItem.from_json(grocery_item) for grocery_item in groceries], 32 | [Meal.from_json(meal) for meal in meals], 33 | [Menu.from_json(menu) for menu in menus], 34 | [MenuItem.from_json(menu_item) for menu_item in menu_items], 35 | [PantryItem.from_json(pantry_item) for pantry_item in pantry_items], 36 | [Recipe.from_json(recipe) for recipe in recipes], 37 | Status.from_json(status) 38 | ) 39 | return await self.link_models.perform_work(model_container) 40 | -------------------------------------------------------------------------------- /pyprika-client/framework/__init__.py: -------------------------------------------------------------------------------- 1 | """Pyprika library root module.""" 2 | -------------------------------------------------------------------------------- /pyprika-client/framework/containers/__init__.py: -------------------------------------------------------------------------------- 1 | """Container module.""" 2 | -------------------------------------------------------------------------------- /pyprika-client/framework/containers/data_container.py: -------------------------------------------------------------------------------- 1 | """Data layer container for IoC.""" 2 | 3 | 4 | class DataContainer: 5 | """IoC container for data layer.""" 6 | __slots__ = ['client', 'domain_data_store'] 7 | 8 | def __init__(self, client, domain_data_store): 9 | """Initialize Container.""" 10 | self.client = client 11 | self.domain_data_store = domain_data_store 12 | -------------------------------------------------------------------------------- /pyprika-client/framework/containers/model_container.py: -------------------------------------------------------------------------------- 1 | """Container for transformed data models.""" 2 | 3 | 4 | class ModelContainer: 5 | """Container for data models.""" 6 | __slots__ = ['bookmarks', 'categories', 'groceries', 'meals', 'menus', 'menu_items', 'pantry', 7 | 'recipes', 'status'] 8 | 9 | def __init__(self, bookmarks, categories, groceries, meals, menus, menu_items, pantry, recipes, 10 | status): 11 | """Initialize container.""" 12 | self.bookmarks = bookmarks 13 | self.categories = categories 14 | self.groceries = groceries 15 | self.meals = meals 16 | self.menus = menus 17 | self.menu_items = menu_items 18 | self.pantry = pantry 19 | self.recipes = recipes 20 | self.status = status 21 | -------------------------------------------------------------------------------- /pyprika-client/framework/containers/work_unit_container.py: -------------------------------------------------------------------------------- 1 | """IoC Container for Injecting WorkUnits.""" 2 | 3 | 4 | class WorkUnitContainer: 5 | """IoC Container for Work Units.""" 6 | __slots__ = ['background_refresh_data', 'fetch_data', 'transform_models', 'link_models', 7 | 'store_models', 'filter_recipes', 'create_filter_specifications'] 8 | 9 | def __init__(self, 10 | background_data_refresh, 11 | fetch_data, 12 | transform_models, 13 | link_models, 14 | store_models, 15 | filter_recipes, 16 | create_filter_specifications): 17 | """Initialize container.""" 18 | 19 | self.background_refresh_data = background_data_refresh 20 | self.fetch_data = fetch_data 21 | self.transform_models = transform_models 22 | self.link_models = link_models 23 | self.store_models = store_models 24 | self.filter_recipes = filter_recipes 25 | self.create_filter_specifications = create_filter_specifications 26 | -------------------------------------------------------------------------------- /pyprika-client/framework/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Pyprika API Models.""" 2 | -------------------------------------------------------------------------------- /pyprika-client/framework/models/base_model.py: -------------------------------------------------------------------------------- 1 | """Base class for data models.""" 2 | from abc import abstractmethod, ABC 3 | 4 | 5 | class BaseModel(ABC): 6 | """Abstract base class for unitt of work.""" 7 | 8 | @abstractmethod 9 | async def link_to(self, *args): 10 | """Link to parent models.""" 11 | pass 12 | -------------------------------------------------------------------------------- /pyprika-client/framework/models/bookmark.py: -------------------------------------------------------------------------------- 1 | """Bookmark resource.""" 2 | from pyprika.framework.models.base_model import BaseModel 3 | 4 | 5 | class Bookmark(BaseModel): 6 | """Model for bookmark resources.""" 7 | 8 | __slots__ = ['url', 'title', 'uid', 'order_flag'] 9 | 10 | @staticmethod 11 | def from_json(bookmark_json): 12 | """Create model from json.""" 13 | return Bookmark( 14 | bookmark_json.get('url', None), 15 | bookmark_json.get('title', None), 16 | bookmark_json.get('uid', None), 17 | bookmark_json.get('order_flag', None) 18 | ) 19 | 20 | def __init__(self, url, title, uid, order_flag): 21 | """Initialize model.""" 22 | self.url = url 23 | self.title = title 24 | self.uid = uid 25 | self.order_flag = order_flag 26 | 27 | async def link_to(self): 28 | pass 29 | -------------------------------------------------------------------------------- /pyprika-client/framework/models/category.py: -------------------------------------------------------------------------------- 1 | """Category data model.""" 2 | from pyprika.framework.models.base_model import BaseModel 3 | 4 | 5 | class Category(BaseModel): 6 | """Model for category resource.""" 7 | 8 | __slots__ = ['name', 'uid', 'parent_uid', 'order_flag'] 9 | 10 | @staticmethod 11 | def from_json(category_json): 12 | """Create model from json.""" 13 | return Category( 14 | category_json.get('name', None), 15 | category_json.get('uid', None), 16 | category_json.get('parent_uid', None), 17 | category_json.get('order_flag', None) 18 | ) 19 | 20 | def __init__(self, name, uid, parent_uid, order_flag): 21 | """Initialize the model.""" 22 | self.name = name 23 | self.uid = uid 24 | self.parent_uid = parent_uid 25 | self.order_flag = order_flag 26 | self.parent_category = None 27 | 28 | async def link_to(self, categories): 29 | """Link categories to parents.""" 30 | self.parent_category = next( 31 | (parent_category for parent_category in categories if 32 | parent_category.uid == self.parent_uid), None) 33 | -------------------------------------------------------------------------------- /pyprika-client/framework/models/grocery_item.py: -------------------------------------------------------------------------------- 1 | """Grocery data model.""" 2 | from pyprika.framework.models.base_model import BaseModel 3 | 4 | 5 | class GroceryItem(BaseModel): 6 | """Model for grocery item resource.""" 7 | 8 | __slots__ = ['name', 'ingredient', 'recipe_name', 'purchased', 'uid', 'recipe_uid', 9 | 'order_flag'] 10 | 11 | @staticmethod 12 | def from_json(grocery_json): 13 | """Create model from json.""" 14 | return GroceryItem( 15 | grocery_json.get('name', None), 16 | grocery_json.get('ingredient', None), 17 | grocery_json.get('recipe', None), 18 | grocery_json.get('purchased', False), 19 | grocery_json.get('uid', None), 20 | grocery_json.get('recipe_uid', None), 21 | grocery_json.get('order_flag', None) 22 | ) 23 | 24 | def __init__(self, name, ingredient, recipe_name, purchased, uid, recipe_uid, order_flag): 25 | """Initialize the model.""" 26 | self.name = name 27 | self.ingredient = ingredient 28 | self.recipe_name = recipe_name 29 | self.purchased = purchased 30 | self.uid = uid 31 | self.recipe_uid = recipe_uid 32 | self.order_flag = order_flag 33 | 34 | self.recipe = None 35 | 36 | async def link_to(self, recipes): 37 | """Link to transformed recipe model.""" 38 | self.recipe = next((recipe for recipe in recipes if recipe.uid == self.recipe_uid), None) 39 | -------------------------------------------------------------------------------- /pyprika-client/framework/models/meal.py: -------------------------------------------------------------------------------- 1 | """Meal data model""" 2 | from pyprika.framework.models.base_model import BaseModel 3 | 4 | 5 | class Meal(BaseModel): 6 | """Model for a meal resource.""" 7 | 8 | __slots__ = ['name', 'type', 'date', 'uid', 'recipe_uid', 'order_flag'] 9 | 10 | @staticmethod 11 | def from_json(meal_json): 12 | """Create model from json.""" 13 | return Meal( 14 | meal_json.get('name', None), 15 | meal_json.get('type', None), 16 | meal_json.get('date', None), 17 | meal_json.get('uid', None), 18 | meal_json.get('recipe_uid', None), 19 | meal_json.get('order_flag', None) 20 | ) 21 | 22 | def __init__(self, name, meal_type, meal_date, uid, recipe_uid, order_flag): 23 | """Initialize the model.""" 24 | self.name = name 25 | self.type = meal_type 26 | self.date = meal_date 27 | self.uid = uid 28 | self.recipe_uid = recipe_uid 29 | self.order_flag = order_flag 30 | self.recipe = None 31 | 32 | async def link_to(self, recipes): 33 | """Link the meal to the associated recipe.""" 34 | self.recipe = next((recipe for recipe in recipes if recipe.uid == self.recipe_uid), None) 35 | -------------------------------------------------------------------------------- /pyprika-client/framework/models/menu.py: -------------------------------------------------------------------------------- 1 | """Menu data model.""" 2 | from pyprika.framework.models.base_model import BaseModel 3 | 4 | 5 | class Menu(BaseModel): 6 | """Model for the menu resource.""" 7 | 8 | __slots__ = ['name', 'notes', 'uid', 'order_flag'] 9 | 10 | @staticmethod 11 | def from_json(mean_json): 12 | """Create model from json.""" 13 | return Menu( 14 | mean_json.get('name', None), 15 | mean_json.get('notes', None), 16 | mean_json.get('uid', None), 17 | mean_json.get('order_flag', None) 18 | ) 19 | 20 | def __init__(self, name, notes, uid, order_flag): 21 | """Initialize model.""" 22 | self.name = name 23 | self.notes = notes 24 | self.uid = uid 25 | self.order_flag = order_flag 26 | 27 | async def link_to(self, *args): 28 | """Nothing to link to.""" 29 | pass 30 | -------------------------------------------------------------------------------- /pyprika-client/framework/models/menu_item.py: -------------------------------------------------------------------------------- 1 | """Menu item data model.""" 2 | from pyprika.framework.models.base_model import BaseModel 3 | 4 | 5 | class MenuItem(BaseModel): 6 | """Model for the menu item resource.""" 7 | 8 | __slots__ = ['name', 'uid', 'menu_uid', 'recipe_uid', 'order_flag'] 9 | 10 | @staticmethod 11 | def from_json(menu_item_json, menus, recipes): 12 | """Create model from json.""" 13 | return MenuItem( 14 | menu_item_json.get('name', None), 15 | menu_item_json.get('uid', None), 16 | menu_item_json.get('menu_uid', None), 17 | menu_item_json.get('recipe_uid', None), 18 | menu_item_json.get('order_flag', None) 19 | ) 20 | 21 | def __init__(self, name, uid, menu_uid, recipe_uid, order_flag): 22 | """Initialize model.""" 23 | self.name = name 24 | self.uid = uid 25 | self.menu_uid = menu_uid 26 | self.recipe_uid = recipe_uid 27 | self.order_flag = order_flag 28 | self.menu = None 29 | self.recipe = None 30 | 31 | async def link_to(self, menus, recipes): 32 | """Link to the associated menu and recipe models.""" 33 | self.menu = next((menu for menu in menus if menu.uid == self.menu_uid), None) 34 | self.recipe = next((recipe for recipe in recipes if recipe.uid == self.recipe_uid), None) 35 | -------------------------------------------------------------------------------- /pyprika-client/framework/models/pantry_item.py: -------------------------------------------------------------------------------- 1 | """Pantry item data model.""" 2 | from pyprika.framework.models.base_model import BaseModel 3 | 4 | 5 | class PantryItem(BaseModel): 6 | """Model for pantry item resource.""" 7 | 8 | __slots__ = ['aisle', 'ingredient', 'uid'] 9 | 10 | @staticmethod 11 | def from_json(pantry_item_json): 12 | """Create model from json.""" 13 | return PantryItem( 14 | pantry_item_json.get('aisle', None), 15 | pantry_item_json.get('ingredient', None), 16 | pantry_item_json.get('uid', None) 17 | ) 18 | 19 | def __init__(self, aisle, ingredient, uid): 20 | """Initialize the model.""" 21 | self.aisle = aisle 22 | self.ingredient = ingredient 23 | self.uid = uid 24 | 25 | async def link_to(self, *args): 26 | """Nothing to link to.""" 27 | pass 28 | -------------------------------------------------------------------------------- /pyprika-client/framework/models/recipe.py: -------------------------------------------------------------------------------- 1 | """Recipe data model.""" 2 | 3 | from pyprika.framework.models.base_model import BaseModel 4 | 5 | 6 | class Recipe(BaseModel): 7 | """Model for recipe resource.""" 8 | 9 | __slots__ = ['rating', 'photo_hash', 'on_favorites', 'photo', 'scale', 'ingredients', 'source', 10 | 'hash', 'directions', 'source_url', 'difficulty', 'category_uids', 'photo_url', 11 | 'cook_time', 'name', 'created', 'notes', 'image_url', 'prep_time', 'servings', 12 | 'nutritional_info', 'uid', 'categories'] 13 | 14 | @staticmethod 15 | def from_json(recipe_json): 16 | """Create model from json.""" 17 | return Recipe( 18 | recipe_json.get('rating', None), 19 | recipe_json.get('photo_hash', None), 20 | recipe_json.get('on_favorites', False), 21 | recipe_json.get('photo', None), 22 | recipe_json.get('scale', None), 23 | recipe_json.get('ingredients', None), 24 | recipe_json.get('source', None), 25 | recipe_json.get('hash', None), 26 | recipe_json.get('source_url', None), 27 | recipe_json.get('difficulty', None), 28 | recipe_json.get('categories', None), 29 | recipe_json.get('photo_url', None), 30 | recipe_json.get('cook_time', None), 31 | recipe_json.get('name', None), 32 | recipe_json.get('created', None), 33 | recipe_json.get('notes', None), 34 | recipe_json.get('image_url', None), 35 | recipe_json.get('prep_time', None), 36 | recipe_json.get('servings', None), 37 | recipe_json.get('nutritional_info', None), 38 | recipe_json.get('uid', None), 39 | recipe_json.get('directions', None) 40 | ) 41 | 42 | def __init__(self, rating, photo_hash, on_favorites, photo, scale, ingredients, source, hash, 43 | source_url, difficulty, category_uids, photo_url, cook_time, name, created, notes, 44 | image_url, prep_time, servings, nutritional_info, uid, directions): 45 | """Initialize the model.""" 46 | self.rating = rating 47 | self.photo_hash = photo_hash 48 | self.on_favorites = on_favorites 49 | self.photo = photo 50 | self.scale = scale 51 | self.ingredients = ingredients 52 | self.source = source 53 | self.hash = hash 54 | self.source_url = source_url 55 | self.difficulty = difficulty 56 | self.category_uids = category_uids 57 | self.photo_url = photo_url 58 | self.cook_time = cook_time 59 | self.name = name 60 | self.created = created 61 | self.notes = notes 62 | self.image_url = image_url 63 | self.prep_time = prep_time 64 | self.servings = servings 65 | self.nutritional_info = nutritional_info 66 | self.uid = uid 67 | self.directions = directions 68 | 69 | async def link_to(self, categories): 70 | """Link to associated categories.""" 71 | linked_categories = [] 72 | for category_uid in self.category_uids: 73 | linked_category = next( 74 | (category for category in categories if category.uid == category_uid), None) 75 | if not linked_category: 76 | continue 77 | linked_categories.append(linked_category) 78 | 79 | setattr(self, 'categories', linked_categories) 80 | 81 | @property 82 | def category_names(self): 83 | """Get a list of category names.""" 84 | return [category.name for category in self.categories] 85 | 86 | def __str__(self) -> str: 87 | return "{} {}".format(self.name, self.category_names) 88 | -------------------------------------------------------------------------------- /pyprika-client/framework/models/recipe_item.py: -------------------------------------------------------------------------------- 1 | """Tiny model containing the uid of recipe models.""" 2 | from pyprika.framework.models.base_model import BaseModel 3 | 4 | 5 | class RecipeItem(BaseModel): 6 | """Model for recipe item resource.""" 7 | 8 | __slots__ = ['hash', 'uid'] 9 | 10 | @staticmethod 11 | def from_json(recipe_item_json): 12 | """Create model from json.""" 13 | return RecipeItem( 14 | recipe_item_json.get('hash', None), 15 | recipe_item_json.get('uid', None) 16 | ) 17 | 18 | def __init__(self, hash, uid): 19 | """Initialize the model.""" 20 | self.hash = hash 21 | self.uid = uid 22 | 23 | async def link_to(self, *args): 24 | """Nothing to link to.""" 25 | pass 26 | -------------------------------------------------------------------------------- /pyprika-client/framework/models/status.py: -------------------------------------------------------------------------------- 1 | """User recipe book status model.""" 2 | from pyprika.framework.models.base_model import BaseModel 3 | 4 | 5 | class Status(BaseModel): 6 | """Model for status resource.""" 7 | 8 | __slots__ = ['recipes', 'pantry', 'meals', 'menus', 'groceries', 'bookmarks', 'menu_items', 9 | 'categories'] 10 | 11 | @staticmethod 12 | def from_json(status_json): 13 | """Create model from json.""" 14 | return Status( 15 | status_json.get('recipes', 0), 16 | status_json.get('pantry', 0), 17 | status_json.get('meals', 0), 18 | status_json.get('menu', 0), 19 | status_json.get('groceries', 0), 20 | status_json.get('bookmarks', 0), 21 | status_json.get('menuitems', 0), 22 | status_json.get('categories', 0) 23 | ) 24 | 25 | def __init__(self, recipes, pantry, meals, menus, groceries, bookmarks, menu_items, categories): 26 | """Initialize the model.""" 27 | self.recipes = recipes 28 | self.pantry = pantry 29 | self.meals = meals 30 | self.menus = menus 31 | self.groceries = groceries 32 | self.bookmarks = bookmarks 33 | self.menu_items = menu_items 34 | self.categories = categories 35 | 36 | async def link_to(self, *args): 37 | """Nothing to link to.""" 38 | pass 39 | -------------------------------------------------------------------------------- /pyprika-client/framework/specifications.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | 4 | class Specification: 5 | 6 | def __and__(self, other): 7 | return And(self, other) 8 | 9 | def __or__(self, other): 10 | return Or(self, other) 11 | 12 | def __xor__(self, other): 13 | return Xor(self, other) 14 | 15 | def __invert__(self): 16 | return Invert(self) 17 | 18 | def is_satisfied_by(self, candidate): 19 | raise NotImplementedError() 20 | 21 | def remainder_unsatisfied_by(self, candidate): 22 | if self.is_satisfied_by(candidate): 23 | return None 24 | else: 25 | return self 26 | 27 | 28 | class CompositeSpecification(Specification, ABC): 29 | pass 30 | 31 | 32 | class MultaryCompositeSpecification(CompositeSpecification, ABC): 33 | 34 | def __init__(self, *specifications): 35 | self.specifications = specifications 36 | 37 | 38 | class And(MultaryCompositeSpecification): 39 | 40 | def __and__(self, other): 41 | if isinstance(other, And): 42 | self.specifications += other.specifications 43 | else: 44 | self.specifications += (other,) 45 | return self 46 | 47 | def is_satisfied_by(self, candidate): 48 | satisfied = all([ 49 | specification.is_satisfied_by(candidate) 50 | for specification in self.specifications 51 | ]) 52 | return satisfied 53 | 54 | def remainder_unsatisfied_by(self, candidate): 55 | non_satisfied = [ 56 | specification 57 | for specification in self.specifications 58 | if not specification.is_satisfied_by(candidate) 59 | ] 60 | if not non_satisfied: 61 | return None 62 | if len(non_satisfied) == 1: 63 | return non_satisfied[0] 64 | if len(non_satisfied) == len(self.specifications): 65 | return self 66 | return And(*non_satisfied) 67 | 68 | 69 | class UnaryCompositeSpecification(CompositeSpecification, ABC): 70 | 71 | def __init__(self, specification): 72 | self.specification = specification 73 | 74 | 75 | class Invert(UnaryCompositeSpecification): 76 | 77 | def is_satisfied_by(self, candidate): 78 | return not self.specification.is_satisfied_by(candidate) 79 | 80 | 81 | class Or(MultaryCompositeSpecification): 82 | 83 | def __or__(self, other): 84 | if isinstance(other, Or): 85 | self.specifications += other.specifications 86 | else: 87 | self.specifications += (other,) 88 | return self 89 | 90 | def is_satisfied_by(self, candidate): 91 | satisfied = any([ 92 | specification.is_satisfied_by(candidate) 93 | for specification in self.specifications 94 | ]) 95 | return satisfied 96 | 97 | 98 | class BinaryCompositeSpecification(CompositeSpecification, ABC): 99 | 100 | def __init__(self, left, right): 101 | self.left = left 102 | self.right = right 103 | 104 | 105 | class Xor(BinaryCompositeSpecification): 106 | 107 | def is_satisfied_by(self, candidate): 108 | return ( 109 | self.left.is_satisfied_by(candidate) ^ 110 | self.right.is_satisfied_by(candidate) 111 | ) 112 | 113 | 114 | class NullaryCompositeSpecification(CompositeSpecification, ABC): 115 | pass 116 | 117 | 118 | class FalseSpecification(NullaryCompositeSpecification): 119 | 120 | def is_satisfied_by(self, candidate): 121 | return False 122 | 123 | 124 | class TrueSpecification(NullaryCompositeSpecification): 125 | 126 | def is_satisfied_by(self, candidate): 127 | return True 128 | -------------------------------------------------------------------------------- /pyprika-client/framework/work_unit_base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class AsyncWorkUnit(ABC): 5 | """Abstract base class for asynchronous unit of work.""" 6 | 7 | @abstractmethod 8 | async def perform_work(self, *args, **kwargs): 9 | """Perform work unit.""" 10 | 11 | 12 | class WorkUnit(ABC): 13 | """Abstract base class for unit of work.""" 14 | 15 | @abstractmethod 16 | def perform_work(self, *args, **kwargs): 17 | """Perform work unit.""" 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.5 2 | asyncio==3.4.3 3 | async-timeout==3.0.1 4 | attrs==19.3.0 5 | chardet==3.0.4 6 | idna==2.8 7 | multidict==4.6.1 8 | yarl==1.4.2 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | # This includes the license file(s) in the wheel. 3 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file 4 | license_files = LICENSE 5 | 6 | [bdist_wheel] 7 | # This flag says to generate wheels that support both Python 2 and Python 8 | # 3. If your code will not run unchanged on both Python 2 and 3, you will 9 | # need to generate separate wheels for each Python version that you 10 | # support. Removing this line (or setting universal to 0) will prevent 11 | # bdist_wheel from trying to make a universal wheel. For more see: 12 | # https://packaging.python.org/guides/distributing-packages-using-setuptools/#wheels 13 | universal=0 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('README.md') as readme_file: 4 | readme = readme_file.read() 5 | 6 | requirements = [ 7 | "aiohttp==3.8.5", 8 | "async-timeout==3.0.1", 9 | "attrs==19.3.0", 10 | "chardet==3.0.4", 11 | "idna==2.8", 12 | "multidict==4.6.1", 13 | "yarl==1.4.2", 14 | ] 15 | 16 | setup( 17 | name='pyprika-client', 18 | version='0.1.0', 19 | description="AsyncIO Library for Communicating with Paprika backend servers.", 20 | long_description=readme, 21 | long_description_content_type="text/markdown; charset=UTF-8", 22 | author="Teagan Glenn", 23 | author_email='that@teagantotally.rocks', 24 | url='https://github.com/constructorfleet/pyprika', 25 | packages=find_packages(), 26 | include_package_data=True, 27 | install_requires=requirements, 28 | license="MIT", 29 | zip_safe=False, 30 | keywords='Paprika, Cooking, Recipes', 31 | classifiers=[ 32 | 'Development Status :: 4 - Beta', 33 | 'Intended Audience :: Developers', 34 | 'Natural Language :: English', 35 | 'Programming Language :: Python :: 3', 36 | 'Programming Language :: Python :: 3.5', 37 | 'Programming Language :: Python :: 3.6', 38 | 'Programming Language :: Python :: 3.7', 39 | 'Programming Language :: Python :: 3.8', 40 | ], 41 | ) 42 | --------------------------------------------------------------------------------