├── .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 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
12 |
13 |
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 |
--------------------------------------------------------------------------------