Compare commits
No commits in common. "2.0" and "main" have entirely different histories.
67 changed files with 2496 additions and 1918 deletions
14
.github/workflows/main.yml
vendored
14
.github/workflows/main.yml
vendored
|
@ -1,14 +0,0 @@
|
|||
name: Docker Image CI
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the Docker image
|
||||
run: |
|
||||
echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker.io
|
||||
docker build . --file Dockerfile --tag docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA
|
||||
docker push docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA
|
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -27,7 +27,7 @@ var/
|
|||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
#*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
@ -62,12 +62,16 @@ db.sqlite
|
|||
db.json
|
||||
node_modules
|
||||
comments.xml
|
||||
stacosys/bin/
|
||||
stacosys/pyvenv.cfg
|
||||
stacosys/lib64
|
||||
.vscode/
|
||||
.pytest_cache/
|
||||
workspace.code-workspace
|
||||
*.sqlite
|
||||
config-server.ini
|
||||
config-dev.ini
|
||||
.idea/
|
||||
.python-version
|
||||
stacosys.sublime-project
|
||||
stacosys.sublime-workspace
|
||||
out/
|
||||
junit.xml
|
||||
coverage.xml
|
622
.pylintrc
Normal file
622
.pylintrc
Normal file
|
@ -0,0 +1,622 @@
|
|||
[MAIN]
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Load and enable all available extensions. Use --list-extensions to see a list
|
||||
# all available extensions.
|
||||
#enable-all-extensions=
|
||||
|
||||
# In error mode, messages with a category besides ERROR or FATAL are
|
||||
# suppressed, and no reports are done by default. Error mode is compatible with
|
||||
# disabling specific errors.
|
||||
#errors-only=
|
||||
|
||||
# Always return a 0 (non-error) status code, even if lint errors are found.
|
||||
# This is primarily useful in continuous integration scripts.
|
||||
#exit-zero=
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-allow-list=
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
|
||||
# for backward compatibility.)
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Return non-zero exit code if any of these messages/categories are detected,
|
||||
# even if score is above --fail-under value. Syntax same as enable. Messages
|
||||
# specified are enabled, while categories only check already-enabled messages.
|
||||
fail-on=
|
||||
|
||||
# Specify a score threshold under which the program will exit with error.
|
||||
fail-under=10
|
||||
|
||||
# Interpret the stdin as a python script, whose filename needs to be passed as
|
||||
# the module_or_package argument.
|
||||
#from-stdin=
|
||||
|
||||
# Files or directories to be skipped. They should be base names, not paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regular expressions patterns to the
|
||||
# ignore-list. The regex matches against paths and can be in Posix or Windows
|
||||
# format. Because '\' represents the directory delimiter on Windows systems, it
|
||||
# can't be used as an escape character.
|
||||
ignore-paths=
|
||||
|
||||
# Files or directories matching the regular expression patterns are skipped.
|
||||
# The regex matches against base names, not paths. The default value ignores
|
||||
# Emacs file locks
|
||||
ignore-patterns=^\.#
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis). It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use, and will cap the count on Windows to
|
||||
# avoid hangs.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Minimum Python version to use for version dependent checks. Will default to
|
||||
# the version used to run pylint.
|
||||
py-version=3.11
|
||||
|
||||
# Discover python modules and packages in the file system subtree.
|
||||
recursive=no
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
# In verbose mode, extra non-checker-related info will be displayed.
|
||||
#verbose=
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style. If left empty, argument names will be checked with the set
|
||||
# naming style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style. If left empty, attribute names will be checked with the set naming
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Bad variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be refused
|
||||
bad-names-rgxs=
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style. If left empty, class attribute names will be checked
|
||||
# with the set naming style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class constant names.
|
||||
class-const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct class constant names. Overrides class-
|
||||
# const-naming-style. If left empty, class constant names will be checked with
|
||||
# the set naming style.
|
||||
#class-const-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style. If left empty, class names will be checked with the set naming style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style. If left empty, constant names will be checked with the set naming
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style. If left empty, function names will be checked with the set
|
||||
# naming style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Good variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be accepted
|
||||
good-names-rgxs=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style. If left empty, inline iteration names will be checked
|
||||
# with the set naming style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style. If left empty, method names will be checked with the set naming style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style. If left empty, module names will be checked with the set naming style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct type variable names. If left empty, type
|
||||
# variable names will be checked with the set naming style.
|
||||
#typevar-rgx=
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style. If left empty, variable names will be checked with the set
|
||||
# naming style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# Warn about protected attribute access inside special methods
|
||||
check-protected-access-in-special-methods=no
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp,
|
||||
__post_init__
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# List of regular expressions of class ancestor names to ignore when counting
|
||||
# public methods (see R0903)
|
||||
exclude-too-few-public-methods=
|
||||
|
||||
# List of qualified class names to ignore when counting class parents (see
|
||||
# R0901)
|
||||
ignored-parents=
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement (see R0916).
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when caught.
|
||||
overgeneral-exceptions=builtins.BaseException,
|
||||
builtins.Exception
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# List of modules that can be imported at any level, not just the top level
|
||||
# one.
|
||||
allow-any-import-level=
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of external dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of all (i.e. internal and
|
||||
# external) dependencies to the given file (report RP0402 must not be
|
||||
# disabled).
|
||||
import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of internal dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Couples of modules and preferred modules, separated by a comma.
|
||||
preferred-modules=
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# The type of string formatting that logging methods do. `old` means using %
|
||||
# formatting, `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
|
||||
# UNDEFINED.
|
||||
confidence=HIGH,
|
||||
CONTROL_FLOW,
|
||||
INFERENCE,
|
||||
INFERENCE_FAILURE,
|
||||
UNDEFINED
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then re-enable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
missing-function-docstring,
|
||||
too-few-public-methods
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member
|
||||
|
||||
|
||||
[METHOD_ARGS]
|
||||
|
||||
# List of qualified names (i.e., library.method) which require a timeout
|
||||
# parameter e.g. 'requests.api.get,requests.api.post'
|
||||
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
# Regular expression of note tags to take in consideration.
|
||||
notes-rgx=
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit,argparse.parse_error
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a score less than or equal to 10. You
|
||||
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
|
||||
# 'convention', and 'info' which contain the number of messages in each
|
||||
# category, as well as 'statement' which is the total number of statements
|
||||
# analyzed. This score is used by the global evaluation report (RP0004).
|
||||
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
#output-format=
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Comments are removed from the similarity computation
|
||||
ignore-comments=yes
|
||||
|
||||
# Docstrings are removed from the similarity computation
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Imports are removed from the similarity computation
|
||||
ignore-imports=yes
|
||||
|
||||
# Signatures are removed from the similarity computation
|
||||
ignore-signatures=yes
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it work,
|
||||
# install the 'python-enchant' package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should be considered directives if they
|
||||
# appear at the beginning of a comment and should not be checked.
|
||||
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains the private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to the private dictionary (see the
|
||||
# --spelling-private-dict-file option) instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether inconsistent-quotes generates a warning when the
|
||||
# character used as a quote delimiter is used inconsistently within a module.
|
||||
check-quote-consistency=no
|
||||
|
||||
# This flag controls whether the implicit-str-concat should generate a warning
|
||||
# on implicit string concatenation in sequences defined over several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of symbolic message names to ignore for Mixin members.
|
||||
ignored-checks-for-mixins=no-member,
|
||||
not-async-context-manager,
|
||||
not-context-manager,
|
||||
attribute-defined-outside-init
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
# Regex pattern to define which classes are considered mixins.
|
||||
mixin-class-rgx=.*[Mm]ixin
|
||||
|
||||
# List of decorators that change the signature of a decorated function.
|
||||
signature-mutators=
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of names allowed to shadow builtins
|
||||
allowed-redefined-builtins=
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
17
Dockerfile
17
Dockerfile
|
@ -1,16 +1,23 @@
|
|||
FROM python:3.9-alpine
|
||||
FROM python:3.13.1-alpine3.20
|
||||
|
||||
ARG STACOSYS_VERSION=2.0
|
||||
ARG STACOSYS_VERSION=3.4
|
||||
ARG STACOSYS_FILENAME=stacosys-${STACOSYS_VERSION}-py3-none-any.whl
|
||||
|
||||
RUN apk update && apk add bash && apk add wget && rm -rf /var/cache/apk/*
|
||||
RUN apk update && apk add bash && apk add wget
|
||||
|
||||
# Timezone
|
||||
RUN apk add tzdata
|
||||
RUN cp /usr/share/zoneinfo/Europe/Paris /etc/localtime
|
||||
RUN echo "Europe/Paris" > /etc/timezone
|
||||
|
||||
# Clean apk cache
|
||||
RUN rm -rf /var/cache/apk/*
|
||||
|
||||
COPY docker/docker-init.sh /usr/local/bin/
|
||||
RUN chmod +x usr/local/bin/docker-init.sh
|
||||
|
||||
RUN cd /
|
||||
#COPY ${STACOSYS_FILENAME} /
|
||||
RUN wget https://github.com/kianby/stacosys/releases/download/${STACOSYS_VERSION}/${STACOSYS_FILENAME}
|
||||
COPY dist/${STACOSYS_FILENAME} /
|
||||
RUN python3 -m pip install ${STACOSYS_FILENAME} --target /stacosys
|
||||
RUN rm -f ${STACOSYS_FILENAME}
|
||||
|
||||
|
|
46
Makefile
Normal file
46
Makefile
Normal file
|
@ -0,0 +1,46 @@
|
|||
ifeq (run,$(firstword $(MAKECMDGOALS)))
|
||||
# use the rest as arguments for "run"
|
||||
RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
# ...and turn them into do-nothing targets
|
||||
$(eval $(RUN_ARGS):;@:)
|
||||
endif
|
||||
|
||||
.PHONY: all build run test
|
||||
|
||||
# code quality
|
||||
all: black typehint lint
|
||||
|
||||
black:
|
||||
uv run isort --multi-line 3 --profile black src/ tests/
|
||||
uv run black --target-version py311 src/ tests/
|
||||
|
||||
typehint:
|
||||
uv run mypy --ignore-missing-imports src/ tests/
|
||||
|
||||
lint:
|
||||
uv run pylint src/
|
||||
|
||||
# check
|
||||
check: all
|
||||
|
||||
# test
|
||||
test:
|
||||
PYTHONPATH=src/ uv run coverage run -m --source=stacosys pytest --junitxml=junit.xml tests
|
||||
uv run genbadge tests -i junit.xml
|
||||
uv run coverage xml
|
||||
uv run genbadge coverage -i coverage.xml
|
||||
|
||||
# build
|
||||
build:
|
||||
# https://stackoverflow.com/questions/24347450/how-do-you-add-additional-files-to-a-wheel
|
||||
rm -rf build/* dist/* *.egg-info
|
||||
uv sync
|
||||
uv build --wheel --out-dir dist
|
||||
docker build -t source.madyanne.fr/yax/stacosys .
|
||||
|
||||
publish:
|
||||
docker push source.madyanne.fr/yax/stacosys
|
||||
|
||||
# run
|
||||
run:
|
||||
PYTHONPATH=src/ uv run python src/stacosys/run.py $(RUN_ARGS)
|
37
README.md
37
README.md
|
@ -1,8 +1,11 @@
|
|||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://www.python.org/) [](https://github.com/psf/black) [](https://flask.palletsprojects.com)
|
||||
|
||||
[]() []()
|
||||
|
||||
## Stacosys
|
||||
|
||||
Stacosys (aka STAtic blog COmment SYStem) is a fork of [Pecosys](http://github.com/kianby/pecosys) trying to fix Pecosys design drawbacks and to provide an humble alternative to comment hosting services like Disqus. Stacosys protects your readers's privacy.
|
||||
|
||||
Stacosys works with any static blog or even a simple HTML page. It uses e-mails to communicate with the blog administrator. It doesn't sound *hype* but I'm an old-school guy. E-mails are reliable and an universal way to communicate. You can answer from any device using an e-mail client.
|
||||
Stacosys (aka STAtic blog COmment SYStem) is a fork of Pecosys trying to fix design drawbacks and provide a basic alternative to comment hosting services like Disqus. Stacosys works with any static blog or even a simple HTML page.
|
||||
|
||||
### Features overview
|
||||
|
||||
|
@ -11,25 +14,24 @@ Stacosys main feature is comment management.
|
|||
Here is the workflow:
|
||||
|
||||
- Readers submit comments via a comment form embedded in blog pages
|
||||
- Blog administrator receives an email notification from Stacosys when a
|
||||
- Blog administrator receives an e-mail notification from Stacosys when a
|
||||
comment is submitted
|
||||
- Blog administrator can approve or drop the comment by replying to e-mail
|
||||
- Blog administrator can approve or drop the comment through a simple web admin interface
|
||||
- Stacosys stores approved comment in its database.
|
||||
|
||||
Privacy concerns: only surname, gravatar id and comment itself are stored in DB. E-mail is requested in submission form (but optional) to resolve gravatar id and it it not sent to stacosys.
|
||||
Privacy concerns: only surname, gravatar id and comment itself are stored in DB. E-mail is optionally requested in submission form to resolve gravatar id but never sent to Stacosys.
|
||||
|
||||
Stacosys is localized (english and french).
|
||||
Stacosys is more or less localized (english and french).
|
||||
|
||||
### Technically speaking, how does it work?
|
||||
|
||||
Stacosys can be hosted on the same server or on a different server than the blog. Stacosys offers a REST API to retrieve and post comments. Static blog is HTML-based and a piece of JavaScript code interacts with Stacosys using HTTP requests. Each page has a unique id and a simple request allows to retrieve comments for a given page. Similarly a form request allows to post a comment which is relayed to the administrator by e-mail. For this purpose a dedicated email is assigned to Stacosys.
|
||||
|
||||
Stacosys offers a REST API to retrieve and post comments. Static blog is HTML-based and a piece of JavaScript code interacts with Stacosys using HTTP requests. Each page has a unique id and a request allows retrieving comments for a given page. Similarly, a form request allows to post a comment which is relayed to the administrator by e-mail. For this purpose an SMTP configuration is needed.
|
||||
|
||||
### Little FAQ
|
||||
|
||||
*How do you block spammers?*
|
||||
|
||||
- Current comment form is basic: no captcha support but a honey pot.
|
||||
- Current comment form is basic: no captcha support but protected by a honeypot.
|
||||
|
||||
*Which database is used?*
|
||||
|
||||
|
@ -37,15 +39,22 @@ Stacosys can be hosted on the same server or on a different server than the blog
|
|||
|
||||
*Which technologies are used?*
|
||||
|
||||
- [Python 3.9](https://www.python.org)
|
||||
- [Python](https://www.python.org)
|
||||
- [Flask](http://flask.pocoo.org)
|
||||
- [Peewee ORM](http://docs.peewee-orm.com)
|
||||
- [Markdown](http://daringfireball.net/projects/markdown)
|
||||
|
||||
### Installation
|
||||
|
||||
Build is based on [Poetry](https://python-poetry.org/) but you can also use [published releases on GitHub](https://github.com/kianby/stacosys/releases) or the [Docker image](https://hub.docker.com/repository/docker/kianby/stacosys).
|
||||
Build and Dependency management relies on [uv](https://docs.astral.sh/uv/)
|
||||
|
||||
Run tests and coverage
|
||||
|
||||
make test
|
||||
|
||||
Build docker image
|
||||
|
||||
make build
|
||||
|
||||
### Improvements
|
||||
|
||||
Stacosys fits my needs and it manages comments on [my blog](https://blogduyax.madyanne.fr) for a while. I don't have any plan to make big changes, it's more a python playground for me. So I strongly encourage you to fork the project and enhance the project if you need more features.
|
||||
Stacosys fits my needs, and it manages comments on [my blog](https://blogduyax.madyanne.fr) for a while. I don't have any plan to make big changes, it's more a python playground for me. So I strongly encourage you to fork and enhance the project if you need additional features.
|
||||
|
|
27
config.ini
27
config.ini
|
@ -2,34 +2,29 @@
|
|||
; Default configuration
|
||||
[main]
|
||||
lang = fr
|
||||
db_sqlite_file = db.sqlite
|
||||
newcomment_polling = 60
|
||||
db = sqlite://db.sqlite
|
||||
|
||||
[site]
|
||||
name = "My blog"
|
||||
url = http://blog.mydomain.com
|
||||
proto = https
|
||||
url = https://blog.mydomain.com
|
||||
admin_email = admin@mydomain.com
|
||||
redirect = /redirect
|
||||
|
||||
[http]
|
||||
host = 127.0.0.1
|
||||
port = 8100
|
||||
|
||||
[rss]
|
||||
proto = https
|
||||
file = comments.xml
|
||||
|
||||
[imap]
|
||||
polling = 120
|
||||
host = mail.gandi.net
|
||||
ssl = false
|
||||
port = 993
|
||||
[smtp]
|
||||
host = smtp.mail.com
|
||||
port = 465
|
||||
login = blog@mydomain.com
|
||||
password = MYPASSWORD
|
||||
|
||||
[smtp]
|
||||
host = mail.gandi.net
|
||||
starttls = true
|
||||
ssl = false
|
||||
port = 587
|
||||
login = blog@mydomain.com
|
||||
password = MYPASSWORD
|
||||
[web]
|
||||
username = admin
|
||||
; SHA-256 hashed password (https://coding.tools/sha256)
|
||||
password = 8C6976E5B5410415BDE908BD4DEE15DFB167A9C873FC4BB8A81F6F2AB448A918
|
||||
|
|
1
coverage-badge.svg
Normal file
1
coverage-badge.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 86.82%"><title>coverage: 86.82%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#97ca00"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="865" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">86.82%</text><text x="865" y="140" transform="scale(.1)" fill="#fff" textLength="430">86.82%</text></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
24
dbmigration/create_empty_db.py
Normal file
24
dbmigration/create_empty_db.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import sqlite3
|
||||
|
||||
connection = sqlite3.connect("db.sqlite")
|
||||
cursor = connection.cursor()
|
||||
|
||||
script = """
|
||||
CREATE TABLE comment (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
url VARCHAR(255) NOT NULL,
|
||||
notified DATETIME,
|
||||
created DATETIME NOT NULL,
|
||||
published DATETIME,
|
||||
author_name VARCHAR(255) NOT NULL,
|
||||
author_site VARCHAR(255) NOT NULL,
|
||||
author_gravatar varchar(255),
|
||||
content TEXT NOT NULL
|
||||
, ulid INTEGER);
|
||||
"""
|
||||
|
||||
cursor.executescript(script)
|
||||
connection.close()
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import sqlite3
|
||||
|
@ -6,27 +6,30 @@ import sqlite3
|
|||
connection = sqlite3.connect("db.sqlite")
|
||||
cursor = connection.cursor()
|
||||
|
||||
# What script performs:
|
||||
# - first, remove site table: crash here if table doesn't exist (compatibility test without effort)
|
||||
# - remove site_id colum from comment table
|
||||
# What script performs:
|
||||
# - first, remove site table: crash here if table doesn't exist
|
||||
# (compatibility test without effort)
|
||||
# - remove site_id column from comment table
|
||||
script = """
|
||||
PRAGMA foreign_keys = OFF;
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE site;
|
||||
ALTER TABLE comment RENAME TO _comment_old;
|
||||
CREATE TABLE comment (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
url VARCHAR(255) NOT NULL,
|
||||
notified DATETIME,
|
||||
created DATETIME NOT NULL,
|
||||
published DATETIME,
|
||||
author_name VARCHAR(255) NOT NULL,
|
||||
author_site VARCHAR(255) NOT NULL,
|
||||
author_gravatar varchar(255),
|
||||
content TEXT NOT NULL
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
url VARCHAR(255) NOT NULL,
|
||||
notified DATETIME,
|
||||
created DATETIME NOT NULL,
|
||||
published DATETIME,
|
||||
author_name VARCHAR(255) NOT NULL,
|
||||
author_site VARCHAR(255) NOT NULL,
|
||||
author_gravatar varchar(255),
|
||||
content TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO comment (id, url, notified, created, published, author_name, author_site, author_gravatar, content)
|
||||
SELECT id, url, notified, created, published, author_name, author_site, author_gravatar, content
|
||||
INSERT INTO comment (id, url, notified, created, published,
|
||||
author_name, author_site, author_gravatar, content)
|
||||
SELECT id, url, notified, created, published,
|
||||
author_name, author_site, author_gravatar, content
|
||||
FROM _comment_old;
|
||||
DROP TABLE _comment_old;
|
||||
COMMIT;
|
||||
|
@ -34,4 +37,4 @@ PRAGMA foreign_keys = ON;
|
|||
"""
|
||||
|
||||
cursor.executescript(script)
|
||||
connection.close()
|
||||
connection.close()
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
cd /stacosys
|
||||
# workaround for startup
|
||||
cp -f stacosys/run.py .
|
||||
python3 run.py /config/config.ini
|
||||
|
||||
# catch for debug
|
||||
#tail -f /dev/null
|
665
poetry.lock
generated
665
poetry.lock
generated
|
@ -1,665 +0,0 @@
|
|||
[[package]]
|
||||
name = "appdirs"
|
||||
version = "1.4.4"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "apscheduler"
|
||||
version = "3.7.0"
|
||||
description = "In-process task scheduler with Cron-like capabilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
|
||||
[package.dependencies]
|
||||
pytz = "*"
|
||||
six = ">=1.4.0"
|
||||
tzlocal = ">=2.0,<3.0"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["trollius"]
|
||||
doc = ["sphinx", "sphinx-rtd-theme"]
|
||||
gevent = ["gevent"]
|
||||
mongodb = ["pymongo (>=3.0)"]
|
||||
redis = ["redis (>=3.0)"]
|
||||
rethinkdb = ["rethinkdb (>=2.4.0)"]
|
||||
sqlalchemy = ["sqlalchemy (>=0.8)"]
|
||||
testing = ["pytest (<6)", "pytest-cov", "pytest-tornado5", "mock", "pytest-asyncio (<0.6)", "pytest-asyncio"]
|
||||
tornado = ["tornado (>=4.3)"]
|
||||
twisted = ["twisted"]
|
||||
zookeeper = ["kazoo"]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "20.8b1"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
appdirs = "*"
|
||||
click = ">=7.1.2"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
pathspec = ">=0.6,<1"
|
||||
regex = ">=2020.1.8"
|
||||
toml = ">=0.10.1"
|
||||
typed-ast = ">=1.4.0"
|
||||
typing-extensions = ">=3.7.4"
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2021.5.30"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "2.0.3"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5.0"
|
||||
|
||||
[package.extras]
|
||||
unicode_backport = ["unicodedata2"]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.0.1"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.4"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "flake8"
|
||||
version = "3.9.2"
|
||||
description = "the modular source code checker: pep8 pyflakes and co"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
mccabe = ">=0.6.0,<0.7.0"
|
||||
pycodestyle = ">=2.7.0,<2.8.0"
|
||||
pyflakes = ">=2.3.0,<2.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-black"
|
||||
version = "0.2.3"
|
||||
description = "flake8 plugin to call black as a code style validator"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
black = "*"
|
||||
flake8 = ">=3.0.0"
|
||||
toml = "*"
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "2.0.1"
|
||||
description = "A simple framework for building complex web applications."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=7.1.2"
|
||||
itsdangerous = ">=2.0"
|
||||
Jinja2 = ">=3.0"
|
||||
Werkzeug = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
async = ["asgiref (>=3.2)"]
|
||||
dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-apscheduler"
|
||||
version = "1.12.2"
|
||||
description = "Adds APScheduler support to Flask"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
apscheduler = ">=3.2.0,<4.0.0"
|
||||
flask = ">=0.10.1"
|
||||
python-dateutil = ">=2.4.2"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.2"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.0.1"
|
||||
description = "Safely pass data to untrusted environments and back."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.0.1"
|
||||
description = "A very fast and expressive template engine."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "3.3.4"
|
||||
description = "Python implementation of Markdown."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
testing = ["coverage", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.0.1"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "mccabe"
|
||||
version = "0.6.1"
|
||||
description = "McCabe checker, plugin for flake8"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "0.790"
|
||||
description = "Optional static typing for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.dependencies]
|
||||
mypy-extensions = ">=0.4.3,<0.5.0"
|
||||
typed-ast = ">=1.4.0,<1.5.0"
|
||||
typing-extensions = ">=3.7.4"
|
||||
|
||||
[package.extras]
|
||||
dmypy = ["psutil (>=4.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "0.4.3"
|
||||
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.8.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "peewee"
|
||||
version = "3.14.4"
|
||||
description = "a little orm"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "profig"
|
||||
version = "0.5.1"
|
||||
description = "A configuration library."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pycodestyle"
|
||||
version = "2.7.0"
|
||||
description = "Python style guide checker"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyflakes"
|
||||
version = "2.3.1"
|
||||
description = "passive checker of Python programs"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyrss2gen"
|
||||
version = "1.1"
|
||||
description = "Generate RSS2 using a Python data structure"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.8.2"
|
||||
description = "Extensions to the standard Python datetime module"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2021.1"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2021.7.6"
|
||||
description = "Alternative regular expression module, to replace re."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.26.0"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
|
||||
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
|
||||
|
||||
[[package]]
|
||||
name = "rope"
|
||||
version = "0.16.0"
|
||||
description = "a python refactoring library..."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.10.2"
|
||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "typed-ast"
|
||||
version = "1.4.3"
|
||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "3.10.0.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.5+"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "2.1"
|
||||
description = "tzinfo object for the local timezone"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
pytz = "*"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.6"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotlipy (>=0.6.0)"]
|
||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "2.0.1"
|
||||
description = "The comprehensive WSGI web application library."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
watchdog = ["watchdog"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "8190054ee0a6bf5fccefd841ac71fa6851a4e7d057c5af6fb83ea2364711cb78"
|
||||
|
||||
[metadata.files]
|
||||
appdirs = [
|
||||
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
|
||||
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
|
||||
]
|
||||
apscheduler = [
|
||||
{file = "APScheduler-3.7.0-py2.py3-none-any.whl", hash = "sha256:c06cc796d5bb9eb3c4f77727f6223476eb67749e7eea074d1587550702a7fbe3"},
|
||||
{file = "APScheduler-3.7.0.tar.gz", hash = "sha256:1cab7f2521e107d07127b042155b632b7a1cd5e02c34be5a28ff62f77c900c6a"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
|
||||
{file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
|
||||
]
|
||||
charset-normalizer = [
|
||||
{file = "charset-normalizer-2.0.3.tar.gz", hash = "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"},
|
||||
{file = "charset_normalizer-2.0.3-py3-none-any.whl", hash = "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
|
||||
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||
]
|
||||
flake8 = [
|
||||
{file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
|
||||
{file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
|
||||
]
|
||||
flake8-black = [
|
||||
{file = "flake8-black-0.2.3.tar.gz", hash = "sha256:c199844bc1b559d91195ebe8620216f21ed67f2cc1ff6884294c91a0d2492684"},
|
||||
{file = "flake8_black-0.2.3-py3-none-any.whl", hash = "sha256:cc080ba5b3773b69ba102b6617a00cc4ecbad8914109690cfda4d565ea435d96"},
|
||||
]
|
||||
flask = [
|
||||
{file = "Flask-2.0.1-py3-none-any.whl", hash = "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9"},
|
||||
{file = "Flask-2.0.1.tar.gz", hash = "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55"},
|
||||
]
|
||||
flask-apscheduler = [
|
||||
{file = "Flask-APScheduler-1.12.2.tar.gz", hash = "sha256:b9fe174b90d201d8beeba5522b023208f7bb6e2583fc02fea4be4bce5ee8f9e5"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
|
||||
{file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
|
||||
]
|
||||
itsdangerous = [
|
||||
{file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"},
|
||||
{file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"},
|
||||
]
|
||||
jinja2 = [
|
||||
{file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"},
|
||||
{file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"},
|
||||
]
|
||||
markdown = [
|
||||
{file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"},
|
||||
{file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"},
|
||||
]
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
|
||||
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
|
||||
]
|
||||
mccabe = [
|
||||
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
||||
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
|
||||
]
|
||||
mypy = [
|
||||
{file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"},
|
||||
{file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"},
|
||||
{file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"},
|
||||
{file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"},
|
||||
{file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"},
|
||||
{file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"},
|
||||
{file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"},
|
||||
{file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"},
|
||||
{file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"},
|
||||
{file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"},
|
||||
{file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"},
|
||||
{file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"},
|
||||
{file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"},
|
||||
{file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"},
|
||||
]
|
||||
mypy-extensions = [
|
||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
|
||||
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
|
||||
]
|
||||
peewee = [
|
||||
{file = "peewee-3.14.4.tar.gz", hash = "sha256:9e356b327c2eaec6dd42ecea6f4ddded025793dba906a3d065a0452e726c51a2"},
|
||||
]
|
||||
profig = [
|
||||
{file = "profig-0.5.1.tar.gz", hash = "sha256:cb9c094325a93505fc6325d13f3e679b281093223f143a96a6df8ad9c2bfc9a6"},
|
||||
]
|
||||
pycodestyle = [
|
||||
{file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
|
||||
{file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
|
||||
]
|
||||
pyflakes = [
|
||||
{file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
|
||||
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
|
||||
]
|
||||
pyrss2gen = [
|
||||
{file = "PyRSS2Gen-1.1.tar.gz", hash = "sha256:7960aed7e998d2482bf58716c316509786f596426f879b05f8d84e98b82c6ee7"},
|
||||
]
|
||||
python-dateutil = [
|
||||
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
|
||||
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
|
||||
]
|
||||
pytz = [
|
||||
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
|
||||
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
|
||||
]
|
||||
regex = [
|
||||
{file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"},
|
||||
{file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"},
|
||||
{file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"},
|
||||
{file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"},
|
||||
{file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"},
|
||||
{file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"},
|
||||
{file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"},
|
||||
{file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"},
|
||||
{file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"},
|
||||
{file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"},
|
||||
{file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"},
|
||||
{file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"},
|
||||
{file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"},
|
||||
{file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"},
|
||||
{file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"},
|
||||
{file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"},
|
||||
{file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"},
|
||||
{file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"},
|
||||
{file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"},
|
||||
{file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"},
|
||||
{file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"},
|
||||
{file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"},
|
||||
{file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"},
|
||||
{file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"},
|
||||
{file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"},
|
||||
{file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"},
|
||||
{file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"},
|
||||
{file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"},
|
||||
{file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"},
|
||||
{file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"},
|
||||
{file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"},
|
||||
{file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"},
|
||||
{file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"},
|
||||
{file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"},
|
||||
{file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"},
|
||||
{file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"},
|
||||
{file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"},
|
||||
{file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"},
|
||||
{file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"},
|
||||
{file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"},
|
||||
{file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
|
||||
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
|
||||
]
|
||||
rope = [
|
||||
{file = "rope-0.16.0-py2-none-any.whl", hash = "sha256:ae1fa2fd56f64f4cc9be46493ce54bed0dd12dee03980c61a4393d89d84029ad"},
|
||||
{file = "rope-0.16.0-py3-none-any.whl", hash = "sha256:52423a7eebb5306a6d63bdc91a7c657db51ac9babfb8341c9a1440831ecf3203"},
|
||||
{file = "rope-0.16.0.tar.gz", hash = "sha256:d2830142c2e046f5fc26a022fe680675b6f48f81c7fc1f03a950706e746e9dfe"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||
]
|
||||
typed-ast = [
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
|
||||
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
|
||||
{file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
|
||||
{file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
|
||||
]
|
||||
tzlocal = [
|
||||
{file = "tzlocal-2.1-py2.py3-none-any.whl", hash = "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"},
|
||||
{file = "tzlocal-2.1.tar.gz", hash = "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"},
|
||||
{file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"},
|
||||
]
|
||||
werkzeug = [
|
||||
{file = "Werkzeug-2.0.1-py3-none-any.whl", hash = "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8"},
|
||||
{file = "Werkzeug-2.0.1.tar.gz", hash = "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42"},
|
||||
]
|
|
@ -1,28 +1,43 @@
|
|||
[tool.poetry]
|
||||
[project]
|
||||
name = "stacosys"
|
||||
version = "2.0b4"
|
||||
version = "3.4"
|
||||
description = "STAtic COmmenting SYStem"
|
||||
authors = ["Yax"]
|
||||
readme = "README.md"
|
||||
include = ["run.py"]
|
||||
authors = [
|
||||
{ name = "Yax" }
|
||||
]
|
||||
requires-python = ">=3.13.1"
|
||||
dependencies = [
|
||||
"background>=0.2.1",
|
||||
"defusedxml>=0.7.1",
|
||||
"flask>=3.1.0",
|
||||
"genbadge>=1.1.2",
|
||||
"markdown>=3.7",
|
||||
"pydal>=20241204.1",
|
||||
"pyrss2gen>=1.1",
|
||||
"requests>=2.32.3",
|
||||
"types-markdown>=3.7.0.20241204",
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
apscheduler = "^3.6.3"
|
||||
pyrss2gen = "^1.1"
|
||||
profig = "^0.5.1"
|
||||
markdown = "^3.1.1"
|
||||
flask_apscheduler = "^1.11.0"
|
||||
Flask = "^2.0.1"
|
||||
peewee = "^3.14.0"
|
||||
requests = "^2.25.1"
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"coveralls>=4.0.1",
|
||||
"mypy>=1.13.0",
|
||||
"pylint>=3.3.2",
|
||||
"pytest-cov>=6.0.0",
|
||||
"pytest>=8.3.4",
|
||||
"black>=24.10.0",
|
||||
]
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
rope = "^0.16.0"
|
||||
mypy = "^0.790"
|
||||
flake8-black = "^0.2.1"
|
||||
black = "^20.8b1"
|
||||
[tool.setuptools]
|
||||
package-dir = { "" = "src" } # Specify the root directory for packages
|
||||
packages = ["stacosys", "stacosys.db", "stacosys.i18n", "stacosys.interface", "stacosys.interface.web", "stacosys.interface.templates", "stacosys.model", "stacosys.service"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
# Include `.properties` and `.html` files in the specified directories
|
||||
"stacosys.i18n" = ["*.properties"]
|
||||
"stacosys.interface.templates" = ["*.html"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
|
124
run.py
124
run.py
|
@ -1,124 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import logging
|
||||
import hashlib
|
||||
|
||||
from stacosys.conf.config import Config, ConfigParameter
|
||||
from stacosys.db import database
|
||||
from stacosys.core.rss import Rss
|
||||
from stacosys.core.mailer import Mailer
|
||||
from stacosys.interface import app
|
||||
from stacosys.interface import api
|
||||
from stacosys.interface import form
|
||||
from stacosys.interface import scheduler
|
||||
|
||||
|
||||
# configure logging
|
||||
def configure_logging(level):
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(level)
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(level)
|
||||
# create formatter
|
||||
formatter = logging.Formatter("[%(asctime)s] %(name)s %(levelname)s %(message)s")
|
||||
# add formatter to ch
|
||||
ch.setFormatter(formatter)
|
||||
# add ch to logger
|
||||
root_logger.addHandler(ch)
|
||||
|
||||
|
||||
def stacosys_server(config_pathname):
|
||||
|
||||
# configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
configure_logging(logging.INFO)
|
||||
logging.getLogger("werkzeug").level = logging.WARNING
|
||||
logging.getLogger("apscheduler.executors").level = logging.WARNING
|
||||
|
||||
# check config file exists
|
||||
if not os.path.isfile(config_pathname):
|
||||
logger.error(f"Configuration file '{config_pathname}' not found.")
|
||||
sys.exit(1)
|
||||
|
||||
# initialize config
|
||||
conf = Config.load(config_pathname)
|
||||
logger.info(conf.__repr__())
|
||||
|
||||
# check database file exists (prevents from creating a fresh db)
|
||||
db_pathname = conf.get(ConfigParameter.DB_SQLITE_FILE)
|
||||
if not os.path.isfile(db_pathname):
|
||||
logger.error(f"Database file '{db_pathname}' not found.")
|
||||
sys.exit(1)
|
||||
|
||||
# initialize database
|
||||
db = database.Database()
|
||||
db.setup(db_pathname)
|
||||
|
||||
logger.info("Start Stacosys application")
|
||||
|
||||
# generate RSS for all sites
|
||||
rss = Rss(
|
||||
conf.get(ConfigParameter.LANG),
|
||||
conf.get(ConfigParameter.RSS_FILE),
|
||||
conf.get(ConfigParameter.RSS_PROTO),
|
||||
conf.get(ConfigParameter.SITE_NAME),
|
||||
conf.get(ConfigParameter.SITE_URL),
|
||||
)
|
||||
rss.generate()
|
||||
|
||||
# configure mailer
|
||||
mailer = Mailer(
|
||||
conf.get(ConfigParameter.IMAP_HOST),
|
||||
conf.get_int(ConfigParameter.IMAP_PORT),
|
||||
conf.get_bool(ConfigParameter.IMAP_SSL),
|
||||
conf.get(ConfigParameter.IMAP_LOGIN),
|
||||
conf.get(ConfigParameter.IMAP_PASSWORD),
|
||||
conf.get(ConfigParameter.SMTP_HOST),
|
||||
conf.get_int(ConfigParameter.SMTP_PORT),
|
||||
conf.get_bool(ConfigParameter.SMTP_STARTTLS),
|
||||
conf.get_bool(ConfigParameter.SMTP_SSL),
|
||||
conf.get(ConfigParameter.SMTP_LOGIN),
|
||||
conf.get(ConfigParameter.SMTP_PASSWORD),
|
||||
conf.get(ConfigParameter.SITE_ADMIN_EMAIL)
|
||||
)
|
||||
|
||||
# configure mailer logger
|
||||
mail_handler = mailer.get_error_handler()
|
||||
logger.addHandler(mail_handler)
|
||||
app.logger.addHandler(mail_handler)
|
||||
|
||||
# configure scheduler
|
||||
conf.set(ConfigParameter.SITE_TOKEN, hashlib.sha1(conf.get(ConfigParameter.SITE_NAME)).hexdigest())
|
||||
scheduler.configure(
|
||||
conf.get_int(ConfigParameter.IMAP_POLLING),
|
||||
conf.get_int(ConfigParameter.COMMENT_POLLING),
|
||||
conf.get(ConfigParameter.LANG),
|
||||
conf.get(ConfigParameter.SITE_NAME),
|
||||
conf.get(ConfigParameter.SITE_TOKEN),
|
||||
conf.get(ConfigParameter.SITE_ADMIN_EMAIL),
|
||||
mailer,
|
||||
rss,
|
||||
)
|
||||
|
||||
# inject config parameters into flask
|
||||
app.config.update(SITE_TOKEN=conf.get(ConfigParameter.SITE_TOKEN))
|
||||
logger.info(f"start interfaces {api} {form}")
|
||||
|
||||
# start Flask
|
||||
app.run(
|
||||
host=conf.get(ConfigParameter.HTTP_HOST),
|
||||
port=conf.get(ConfigParameter.HTTP_PORT),
|
||||
debug=False,
|
||||
use_reloader=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("config", help="config path name")
|
||||
args = parser.parse_args()
|
||||
stacosys_server(args.config)
|
3
run.sh
3
run.sh
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
python3 run.py "$@"
|
||||
|
29
src/stacosys/db/__init__.py
Normal file
29
src/stacosys/db/__init__.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydal import DAL, Field
|
||||
|
||||
|
||||
class Database:
|
||||
db_dal = DAL()
|
||||
|
||||
def configure(self, db_uri):
|
||||
self.db_dal = DAL(db_uri, migrate=db_uri.startswith("sqlite:memory"))
|
||||
self.db_dal.define_table(
|
||||
"comment",
|
||||
Field("url"),
|
||||
Field("created", type="datetime"),
|
||||
Field("notified", type="datetime"),
|
||||
Field("published", type="datetime"),
|
||||
Field("author_name"),
|
||||
Field("author_site"),
|
||||
Field("author_gravatar"),
|
||||
Field("content", type="text"),
|
||||
)
|
||||
|
||||
def get(self):
|
||||
return self.db_dal
|
||||
|
||||
|
||||
database = Database()
|
||||
db = database.get
|
80
src/stacosys/db/dao.py
Normal file
80
src/stacosys/db/dao.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
# pylint: disable=singleton-comparison
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from stacosys.db import db
|
||||
from stacosys.model.comment import Comment
|
||||
|
||||
|
||||
def find_comment_by_id(comment_id):
|
||||
return db().comment(comment_id)
|
||||
|
||||
|
||||
def notify_comment(comment: Comment):
|
||||
db()(db().comment.id == comment.id).update(notified=datetime.now())
|
||||
db().commit()
|
||||
|
||||
|
||||
def publish_comment(comment: Comment):
|
||||
db()(db().comment.id == comment.id).update(published=datetime.now())
|
||||
db().commit()
|
||||
|
||||
|
||||
def delete_comment(comment: Comment):
|
||||
db()(db().comment.id == comment.id).delete()
|
||||
db().commit()
|
||||
|
||||
|
||||
def find_not_notified_comments():
|
||||
return db()(db().comment.notified == None).select()
|
||||
|
||||
|
||||
def find_not_published_comments():
|
||||
return db()(db().comment.published == None).select()
|
||||
|
||||
|
||||
def find_published_comments_by_url(url):
|
||||
return db()((db().comment.url == url) & (db().comment.published != None)).select(
|
||||
orderby=db().comment.published
|
||||
)
|
||||
|
||||
|
||||
def count_published_comments(url):
|
||||
return (
|
||||
db()((db().comment.url == url) & (db().comment.published != None)).count()
|
||||
if url
|
||||
else db()(db().comment.published != None).count()
|
||||
)
|
||||
|
||||
|
||||
def find_recent_published_comments():
|
||||
return db()(db().comment.published != None).select(
|
||||
orderby=~db().comment.published, limitby=(0, 10)
|
||||
)
|
||||
|
||||
|
||||
def create_comment(url, author_name, author_site, author_gravatar, message):
|
||||
row = db().comment.insert(
|
||||
url=url,
|
||||
author_name=author_name,
|
||||
author_site=author_site,
|
||||
author_gravatar=author_gravatar,
|
||||
content=message,
|
||||
created=datetime.now(),
|
||||
notified=None,
|
||||
published=None,
|
||||
)
|
||||
db().commit()
|
||||
return Comment(
|
||||
id=row.id,
|
||||
url=row.url,
|
||||
author_name=row.author_name,
|
||||
author_site=row.author_site,
|
||||
author_gravatar=row.author_gravatar,
|
||||
content=row.content,
|
||||
created=row.created,
|
||||
notified=row.notified,
|
||||
published=row.published,
|
||||
)
|
23
src/stacosys/i18n/messages.py
Normal file
23
src/stacosys/i18n/messages.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
import configparser
|
||||
import importlib.resources
|
||||
import os
|
||||
|
||||
|
||||
class Messages:
|
||||
def __init__(self):
|
||||
self.property_dict = {}
|
||||
|
||||
def load_messages(self, lang):
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
# Access the resource file within the package
|
||||
with importlib.resources.open_text(
|
||||
__package__, f"messages_{lang}.properties"
|
||||
) as file:
|
||||
config.read_file(file)
|
||||
|
||||
for key, value in config.items("messages"):
|
||||
self.property_dict[key] = value
|
||||
|
||||
def get(self, key):
|
||||
return self.property_dict.get(key)
|
6
src/stacosys/i18n/messages_en.properties
Normal file
6
src/stacosys/i18n/messages_en.properties
Normal file
|
@ -0,0 +1,6 @@
|
|||
[messages]
|
||||
login.failure.username=Username or password incorrect
|
||||
logout.flash=You have been logged out.
|
||||
admin.comment.notfound=Comment not found.
|
||||
admin.comment.approved=Comment published.
|
||||
admin.comment.deleted=Comment deleted.
|
6
src/stacosys/i18n/messages_fr.properties
Normal file
6
src/stacosys/i18n/messages_fr.properties
Normal file
|
@ -0,0 +1,6 @@
|
|||
[messages]
|
||||
login.failure.username=Identifiant ou mot de passe incorrect
|
||||
logout.flash=Vous avez été déconnecté.
|
||||
admin.comment.notfound=Commentaire introuvable
|
||||
admin.comment.approved=Commentaire publié
|
||||
admin.comment.deleted=Commentaire supprimé
|
42
src/stacosys/interface/__init__.py
Normal file
42
src/stacosys/interface/__init__.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
import background
|
||||
from flask import Flask
|
||||
|
||||
from stacosys.db import dao
|
||||
from stacosys.service.configuration import ConfigParameter
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Set the secret key to some random bytes. Keep this really secret!
|
||||
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@background.task
|
||||
def submit_new_comment(comment):
|
||||
site_url = app.config["CONFIG"].get(ConfigParameter.SITE_URL)
|
||||
comment_list = (
|
||||
f"Web admin interface: {site_url}/web/admin",
|
||||
"",
|
||||
f"author: {comment.author_name}",
|
||||
f"site: {comment.author_site}",
|
||||
f"date: {comment.created}",
|
||||
f"url: {comment.url}",
|
||||
"",
|
||||
comment.content,
|
||||
"",
|
||||
)
|
||||
email_body = "\n".join(comment_list)
|
||||
|
||||
# send email to notify admin
|
||||
site_name = app.config["CONFIG"].get(ConfigParameter.SITE_NAME)
|
||||
subject = f"STACOSYS {site_name}"
|
||||
if app.config["MAILER"].send(subject, email_body):
|
||||
logger.debug("new comment processed")
|
||||
# save notification datetime
|
||||
dao.notify_comment(comment)
|
|
@ -6,37 +6,40 @@ import logging
|
|||
from flask import jsonify, request
|
||||
|
||||
from stacosys.db import dao
|
||||
from stacosys.interface import app
|
||||
from stacosys.interface import app, submit_new_comment
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@app.route("/ping", methods=["GET"])
|
||||
@app.route("/api/ping", methods=["GET"])
|
||||
def ping():
|
||||
return "OK"
|
||||
|
||||
|
||||
@app.route("/comments", methods=["GET"])
|
||||
@app.route("/api/comments", methods=["GET"])
|
||||
def query_comments():
|
||||
comments = []
|
||||
url = request.args.get("url", "")
|
||||
|
||||
logger.info("retrieve comments for url %s" % url)
|
||||
logger.info("retrieve comments for url %s", url)
|
||||
for comment in dao.find_published_comments_by_url(url):
|
||||
d = {
|
||||
comment_dto = {
|
||||
"author": comment.author_name,
|
||||
"content": comment.content,
|
||||
"avatar": comment.author_gravatar,
|
||||
"date": comment.published.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
if comment.author_site:
|
||||
d["site"] = comment.author_site
|
||||
logger.debug(d)
|
||||
comments.append(d)
|
||||
comment_dto["site"] = comment.author_site
|
||||
logger.debug(comment_dto)
|
||||
comments.append(comment_dto)
|
||||
return jsonify({"data": comments})
|
||||
|
||||
|
||||
@app.route("/comments/count", methods=["GET"])
|
||||
@app.route("/api/comments/count", methods=["GET"])
|
||||
def get_comments_count():
|
||||
# send notification for pending e-mails asynchronously
|
||||
for comment in dao.find_not_notified_comments():
|
||||
submit_new_comment(comment)
|
||||
url = request.args.get("url", "")
|
||||
return jsonify({"count": dao.count_published_comments(url)})
|
|
@ -1,26 +1,25 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
from flask import abort, redirect, request
|
||||
|
||||
from stacosys.db import dao
|
||||
from stacosys.interface import app
|
||||
from stacosys.interface import app, submit_new_comment
|
||||
from stacosys.service.configuration import ConfigParameter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@app.route("/newcomment", methods=["POST"])
|
||||
def new_form_comment():
|
||||
|
||||
data = request.form
|
||||
logger.info("form data " + str(data))
|
||||
logger.info("form data %s", str(data))
|
||||
|
||||
# honeypot for spammers
|
||||
captcha = data.get("remarque", "")
|
||||
if captcha:
|
||||
logger.warning("discard spam: data %s" % data)
|
||||
logger.warning("discard spam: data %s", data)
|
||||
abort(400)
|
||||
|
||||
url = data.get("url", "")
|
||||
|
@ -33,21 +32,24 @@ def new_form_comment():
|
|||
|
||||
# anti-spam again
|
||||
if not url or not author_name or not message:
|
||||
logger.warning("empty field: data %s" % data)
|
||||
logger.warning("empty field: data %s", data)
|
||||
abort(400)
|
||||
if not check_form_data(data.to_dict()):
|
||||
logger.warning("additional field: data %s" % data)
|
||||
logger.warning("additional field: data %s", data)
|
||||
abort(400)
|
||||
|
||||
# add a row to Comment table
|
||||
dao.create_comment(url, author_name, author_site, author_gravatar, message)
|
||||
comment = dao.create_comment(
|
||||
url, author_name, author_site, author_gravatar, message
|
||||
)
|
||||
|
||||
return redirect("/redirect/", code=302)
|
||||
# send notification e-mail asynchronously
|
||||
submit_new_comment(comment)
|
||||
|
||||
return redirect(app.config["CONFIG"].get(ConfigParameter.SITE_REDIRECT), code=302)
|
||||
|
||||
|
||||
def check_form_data(d):
|
||||
def check_form_data(posted_comment):
|
||||
fields = ["url", "message", "site", "remarque", "author", "token", "email"]
|
||||
filtered = dict(filter(lambda x: x[0] not in fields, d.items()))
|
||||
filtered = dict(filter(lambda x: x[0] not in fields, posted_comment.items()))
|
||||
return not filtered
|
||||
|
||||
|
64
src/stacosys/interface/templates/admin_en.html
Normal file
64
src/stacosys/interface/templates/admin_en.html
Normal file
|
@ -0,0 +1,64 @@
|
|||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stacosys Comment Moderation</title>
|
||||
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h2>Comment Moderation</h2>
|
||||
<nav>
|
||||
<a href="/web/logout">Log out</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<blockquote>
|
||||
{% for message in messages %}
|
||||
<p>{{ message }}</p>
|
||||
{% endfor %}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Author</th>
|
||||
<th>Comment</th>
|
||||
<th>Article</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for comment in comments %}
|
||||
<tr>
|
||||
<td>{{ comment.created }}</td>
|
||||
<td>{{ comment.author_name }}</td>
|
||||
<td>{{ comment.content }}</td>
|
||||
<td><a href="{{ baseurl + comment.url }}">{{ comment.url }}</a></td>
|
||||
<td>
|
||||
<form action="/web/admin" method="post">
|
||||
<input type="hidden" name="comment" value="{{comment.id}}">
|
||||
<input type="hidden" name="action" value="APPROVE">
|
||||
<button type="submit">Approve</button>
|
||||
</form>
|
||||
<form action="/web/admin" method="post">
|
||||
<input type="hidden" name="comment" value="{{comment.id}}">
|
||||
<input type="hidden" name="action" value="REJECT">
|
||||
<button type="submit">Reject</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
<footer>
|
||||
<p>This page was designed by Yax with <a href="https://simplecss.org">Simple.css</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
64
src/stacosys/interface/templates/admin_fr.html
Normal file
64
src/stacosys/interface/templates/admin_fr.html
Normal file
|
@ -0,0 +1,64 @@
|
|||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stacosys</title>
|
||||
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h2>Modération des commentaires</h2>
|
||||
<nav>
|
||||
<a href="/web/logout">Déconnecter</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<blockquote>
|
||||
{% for message in messages %}
|
||||
<p>{{ message }}</p>
|
||||
{% endfor %}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Auteur</th>
|
||||
<th>Commentaire</th>
|
||||
<th>Article</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for comment in comments %}
|
||||
<tr>
|
||||
<td>{{ comment.created }}</td>
|
||||
<td>{{ comment.author_name }}</td>
|
||||
<td>{{ comment.content }}</td>
|
||||
<td><a href="{{ baseurl + comment.url }}">{{ comment.url }}</a></td>
|
||||
<td>
|
||||
<form action="/web/admin" method="post">
|
||||
<input type="hidden" name="comment" value="{{comment.id}}">
|
||||
<input type="hidden" name="action" value="APPROVE">
|
||||
<button type="submit">Accepter</button>
|
||||
</form>
|
||||
<form action="/web/admin" method="post">
|
||||
<input type="hidden" name="comment" value="{{comment.id}}">
|
||||
<input type="hidden" name="action" value="REJECT">
|
||||
<button type="submit">Rejeter</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
<footer>
|
||||
<p>Cette page a été conçue par Yax avec <a href="https://simplecss.org">Simple.css</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
42
src/stacosys/interface/templates/login_en.html
Normal file
42
src/stacosys/interface/templates/login_en.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stacosys</title>
|
||||
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||
<style>
|
||||
form {
|
||||
width: 350px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h2>Comment Moderation Login</h2>
|
||||
</header>
|
||||
<main>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<blockquote>
|
||||
{% for message in messages %}
|
||||
<p>{{ message }}</p>
|
||||
{% endfor %}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form action="/web/login" method="POST">
|
||||
<p><label>Username:</label></p>
|
||||
<p><input type="text" name="username" /></p>
|
||||
<p><label>Password:</label></p>
|
||||
<p><input type="password" name="password" /></p>
|
||||
<input type="submit" value="Log in" />
|
||||
</form>
|
||||
</main>
|
||||
<footer>
|
||||
<p>This page was designed with <a href="https://simplecss.org">Simple.css</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
42
src/stacosys/interface/templates/login_fr.html
Normal file
42
src/stacosys/interface/templates/login_fr.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stacosys</title>
|
||||
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||
<style>
|
||||
form {
|
||||
width: 350px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h2>Modération des commentaires</h2>
|
||||
</header>
|
||||
<main>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<blockquote>
|
||||
{% for message in messages %}
|
||||
<p>{{ message }}</p>
|
||||
{% endfor %}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form action="/web/login" method="POST">
|
||||
<p><label>Utilisateur </label></p>
|
||||
<p><input type="text" name="username" /></p>
|
||||
<p><label>Mot de passe </label></p>
|
||||
<p><input type="password" name="password" /></p>
|
||||
<input type="submit" value="Connecter" />
|
||||
</form>
|
||||
</main>
|
||||
<footer>
|
||||
<p>Cette page a été conçue avec <a href="https://simplecss.org">Simple.css</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
83
src/stacosys/interface/web/admin.py
Normal file
83
src/stacosys/interface/web/admin.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from flask import flash, redirect, render_template, request, session
|
||||
|
||||
from stacosys.db import dao
|
||||
from stacosys.interface import app
|
||||
from stacosys.service.configuration import ConfigParameter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app.add_url_rule("/web", endpoint="index")
|
||||
app.add_url_rule("/web/", endpoint="index")
|
||||
|
||||
|
||||
@app.endpoint("index")
|
||||
def index():
|
||||
return redirect("/web/admin")
|
||||
|
||||
|
||||
def is_login_ok(username, password):
|
||||
hashed = hashlib.sha256(password.encode()).hexdigest().upper()
|
||||
return (
|
||||
app.config["CONFIG"].get(ConfigParameter.WEB_USERNAME) == username
|
||||
and app.config["CONFIG"].get(ConfigParameter.WEB_PASSWORD) == hashed
|
||||
)
|
||||
|
||||
|
||||
@app.route("/web/login", methods=["POST", "GET"])
|
||||
def login():
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username")
|
||||
password = request.form.get("password")
|
||||
if is_login_ok(username, password):
|
||||
session["user"] = username
|
||||
return redirect("/web/admin")
|
||||
flash(app.config["MESSAGES"].get("login.failure.username"))
|
||||
return redirect("/web/login")
|
||||
# GET
|
||||
return render_template(
|
||||
"login_" + app.config["CONFIG"].get(ConfigParameter.LANG) + ".html"
|
||||
)
|
||||
|
||||
|
||||
@app.route("/web/logout", methods=["GET"])
|
||||
def logout():
|
||||
session.pop("user")
|
||||
flash(app.config["MESSAGES"].get("logout.flash"))
|
||||
return redirect("/web/admin")
|
||||
|
||||
|
||||
@app.route("/web/admin", methods=["GET"])
|
||||
def admin_homepage():
|
||||
if not (
|
||||
"user" in session
|
||||
and session["user"] == app.config["CONFIG"].get(ConfigParameter.WEB_USERNAME)
|
||||
):
|
||||
return redirect("/web/login")
|
||||
|
||||
comments = dao.find_not_published_comments()
|
||||
return render_template(
|
||||
"admin_" + app.config["CONFIG"].get(ConfigParameter.LANG) + ".html",
|
||||
comments=comments,
|
||||
baseurl=app.config["CONFIG"].get(ConfigParameter.SITE_URL),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/web/admin", methods=["POST"])
|
||||
def admin_action():
|
||||
comment = dao.find_comment_by_id(request.form.get("comment"))
|
||||
if comment is None:
|
||||
flash(app.config["MESSAGES"].get("admin.comment.notfound"))
|
||||
elif request.form.get("action") == "APPROVE":
|
||||
dao.publish_comment(comment)
|
||||
app.config["RSS"].generate()
|
||||
flash(app.config["MESSAGES"].get("admin.comment.approved"))
|
||||
else:
|
||||
dao.delete_comment(comment)
|
||||
flash(app.config["MESSAGES"].get("admin.comment.deleted"))
|
||||
return redirect("/web/admin")
|
19
src/stacosys/model/comment.py
Normal file
19
src/stacosys/model/comment.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Comment:
|
||||
id: int = 0
|
||||
url: str = ""
|
||||
created: Optional[datetime] = None
|
||||
notified: Optional[datetime] = None
|
||||
published: Optional[datetime] = None
|
||||
author_name: str = ""
|
||||
author_site: str = ""
|
||||
author_gravatar: str = ""
|
||||
content: str = ""
|
105
src/stacosys/run.py
Normal file
105
src/stacosys/run.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from stacosys.db import database
|
||||
from stacosys.i18n.messages import Messages
|
||||
from stacosys.interface import api, app, form
|
||||
from stacosys.interface.web import admin
|
||||
from stacosys.service.configuration import Config, ConfigParameter
|
||||
from stacosys.service.mail import Mailer
|
||||
from stacosys.service.rssfeed import Rss
|
||||
|
||||
|
||||
# configure logging
|
||||
def configure_logging() -> logging.Logger:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="[%(asctime)s] %(name)s %(levelname)s %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.getLogger("werkzeug").level = logging.WARNING
|
||||
return logger
|
||||
|
||||
|
||||
def load_and_validate_config(config_pathname: str, logger: logging.Logger) -> Config:
|
||||
if not os.path.isfile(config_pathname):
|
||||
logger.error("Configuration file '%s' not found.", config_pathname)
|
||||
raise FileNotFoundError(f"Configuration file '{config_pathname}' not found.")
|
||||
|
||||
config = Config()
|
||||
config.load(config_pathname)
|
||||
if not config.check():
|
||||
raise ValueError(f"Invalid configuration '{config_pathname}'")
|
||||
logger.info("Configuration loaded successfully.")
|
||||
return config
|
||||
|
||||
|
||||
def configure_and_validate_mailer(config, logger):
|
||||
mailer = Mailer()
|
||||
mailer.configure_smtp(
|
||||
config.get(ConfigParameter.SMTP_HOST),
|
||||
config.get_int(ConfigParameter.SMTP_PORT),
|
||||
config.get(ConfigParameter.SMTP_LOGIN),
|
||||
config.get(ConfigParameter.SMTP_PASSWORD),
|
||||
)
|
||||
mailer.configure_destination(config.get(ConfigParameter.SITE_ADMIN_EMAIL))
|
||||
if not mailer.check():
|
||||
logger.error("Email configuration not working")
|
||||
sys.exit(1)
|
||||
return mailer
|
||||
|
||||
|
||||
def configure_rss(config):
|
||||
rss = Rss()
|
||||
rss.configure(
|
||||
config.get(ConfigParameter.RSS_FILE),
|
||||
config.get(ConfigParameter.SITE_NAME),
|
||||
config.get(ConfigParameter.SITE_PROTO),
|
||||
config.get(ConfigParameter.SITE_URL),
|
||||
)
|
||||
rss.generate()
|
||||
return rss
|
||||
|
||||
|
||||
def configure_localization(config):
|
||||
messages = Messages()
|
||||
messages.load_messages(config.get(ConfigParameter.LANG))
|
||||
return messages
|
||||
|
||||
|
||||
def main(config_pathname):
|
||||
logger = configure_logging()
|
||||
config = load_and_validate_config(config_pathname, logger)
|
||||
database.configure(config.get(ConfigParameter.DB))
|
||||
|
||||
logger.info("Start Stacosys application")
|
||||
rss = configure_rss(config)
|
||||
mailer = configure_and_validate_mailer(config, logger)
|
||||
messages = configure_localization(config)
|
||||
|
||||
logger.info("start interfaces %s %s %s", api, form, admin)
|
||||
app.config["CONFIG"] = config
|
||||
app.config["MAILER"] = mailer
|
||||
app.config["RSS"] = rss
|
||||
app.config["MESSAGES"] = messages
|
||||
app.run(
|
||||
host=config.get(ConfigParameter.HTTP_HOST),
|
||||
port=config.get_int(ConfigParameter.HTTP_PORT),
|
||||
debug=False,
|
||||
use_reloader=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("config", help="config path name")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
main(args.config)
|
||||
except Exception as e:
|
||||
logging.error("Failed to start application: %s", e)
|
||||
sys.exit(1)
|
89
src/stacosys/service/configuration.py
Normal file
89
src/stacosys/service/configuration.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import configparser
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ConfigParameter(Enum):
|
||||
DB = "main.db"
|
||||
LANG = "main.lang"
|
||||
|
||||
HTTP_HOST = "http.host"
|
||||
HTTP_PORT = "http.port"
|
||||
|
||||
RSS_FILE = "rss.file"
|
||||
|
||||
SMTP_HOST = "smtp.host"
|
||||
SMTP_PORT = "smtp.port"
|
||||
SMTP_LOGIN = "smtp.login"
|
||||
SMTP_PASSWORD = "smtp.password"
|
||||
|
||||
SITE_PROTO = "site.proto"
|
||||
SITE_NAME = "site.name"
|
||||
SITE_URL = "site.url"
|
||||
SITE_ADMIN_EMAIL = "site.admin_email"
|
||||
SITE_REDIRECT = "site.redirect"
|
||||
|
||||
WEB_USERNAME = "web.username"
|
||||
WEB_PASSWORD = "web.password"
|
||||
|
||||
|
||||
class Config:
|
||||
_cfg = configparser.ConfigParser()
|
||||
|
||||
def load(self, config_pathname):
|
||||
self._cfg.read(config_pathname)
|
||||
|
||||
@staticmethod
|
||||
def _split_key(key: ConfigParameter):
|
||||
section, param = str(key.value).split(".")
|
||||
if not param:
|
||||
param = section
|
||||
section = ""
|
||||
return section, param
|
||||
|
||||
def exists(self, key: ConfigParameter):
|
||||
section, param = self._split_key(key)
|
||||
return self._cfg.has_option(section, param)
|
||||
|
||||
def get(self, key: ConfigParameter) -> str:
|
||||
section, param = self._split_key(key)
|
||||
return (
|
||||
self._cfg.get(section, param)
|
||||
if self._cfg.has_option(section, param)
|
||||
else ""
|
||||
)
|
||||
|
||||
def put(self, key: ConfigParameter, value):
|
||||
section, param = self._split_key(key)
|
||||
if section and not self._cfg.has_section(section):
|
||||
self._cfg.add_section(section)
|
||||
self._cfg.set(section, param, str(value))
|
||||
|
||||
def get_int(self, key: ConfigParameter) -> int:
|
||||
value = self.get(key)
|
||||
return int(value) if value else 0
|
||||
|
||||
def get_bool(self, key: ConfigParameter) -> bool:
|
||||
value = self.get(key)
|
||||
assert value in (
|
||||
"yes",
|
||||
"true",
|
||||
"no",
|
||||
"false",
|
||||
), f"Parameètre booléen incorrect {key.value}"
|
||||
return value in ("yes", "true")
|
||||
|
||||
def check(self):
|
||||
for key in ConfigParameter:
|
||||
if not self.get(key):
|
||||
return False, key.value
|
||||
return True, None
|
||||
|
||||
def __repr__(self):
|
||||
dict_repr = {}
|
||||
for section in self._cfg.sections():
|
||||
for option in self._cfg.options(section):
|
||||
dict_repr[".".join([section, option])] = self._cfg.get(section, option)
|
||||
return str(dict_repr)
|
59
src/stacosys/service/mail.py
Normal file
59
src/stacosys/service/mail.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from email.mime.text import MIMEText
|
||||
from smtplib import SMTP_SSL, SMTPAuthenticationError, SMTPException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Mailer:
|
||||
def __init__(self) -> None:
|
||||
self._smtp_host = ""
|
||||
self._smtp_port = 0
|
||||
self._smtp_login = ""
|
||||
self._smtp_password = ""
|
||||
self._site_admin_email = ""
|
||||
|
||||
def configure_smtp(
|
||||
self, smtp_host: str, smtp_port: int, smtp_login: str, smtp_password: str
|
||||
) -> None:
|
||||
self._smtp_host = smtp_host
|
||||
self._smtp_port = smtp_port
|
||||
self._smtp_login = smtp_login
|
||||
self._smtp_password = smtp_password
|
||||
|
||||
def configure_destination(self, site_admin_email: str) -> None:
|
||||
self._site_admin_email = site_admin_email
|
||||
|
||||
def check(self) -> bool:
|
||||
try:
|
||||
with SMTP_SSL(self._smtp_host, self._smtp_port) as server:
|
||||
server.login(self._smtp_login, self._smtp_password)
|
||||
return True
|
||||
except SMTPAuthenticationError:
|
||||
logger.exception("Invalid credentials")
|
||||
return False
|
||||
|
||||
def send(self, subject: str, message: str) -> bool:
|
||||
sender = self._smtp_login
|
||||
|
||||
try:
|
||||
msg = MIMEText(message)
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = sender
|
||||
msg["To"] = self._site_admin_email
|
||||
|
||||
with SMTP_SSL(self._smtp_host, self._smtp_port) as server:
|
||||
try:
|
||||
server.login(self._smtp_login, self._smtp_password)
|
||||
except SMTPAuthenticationError:
|
||||
logger.exception("Invalid credentials")
|
||||
return False
|
||||
|
||||
server.send_message(msg)
|
||||
return True
|
||||
except SMTPException:
|
||||
logger.error("Error sending email", exc_info=True)
|
||||
return False
|
56
src/stacosys/service/rssfeed.py
Normal file
56
src/stacosys/service/rssfeed.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import markdown
|
||||
import PyRSS2Gen
|
||||
|
||||
from stacosys.db import dao
|
||||
|
||||
|
||||
class Rss:
|
||||
def __init__(self) -> None:
|
||||
self._rss_file: str = ""
|
||||
self._site_proto: str = ""
|
||||
self._site_name: str = ""
|
||||
self._site_url: str = ""
|
||||
|
||||
def configure(
|
||||
self,
|
||||
rss_file,
|
||||
site_name,
|
||||
site_proto,
|
||||
site_url,
|
||||
) -> None:
|
||||
self._rss_file = rss_file
|
||||
self._site_name = site_name
|
||||
self._site_proto = site_proto
|
||||
self._site_url = site_url
|
||||
|
||||
def generate(self) -> None:
|
||||
markdownizer = markdown.Markdown()
|
||||
|
||||
items = []
|
||||
for row in dao.find_recent_published_comments():
|
||||
item_link = f"{self._site_proto}://{self._site_url}{row.url}"
|
||||
items.append(
|
||||
PyRSS2Gen.RSSItem(
|
||||
title=f"{self._site_proto}://{self._site_url}{row.url} - {row.author_name}",
|
||||
link=item_link,
|
||||
description=markdownizer.convert(row.content),
|
||||
guid=PyRSS2Gen.Guid(f"{item_link}{row.id}"),
|
||||
pubDate=row.published,
|
||||
)
|
||||
)
|
||||
|
||||
rss_title = f"Commentaires du site {self._site_name}"
|
||||
rss = PyRSS2Gen.RSS2(
|
||||
title=rss_title,
|
||||
link=f"{self._site_proto}://{self._site_url}",
|
||||
description=rss_title,
|
||||
lastBuildDate=datetime.now(),
|
||||
items=items,
|
||||
)
|
||||
with open(self._rss_file, "w", encoding="utf-8") as outfile:
|
||||
rss.write_xml(outfile, encoding="utf-8")
|
|
@ -1 +0,0 @@
|
|||
__version__ = "2.0"
|
|
@ -1,70 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from enum import Enum
|
||||
|
||||
import profig
|
||||
|
||||
|
||||
class ConfigParameter(Enum):
|
||||
DB_SQLITE_FILE = "main.db_sqlite_file"
|
||||
LANG = "main.lang"
|
||||
COMMENT_POLLING = "main.newcomment_polling"
|
||||
|
||||
HTTP_HOST = "http.host"
|
||||
HTTP_PORT = "http.port"
|
||||
|
||||
RSS_PROTO = "rss.proto"
|
||||
RSS_FILE = "rss.file"
|
||||
|
||||
IMAP_POLLING = "imap.polling"
|
||||
IMAP_SSL = "imap.ssl"
|
||||
IMAP_HOST = "imap.host"
|
||||
IMAP_PORT = "imap.port"
|
||||
IMAP_LOGIN = "imap.login"
|
||||
IMAP_PASSWORD = "imap.password"
|
||||
|
||||
SMTP_STARTTLS = "smtp.starttls"
|
||||
SMTP_SSL = "smtp.ssl"
|
||||
SMTP_HOST = "smtp.host"
|
||||
SMTP_PORT = "smtp.port"
|
||||
SMTP_LOGIN = "smtp.login"
|
||||
SMTP_PASSWORD = "smtp.password"
|
||||
|
||||
SITE_NAME = "site.name"
|
||||
SITE_URL = "site.url"
|
||||
SITE_TOKEN = "site.token"
|
||||
SITE_ADMIN_EMAIL = "site.admin_email"
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
self._params = dict()
|
||||
|
||||
@classmethod
|
||||
def load(cls, config_pathname):
|
||||
cfg = profig.Config(config_pathname)
|
||||
cfg.sync()
|
||||
config = cls()
|
||||
config._params.update(cfg)
|
||||
return config
|
||||
|
||||
def exists(self, key: ConfigParameter):
|
||||
return key.value in self._params
|
||||
|
||||
def get(self, key: ConfigParameter):
|
||||
return self._params[key.value] if key.value in self._params else None
|
||||
|
||||
def put(self, key: ConfigParameter, value):
|
||||
self._params[key.value] = value
|
||||
|
||||
def get_int(self, key: ConfigParameter):
|
||||
return int(self._params[key.value])
|
||||
|
||||
def get_bool(self, key: ConfigParameter):
|
||||
value = self._params[key.value].lower()
|
||||
assert value in ("yes", "true", "no", "false")
|
||||
return value in ("yes", "true")
|
||||
|
||||
def __repr__(self):
|
||||
return self._params.__repr__()
|
|
@ -1,106 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from stacosys.core.mailer import Mailer
|
||||
from stacosys.core.rss import Rss
|
||||
from stacosys.core.templater import Templater, Template
|
||||
from stacosys.db import dao
|
||||
from stacosys.model.email import Email
|
||||
|
||||
REGEX_EMAIL_SUBJECT = r".*STACOSYS.*\[(\d+)\:(\w+)\]"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
template_path = os.path.abspath(os.path.join(current_path, "../templates"))
|
||||
templater = Templater(template_path)
|
||||
|
||||
|
||||
def fetch_mail_answers(lang, mailer: Mailer, rss: Rss, site_token):
|
||||
for msg in mailer.fetch():
|
||||
# filter stacosys e-mails
|
||||
m = re.search(REGEX_EMAIL_SUBJECT, msg.subject, re.DOTALL)
|
||||
if not m:
|
||||
continue
|
||||
|
||||
comment_id = int(m.group(1))
|
||||
submitted_token = m.group(2)
|
||||
|
||||
# validate token
|
||||
if submitted_token != site_token:
|
||||
logger.warning("ignore corrupted email. Unknown token %d" % comment_id)
|
||||
continue
|
||||
|
||||
if not msg.plain_text_content:
|
||||
logger.warning("ignore empty email")
|
||||
continue
|
||||
|
||||
_reply_comment_email(lang, mailer, rss, msg, comment_id)
|
||||
mailer.delete(msg.id)
|
||||
|
||||
|
||||
def _reply_comment_email(lang, mailer: Mailer, rss: Rss, email: Email, comment_id):
|
||||
# retrieve comment
|
||||
comment = dao.find_comment_by_id(comment_id)
|
||||
if not comment:
|
||||
logger.warning("unknown comment %d" % comment_id)
|
||||
return
|
||||
|
||||
if comment.published:
|
||||
logger.warning("ignore already published email. token %d" % comment_id)
|
||||
return
|
||||
|
||||
# safe logic: no answer or unknown answer is a go for publishing
|
||||
if email.plain_text_content[:2].upper() == "NO":
|
||||
logger.info("discard comment: %d" % comment_id)
|
||||
dao.delete_comment(comment)
|
||||
new_email_body = templater.get_template(lang, Template.DROP_COMMENT).render(
|
||||
original=email.plain_text_content
|
||||
)
|
||||
if not mailer.send(email.from_addr, "Re: " + email.subject, new_email_body):
|
||||
logger.warning("minor failure. cannot send rejection mail " + email.subject)
|
||||
else:
|
||||
# save publishing datetime
|
||||
dao.publish_comment(comment)
|
||||
logger.info("commit comment: %d" % comment_id)
|
||||
|
||||
# rebuild RSS
|
||||
rss.generate()
|
||||
|
||||
# send approval confirmation email to admin
|
||||
new_email_body = templater.get_template(lang, Template.APPROVE_COMMENT).render(
|
||||
original=email.plain_text_content
|
||||
)
|
||||
if not mailer.send(email.from_addr, "Re: " + email.subject, new_email_body):
|
||||
logger.warning("minor failure. cannot send approval email " + email.subject)
|
||||
|
||||
|
||||
def submit_new_comment(lang, site_name, site_token, site_admin_email, mailer):
|
||||
for comment in dao.find_not_notified_comments():
|
||||
comment_list = (
|
||||
"author: %s" % comment.author_name,
|
||||
"site: %s" % comment.author_site,
|
||||
"date: %s" % comment.created,
|
||||
"url: %s" % comment.url,
|
||||
"",
|
||||
"%s" % comment.content,
|
||||
"",
|
||||
)
|
||||
comment_text = "\n".join(comment_list)
|
||||
email_body = templater.get_template(lang, Template.NEW_COMMENT).render(
|
||||
url=comment.url, comment=comment_text
|
||||
)
|
||||
|
||||
# send email to notify admin
|
||||
subject = "STACOSYS %s: [%d:%s]" % (site_name, comment.id, site_token)
|
||||
if mailer.send(site_admin_email, subject, email_body):
|
||||
logger.debug("new comment processed ")
|
||||
|
||||
# save notification datetime
|
||||
dao.notify_comment(comment)
|
||||
else:
|
||||
logger.warning("rescheduled. send mail failure " + subject)
|
|
@ -1,162 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import email
|
||||
import imaplib
|
||||
import logging
|
||||
import re
|
||||
from email.message import Message
|
||||
|
||||
from stacosys.model.email import Attachment, Email, Part
|
||||
|
||||
filename_re = re.compile('filename="(.+)"|filename=([^;\n\r"\']+)', re.I | re.S)
|
||||
|
||||
|
||||
class Mailbox(object):
|
||||
def __init__(self, host, port, ssl, login, password):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.ssl = ssl
|
||||
self.login = login
|
||||
self.password = password
|
||||
|
||||
def __enter__(self):
|
||||
if self.ssl:
|
||||
self.imap = imaplib.IMAP4_SSL(self.host, self.port)
|
||||
else:
|
||||
self.imap = imaplib.IMAP4(self.host, self.port)
|
||||
self.imap.login(self.login, self.password)
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.imap.close()
|
||||
self.imap.logout()
|
||||
|
||||
def get_count(self):
|
||||
self.imap.select("Inbox")
|
||||
_, data = self.imap.search(None, "ALL")
|
||||
return sum(1 for num in data[0].split())
|
||||
|
||||
def fetch_raw_message(self, num):
|
||||
self.imap.select("Inbox")
|
||||
_, data = self.imap.fetch(str(num), "(RFC822)")
|
||||
email_msg = email.message_from_bytes(data[0][1])
|
||||
return email_msg
|
||||
|
||||
def fetch_message(self, num):
|
||||
raw_msg = self.fetch_raw_message(num)
|
||||
|
||||
parts = []
|
||||
attachments = []
|
||||
plain_text_content = "no plain-text part"
|
||||
for part in raw_msg.walk():
|
||||
if part.is_multipart():
|
||||
continue
|
||||
|
||||
content_disposition = part.get("Content-Disposition", None)
|
||||
if content_disposition:
|
||||
# we have attachment
|
||||
r = filename_re.findall(content_disposition)
|
||||
if r:
|
||||
filename = sorted(r[0])[1]
|
||||
else:
|
||||
filename = "undefined"
|
||||
content = base64.b64encode(part.get_payload(decode=True))
|
||||
content = content.decode()
|
||||
attachments.append(
|
||||
Attachment(
|
||||
filename=email_nonascii_to_uft8(filename),
|
||||
content=content,
|
||||
content_type=part.get_content_type(),
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
content = to_plain_text_content(part)
|
||||
except Exception:
|
||||
logging.exception("cannot extract content from mail part")
|
||||
|
||||
parts.append(
|
||||
Part(content=content, content_type=part.get_content_type())
|
||||
)
|
||||
|
||||
if part.get_content_type() == "text/plain":
|
||||
plain_text_content = content
|
||||
|
||||
return Email(
|
||||
id=num,
|
||||
encoding="UTF-8",
|
||||
date=parse_date(raw_msg["Date"]).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
from_addr=raw_msg["From"],
|
||||
to_addr=raw_msg["To"],
|
||||
subject=email_nonascii_to_uft8(raw_msg["Subject"]),
|
||||
parts=parts,
|
||||
attachments=attachments,
|
||||
plain_text_content=plain_text_content,
|
||||
)
|
||||
|
||||
def delete_message(self, num):
|
||||
self.imap.select("Inbox")
|
||||
self.imap.store(str(num), "+FLAGS", r"\Deleted")
|
||||
self.imap.expunge()
|
||||
|
||||
def delete_all(self):
|
||||
self.imap.select("Inbox")
|
||||
_, data = self.imap.search(None, "ALL")
|
||||
for num in data[0].split():
|
||||
self.imap.store(num, "+FLAGS", r"\Deleted")
|
||||
self.imap.expunge()
|
||||
|
||||
def print_msgs(self):
|
||||
self.imap.select("Inbox")
|
||||
_, data = self.imap.search(None, "ALL")
|
||||
for num in reversed(data[0].split()):
|
||||
status, data = self.imap.fetch(num, "(RFC822)")
|
||||
self.logger.debug("Message %s\n%s\n" % (num, data[0][1]))
|
||||
|
||||
|
||||
def parse_date(v):
|
||||
if v is None:
|
||||
return datetime.datetime.now()
|
||||
|
||||
tt = email.utils.parsedate_tz(v)
|
||||
|
||||
if tt is None:
|
||||
return datetime.datetime.now()
|
||||
|
||||
timestamp = email.utils.mktime_tz(tt)
|
||||
date = datetime.datetime.fromtimestamp(timestamp)
|
||||
return date
|
||||
|
||||
|
||||
def to_utf8(string, charset):
|
||||
return string.decode(charset).encode("UTF-8").decode("UTF-8")
|
||||
|
||||
|
||||
def email_nonascii_to_uft8(string):
|
||||
|
||||
# RFC 1342 is a recommendation that provides a way to represent non ASCII
|
||||
# characters inside e-mail in a way that won’t confuse e-mail servers
|
||||
subject = ""
|
||||
for v, charset in email.header.decode_header(string):
|
||||
if charset is None:
|
||||
if type(v) is bytes:
|
||||
v = v.decode()
|
||||
subject = subject + v
|
||||
else:
|
||||
subject = subject + to_utf8(v, charset)
|
||||
return subject
|
||||
|
||||
|
||||
def to_plain_text_content(part: Message) -> str:
|
||||
content = part.get_payload(decode=True)
|
||||
charset = part.get_param("charset", None)
|
||||
if charset:
|
||||
content = to_utf8(content, charset)
|
||||
elif type(content) == bytes:
|
||||
content = content.decode("utf8")
|
||||
# RFC 3676: remove automatic word-wrapping
|
||||
return content.replace(" \r\n", " ")
|
|
@ -1,150 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import smtplib
|
||||
import email.utils
|
||||
from email.mime.text import MIMEText
|
||||
from email.message import EmailMessage
|
||||
from logging.handlers import SMTPHandler
|
||||
|
||||
from stacosys.core import imap
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Mailer:
|
||||
def __init__(
|
||||
self,
|
||||
imap_host,
|
||||
imap_port,
|
||||
imap_ssl,
|
||||
imap_login,
|
||||
imap_password,
|
||||
smtp_host,
|
||||
smtp_port,
|
||||
smtp_starttls,
|
||||
smtp_ssl,
|
||||
smtp_login,
|
||||
smtp_password,
|
||||
site_admin_email,
|
||||
):
|
||||
self._imap_host = imap_host
|
||||
self._imap_port = imap_port
|
||||
self._imap_ssl = imap_ssl
|
||||
self._imap_login = imap_login
|
||||
self._imap_password = imap_password
|
||||
self._smtp_host = smtp_host
|
||||
self._smtp_port = smtp_port
|
||||
self._smtp_starttls = smtp_starttls
|
||||
self._smtp_ssl = smtp_ssl
|
||||
self._smtp_login = smtp_login
|
||||
self._smtp_password = smtp_password
|
||||
self._site_admin_email = site_admin_email
|
||||
|
||||
def _open_mailbox(self):
|
||||
return imap.Mailbox(
|
||||
self._imap_host,
|
||||
self._imap_port,
|
||||
self._imap_ssl,
|
||||
self._imap_login,
|
||||
self._imap_password,
|
||||
)
|
||||
|
||||
def fetch(self):
|
||||
msgs = []
|
||||
try:
|
||||
with self._open_mailbox() as mbox:
|
||||
count = mbox.get_count()
|
||||
for num in range(count):
|
||||
msgs.append(mbox.fetch_message(num + 1))
|
||||
except Exception:
|
||||
logger.exception("fetch mail exception")
|
||||
return msgs
|
||||
|
||||
def send(self, to_email, subject, message):
|
||||
|
||||
# Create the container (outer) email message.
|
||||
msg = MIMEText(message)
|
||||
msg["Subject"] = subject
|
||||
msg["To"] = to_email
|
||||
msg["From"] = self._smtp_login
|
||||
|
||||
success = True
|
||||
try:
|
||||
if self._smtp_ssl:
|
||||
s = smtplib.SMTP_SSL(self._smtp_host, self._smtp_port)
|
||||
else:
|
||||
s = smtplib.SMTP(self._smtp_host, self._smtp_port)
|
||||
if self._smtp_starttls:
|
||||
s.starttls()
|
||||
if self._smtp_login:
|
||||
s.login(self._smtp_login, self._smtp_password)
|
||||
s.send_message(msg)
|
||||
s.quit()
|
||||
except Exception:
|
||||
logger.exception("send mail exception")
|
||||
success = False
|
||||
return success
|
||||
|
||||
def delete(self, id):
|
||||
try:
|
||||
with self._open_mailbox() as mbox:
|
||||
mbox.delete_message(id)
|
||||
except Exception:
|
||||
logger.exception("delete mail exception")
|
||||
|
||||
def get_error_handler(self):
|
||||
if self._smtp_ssl:
|
||||
mail_handler = SSLSMTPHandler(
|
||||
mailhost=(
|
||||
self._smtp_host,
|
||||
self._smtp_port,
|
||||
),
|
||||
credentials=(
|
||||
self._smtp_login,
|
||||
self._smtp_password,
|
||||
),
|
||||
fromaddr=self._smtp_login,
|
||||
toaddrs=self._site_admin_email,
|
||||
subject="Stacosys error",
|
||||
)
|
||||
else:
|
||||
mail_handler = SMTPHandler(
|
||||
mailhost=(
|
||||
self._smtp_host,
|
||||
self._smtp_port,
|
||||
),
|
||||
credentials=(
|
||||
self._smtp_login,
|
||||
self._smtp_password,
|
||||
),
|
||||
fromaddr=self._smtp_login,
|
||||
toaddrs=self._site_admin_email,
|
||||
subject="Stacosys error",
|
||||
)
|
||||
mail_handler.setLevel(logging.ERROR)
|
||||
return mail_handler
|
||||
|
||||
|
||||
class SSLSMTPHandler(SMTPHandler):
|
||||
def emit(self, record):
|
||||
"""
|
||||
Emit a record.
|
||||
|
||||
Format the record and send it to the specified addressees.
|
||||
"""
|
||||
try:
|
||||
smtp = smtplib.SMTP_SSL(self.mailhost, self.mailport)
|
||||
msg = EmailMessage()
|
||||
msg["From"] = self.fromaddr
|
||||
msg["To"] = ",".join(self.toaddrs)
|
||||
msg["Subject"] = self.getSubject(record)
|
||||
msg["Date"] = email.utils.localtime()
|
||||
msg.set_content(self.format(record))
|
||||
if self.username:
|
||||
smtp.login(self.username, self.password)
|
||||
smtp.send_message(msg)
|
||||
smtp.quit()
|
||||
except Exception:
|
||||
self.handleError(record)
|
|
@ -1,64 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import markdown
|
||||
import PyRSS2Gen
|
||||
|
||||
from stacosys.core.templater import Templater, Template
|
||||
from stacosys.model.comment import Comment
|
||||
|
||||
|
||||
class Rss:
|
||||
def __init__(
|
||||
self,
|
||||
lang,
|
||||
rss_file,
|
||||
rss_proto,
|
||||
site_name,
|
||||
site_url,
|
||||
):
|
||||
self._lang = lang
|
||||
self._rss_file = rss_file
|
||||
self._rss_proto = rss_proto
|
||||
self._site_name = site_name
|
||||
self._site_url = site_url
|
||||
current_path = os.path.dirname(__file__)
|
||||
template_path = os.path.abspath(os.path.join(current_path, "../templates"))
|
||||
self._templater = Templater(template_path)
|
||||
|
||||
def generate(self):
|
||||
rss_title = self._templater.get_template(
|
||||
self._lang, Template.RSS_TITLE_MESSAGE
|
||||
).render(site=self._site_name)
|
||||
md = markdown.Markdown()
|
||||
|
||||
items = []
|
||||
for row in (
|
||||
Comment.select()
|
||||
.where(Comment.published)
|
||||
.order_by(-Comment.published)
|
||||
.limit(10)
|
||||
):
|
||||
item_link = "%s://%s%s" % (self._rss_proto, self._site_url, row.url)
|
||||
items.append(
|
||||
PyRSS2Gen.RSSItem(
|
||||
title="%s - %s://%s%s"
|
||||
% (self._rss_proto, row.author_name, self._site_url, row.url),
|
||||
link=item_link,
|
||||
description=md.convert(row.content),
|
||||
guid=PyRSS2Gen.Guid("%s/%d" % (item_link, row.id)),
|
||||
pubDate=row.published,
|
||||
)
|
||||
)
|
||||
|
||||
rss = PyRSS2Gen.RSS2(
|
||||
title=rss_title,
|
||||
link="%s://%s" % (self._rss_proto, self._site_url),
|
||||
description='Commentaires du site "%s"' % self._site_name,
|
||||
lastBuildDate=datetime.now(),
|
||||
items=items,
|
||||
)
|
||||
rss.write_xml(open(self._rss_file, "w"), encoding="utf-8")
|
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from enum import Enum
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
|
||||
class Template(Enum):
|
||||
DROP_COMMENT = "drop_comment"
|
||||
APPROVE_COMMENT = "approve_comment"
|
||||
NEW_COMMENT = "new_comment"
|
||||
NOTIFY_MESSAGE = "notify_message"
|
||||
RSS_TITLE_MESSAGE = "rss_title_message"
|
||||
|
||||
|
||||
class Templater:
|
||||
def __init__(self, template_path):
|
||||
self._env = Environment(loader=FileSystemLoader(template_path))
|
||||
|
||||
def get_template(self, lang, template: Template):
|
||||
return self._env.get_template(lang + "/" + template.value + ".tpl")
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from stacosys.model.comment import Comment
|
||||
|
||||
|
||||
def find_comment_by_id(id):
|
||||
return Comment.get_by_id(id)
|
||||
|
||||
|
||||
def notify_comment(comment: Comment):
|
||||
comment.notified = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
comment.save()
|
||||
|
||||
|
||||
def publish_comment(comment: Comment):
|
||||
comment.published = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
comment.save()
|
||||
|
||||
|
||||
def delete_comment(comment: Comment):
|
||||
comment.delete_instance()
|
||||
|
||||
|
||||
def find_not_notified_comments():
|
||||
return Comment.select().where(Comment.notified.is_null())
|
||||
|
||||
|
||||
def find_published_comments_by_url(url):
|
||||
return Comment.select(Comment).where((Comment.url == url) & (Comment.published.is_null(False))).order_by(
|
||||
+Comment.published)
|
||||
|
||||
|
||||
def count_published_comments(url):
|
||||
return Comment.select(Comment).where(
|
||||
(Comment.url == url) & (Comment.published.is_null(False))).count() if url else Comment.select(Comment).where(
|
||||
Comment.published.is_null(False)).count()
|
||||
|
||||
|
||||
def create_comment(url, author_name, author_site, author_gravatar, message):
|
||||
created = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
comment = Comment(
|
||||
url=url,
|
||||
author_name=author_name,
|
||||
author_site=author_site,
|
||||
author_gravatar=author_gravatar,
|
||||
content=message,
|
||||
created=created,
|
||||
notified=None,
|
||||
published=None,
|
||||
)
|
||||
comment.save()
|
||||
return comment
|
|
@ -1,24 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from peewee import Model
|
||||
from playhouse.db_url import SqliteDatabase
|
||||
|
||||
db = SqliteDatabase(None)
|
||||
|
||||
|
||||
class BaseModel(Model):
|
||||
class Meta:
|
||||
database = db
|
||||
|
||||
|
||||
class Database:
|
||||
def get_db(self):
|
||||
return db
|
||||
|
||||
def setup(self, db_url):
|
||||
db.init(db_url)
|
||||
db.connect()
|
||||
|
||||
from stacosys.model.comment import Comment
|
||||
db.create_tables([Comment], safe=True)
|
|
@ -1,6 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
|
@ -1,67 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from flask_apscheduler import APScheduler
|
||||
from stacosys.interface import app
|
||||
|
||||
|
||||
class JobConfig(object):
|
||||
|
||||
JOBS: list = []
|
||||
|
||||
SCHEDULER_EXECUTORS = {"default": {"type": "threadpool", "max_workers": 4}}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
imap_polling_seconds,
|
||||
new_comment_polling_seconds,
|
||||
lang,
|
||||
site_name,
|
||||
site_token,
|
||||
site_admin_email,
|
||||
mailer,
|
||||
rss,
|
||||
):
|
||||
self.JOBS = [
|
||||
{
|
||||
"id": "fetch_mail",
|
||||
"func": "stacosys.core.cron:fetch_mail_answers",
|
||||
"args": [lang, mailer, rss, site_token],
|
||||
"trigger": "interval",
|
||||
"seconds": imap_polling_seconds,
|
||||
},
|
||||
{
|
||||
"id": "submit_new_comment",
|
||||
"func": "stacosys.core.cron:submit_new_comment",
|
||||
"args": [lang, site_name, site_token, site_admin_email, mailer],
|
||||
"trigger": "interval",
|
||||
"seconds": new_comment_polling_seconds,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def configure(
|
||||
imap_polling,
|
||||
comment_polling,
|
||||
lang,
|
||||
site_name,
|
||||
site_token,
|
||||
site_admin_email,
|
||||
mailer,
|
||||
rss,
|
||||
):
|
||||
app.config.from_object(
|
||||
JobConfig(
|
||||
imap_polling,
|
||||
comment_polling,
|
||||
lang,
|
||||
site_name,
|
||||
site_token,
|
||||
site_admin_email,
|
||||
mailer,
|
||||
rss,
|
||||
)
|
||||
)
|
||||
scheduler = APScheduler()
|
||||
scheduler.init_app(app)
|
||||
scheduler.start()
|
|
@ -1,19 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from peewee import CharField
|
||||
from peewee import DateTimeField
|
||||
from peewee import TextField
|
||||
|
||||
from stacosys.db.database import BaseModel
|
||||
|
||||
|
||||
class Comment(BaseModel):
|
||||
url = CharField()
|
||||
created = DateTimeField()
|
||||
notified = DateTimeField(null=True, default=None)
|
||||
published = DateTimeField(null=True, default=None)
|
||||
author_name = CharField()
|
||||
author_site = CharField(default="")
|
||||
author_gravatar = CharField(default="")
|
||||
content = TextField()
|
|
@ -1,32 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
|
||||
@dataclass
|
||||
class Part:
|
||||
content: str
|
||||
content_type: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Attachment:
|
||||
filename: str
|
||||
content: str
|
||||
content_type: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Email:
|
||||
id: int
|
||||
encoding: str
|
||||
date: datetime
|
||||
from_addr: str
|
||||
to_addr: str
|
||||
subject: str
|
||||
parts: List[Part]
|
||||
attachments: List[Attachment]
|
||||
plain_text_content: str
|
|
@ -1,9 +0,0 @@
|
|||
Hi,
|
||||
|
||||
The comment should be published soon. It has been approved.
|
||||
|
||||
--
|
||||
Stacosys
|
||||
|
||||
|
||||
{{ original }}
|
|
@ -1,9 +0,0 @@
|
|||
Hi,
|
||||
|
||||
The comment will not be published. It has been dropped.
|
||||
|
||||
--
|
||||
Stacosys
|
||||
|
||||
|
||||
{{ original }}
|
|
@ -1,16 +0,0 @@
|
|||
Hi,
|
||||
|
||||
A new comment has been submitted for post {{ url }}
|
||||
|
||||
You have two choices:
|
||||
- reject the comment by replying NO (or no),
|
||||
- accept the comment by sending back the email as it is.
|
||||
|
||||
If you choose the latter option, Stacosys is going to publish the commennt.
|
||||
|
||||
Please find comment details below:
|
||||
|
||||
{{ comment }}
|
||||
|
||||
--
|
||||
Stacosys
|
|
@ -1 +0,0 @@
|
|||
New comment
|
|
@ -1 +0,0 @@
|
|||
{{ site }} : comments
|
|
@ -1,9 +0,0 @@
|
|||
Bonjour,
|
||||
|
||||
Le commentaire sera bientôt publié. Il a été approuvé.
|
||||
|
||||
--
|
||||
Stacosys
|
||||
|
||||
|
||||
{{ original }}
|
|
@ -1,9 +0,0 @@
|
|||
Bonjour,
|
||||
|
||||
Le commentaire ne sera pas publié. Il a été rejeté.
|
||||
|
||||
--
|
||||
Stacosys
|
||||
|
||||
|
||||
{{ original }}
|
|
@ -1,16 +0,0 @@
|
|||
Bonjour,
|
||||
|
||||
Un nouveau commentaire a été posté pour l'article {{ url }}
|
||||
|
||||
Vous avez deux réponses possibles :
|
||||
- rejeter le commentaire en répondant NO (ou no),
|
||||
- accepter le commentaire en renvoyant cet email tel quel.
|
||||
|
||||
Si cette dernière option est choisie, Stacosys publiera le commentaire très bientôt.
|
||||
|
||||
Voici les détails concernant le commentaire :
|
||||
|
||||
{{ comment }}
|
||||
|
||||
--
|
||||
Stacosys
|
|
@ -1 +0,0 @@
|
|||
Nouveau commentaire
|
|
@ -1 +0,0 @@
|
|||
{{ site }} : commentaires
|
1
tests-badge.svg
Normal file
1
tests-badge.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="20" role="img" aria-label="tests: 19"><title>tests: 19</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="60" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="23" height="20" fill="#4c1"/><rect width="60" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="130">19</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="130">19</text></g></svg>
|
After Width: | Height: | Size: 1 KiB |
63
tests/test_api.py
Normal file
63
tests/test_api.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from stacosys.db import dao, database
|
||||
from stacosys.interface import api, app
|
||||
|
||||
|
||||
def init_test_db():
|
||||
c1 = dao.create_comment("/site1", "Bob", "/bob.site", "", "comment 1")
|
||||
c2 = dao.create_comment("/site2", "Bill", "/bill.site", "", "comment 2")
|
||||
c3 = dao.create_comment("/site3", "Jack", "/jack.site", "", "comment 3")
|
||||
dao.publish_comment(c1)
|
||||
dao.publish_comment(c3)
|
||||
assert c2
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
logger = logging.getLogger(__name__)
|
||||
database.configure("sqlite:memory://db.sqlite")
|
||||
init_test_db()
|
||||
logger.info(f"start interface {api}")
|
||||
return app.test_client()
|
||||
|
||||
|
||||
def test_api_ping(client):
|
||||
resp = client.get("/api/ping")
|
||||
assert resp.data == b"OK"
|
||||
|
||||
|
||||
def test_api_count_global(client):
|
||||
resp = client.get("/api/comments/count")
|
||||
d = json.loads(resp.data)
|
||||
assert d and d["count"] == 2
|
||||
|
||||
|
||||
def test_api_count_url(client):
|
||||
resp = client.get("/api/comments/count?url=/site1")
|
||||
d = json.loads(resp.data)
|
||||
assert d and d["count"] == 1
|
||||
resp = client.get("/api/comments/count?url=/site2")
|
||||
d = json.loads(resp.data)
|
||||
assert d and d["count"] == 0
|
||||
|
||||
|
||||
def test_api_comment(client):
|
||||
resp = client.get("/api/comments?url=/site1")
|
||||
d = json.loads(resp.data)
|
||||
assert d and len(d["data"]) == 1
|
||||
comment = d["data"][0]
|
||||
assert comment["author"] == "Bob"
|
||||
assert comment["content"] == "comment 1"
|
||||
|
||||
|
||||
def test_api_comment_not_found(client):
|
||||
resp = client.get("/api/comments?url=/site2")
|
||||
d = json.loads(resp.data)
|
||||
assert d and d["data"] == []
|
|
@ -1,48 +1,48 @@
|
|||
#!/usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import unittest
|
||||
import pytest
|
||||
|
||||
from stacosys.conf.config import Config, ConfigParameter
|
||||
from stacosys.service.configuration import Config, ConfigParameter
|
||||
|
||||
EXPECTED_DB_SQLITE_FILE = "db.sqlite"
|
||||
EXPECTED_DB = "sqlite://db.sqlite"
|
||||
EXPECTED_HTTP_PORT = 8080
|
||||
EXPECTED_IMAP_PORT = "5000"
|
||||
EXPECTED_IMAP_LOGIN = "user"
|
||||
EXPECTED_LANG = "fr"
|
||||
|
||||
config = Config()
|
||||
|
||||
|
||||
class ConfigTestCase(unittest.TestCase):
|
||||
@pytest.fixture
|
||||
def init_config():
|
||||
config.put(ConfigParameter.DB, EXPECTED_DB)
|
||||
config.put(ConfigParameter.HTTP_PORT, EXPECTED_HTTP_PORT)
|
||||
|
||||
def setUp(self):
|
||||
self.conf = Config()
|
||||
self.conf.put(ConfigParameter.DB_SQLITE_FILE, EXPECTED_DB_SQLITE_FILE)
|
||||
self.conf.put(ConfigParameter.HTTP_PORT, EXPECTED_HTTP_PORT)
|
||||
self.conf.put(ConfigParameter.IMAP_PORT, EXPECTED_IMAP_PORT)
|
||||
self.conf.put(ConfigParameter.SMTP_STARTTLS, "yes")
|
||||
self.conf.put(ConfigParameter.IMAP_SSL, "false")
|
||||
|
||||
def test_exists(self):
|
||||
self.assertTrue(self.conf.exists(ConfigParameter.DB_SQLITE_FILE))
|
||||
self.assertFalse(self.conf.exists(ConfigParameter.IMAP_HOST))
|
||||
def test_split_key():
|
||||
section, param = config._split_key(ConfigParameter.HTTP_PORT)
|
||||
assert section == "http" and param == "port"
|
||||
|
||||
def test_get(self):
|
||||
self.assertEqual(self.conf.get(ConfigParameter.DB_SQLITE_FILE), EXPECTED_DB_SQLITE_FILE)
|
||||
self.assertEqual(self.conf.get(ConfigParameter.HTTP_PORT), EXPECTED_HTTP_PORT)
|
||||
self.assertIsNone(self.conf.get(ConfigParameter.HTTP_HOST))
|
||||
self.assertEqual(self.conf.get(ConfigParameter.HTTP_PORT), EXPECTED_HTTP_PORT)
|
||||
self.assertEqual(self.conf.get(ConfigParameter.IMAP_PORT), EXPECTED_IMAP_PORT)
|
||||
self.assertEqual(self.conf.get_int(ConfigParameter.IMAP_PORT), int(EXPECTED_IMAP_PORT))
|
||||
self.assertEqual(self.conf.get_int(ConfigParameter.HTTP_PORT), 8080)
|
||||
self.assertTrue(self.conf.get_bool(ConfigParameter.SMTP_STARTTLS))
|
||||
self.assertFalse(self.conf.get_bool(ConfigParameter.IMAP_SSL))
|
||||
try:
|
||||
self.conf.get_bool(ConfigParameter.DB_SQLITE_FILE)
|
||||
self.assertTrue(False)
|
||||
except AssertionError:
|
||||
pass
|
||||
|
||||
def test_put(self):
|
||||
self.assertFalse(self.conf.exists(ConfigParameter.IMAP_LOGIN))
|
||||
self.conf.put(ConfigParameter.IMAP_LOGIN, EXPECTED_IMAP_LOGIN)
|
||||
self.assertTrue(self.conf.exists(ConfigParameter.IMAP_LOGIN))
|
||||
self.assertEqual(self.conf.get(ConfigParameter.IMAP_LOGIN), EXPECTED_IMAP_LOGIN)
|
||||
def test_exists(init_config):
|
||||
assert config.exists(ConfigParameter.DB)
|
||||
|
||||
|
||||
def test_get(init_config):
|
||||
assert config.get(ConfigParameter.DB) == EXPECTED_DB
|
||||
assert config.get(ConfigParameter.HTTP_HOST) == ""
|
||||
assert config.get(ConfigParameter.HTTP_PORT) == str(EXPECTED_HTTP_PORT)
|
||||
assert config.get_int(ConfigParameter.HTTP_PORT) == EXPECTED_HTTP_PORT
|
||||
with pytest.raises(AssertionError):
|
||||
config.get_bool(ConfigParameter.DB)
|
||||
|
||||
|
||||
def test_put(init_config):
|
||||
assert not config.exists(ConfigParameter.LANG)
|
||||
config.put(ConfigParameter.LANG, EXPECTED_LANG)
|
||||
assert config.exists(ConfigParameter.LANG)
|
||||
assert config.get(ConfigParameter.LANG) == EXPECTED_LANG
|
||||
|
||||
|
||||
def test_check(init_config):
|
||||
success, error = config.check()
|
||||
assert not success and error
|
||||
|
|
169
tests/test_db.py
169
tests/test_db.py
|
@ -1,55 +1,124 @@
|
|||
import unittest
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from stacosys.db import dao
|
||||
from stacosys.db import database
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from stacosys.db import dao, database
|
||||
from stacosys.model.comment import Comment
|
||||
|
||||
|
||||
class DbTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
db = database.Database()
|
||||
db.setup(":memory:")
|
||||
|
||||
def test_dao_published(self):
|
||||
|
||||
# test count published
|
||||
self.assertEqual(0, dao.count_published_comments(""))
|
||||
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
|
||||
self.assertEqual(0, dao.count_published_comments(""))
|
||||
dao.publish_comment(c1)
|
||||
self.assertEqual(1, dao.count_published_comments(""))
|
||||
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
|
||||
dao.publish_comment(c2)
|
||||
self.assertEqual(2, dao.count_published_comments(""))
|
||||
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
|
||||
dao.publish_comment(c3)
|
||||
self.assertEqual(1, dao.count_published_comments("/post1"))
|
||||
self.assertEqual(2, dao.count_published_comments("/post2"))
|
||||
|
||||
# test find published
|
||||
self.assertEqual(0, len(dao.find_published_comments_by_url("/")))
|
||||
self.assertEqual(1, len(dao.find_published_comments_by_url("/post1")))
|
||||
self.assertEqual(2, len(dao.find_published_comments_by_url("/post2")))
|
||||
|
||||
dao.delete_comment(c1)
|
||||
self.assertEqual(0, len(dao.find_published_comments_by_url("/post1")))
|
||||
|
||||
def test_dao_notified(self):
|
||||
|
||||
# test count notified
|
||||
self.assertEqual(0, len(dao.find_not_notified_comments()))
|
||||
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
|
||||
self.assertEqual(1, len(dao.find_not_notified_comments()))
|
||||
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
|
||||
self.assertEqual(2, len(dao.find_not_notified_comments()))
|
||||
dao.notify_comment(c1)
|
||||
dao.notify_comment(c2)
|
||||
self.assertEqual(0, len(dao.find_not_notified_comments()))
|
||||
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
|
||||
self.assertEqual(1, len(dao.find_not_notified_comments()))
|
||||
dao.notify_comment(c3)
|
||||
self.assertEqual(0, len(dao.find_not_notified_comments()))
|
||||
@pytest.fixture
|
||||
def setup_db():
|
||||
database.configure("sqlite:memory://db.sqlite")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
def equals_comment(comment: Comment, other):
|
||||
return (
|
||||
comment.id == other.id
|
||||
and comment.author_gravatar == other.author_gravatar
|
||||
and comment.author_name == other.author_name
|
||||
and comment.author_site == other.author_site
|
||||
and comment.content == other.content
|
||||
and comment.created == other.created
|
||||
and comment.notified == other.notified
|
||||
and comment.published == other.published
|
||||
)
|
||||
|
||||
|
||||
def test_find_comment_by_id(setup_db):
|
||||
assert dao.find_comment_by_id(1) is None
|
||||
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
|
||||
assert c1.id is not None
|
||||
find_c1 = dao.find_comment_by_id(c1.id)
|
||||
assert find_c1
|
||||
assert equals_comment(c1, find_c1)
|
||||
c1.id = find_c1.id
|
||||
dao.delete_comment(c1)
|
||||
assert dao.find_comment_by_id(c1.id) is None
|
||||
|
||||
|
||||
def test_dao_published(setup_db):
|
||||
assert 0 == dao.count_published_comments("")
|
||||
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
|
||||
assert 0 == dao.count_published_comments("")
|
||||
assert 1 == len(dao.find_not_published_comments())
|
||||
dao.publish_comment(c1)
|
||||
assert 1 == dao.count_published_comments("")
|
||||
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
|
||||
dao.publish_comment(c2)
|
||||
assert 2 == dao.count_published_comments("")
|
||||
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
|
||||
dao.publish_comment(c3)
|
||||
assert 0 == len(dao.find_not_published_comments())
|
||||
|
||||
# count published
|
||||
assert 1 == dao.count_published_comments("/post1")
|
||||
assert 2 == dao.count_published_comments("/post2")
|
||||
|
||||
# find published
|
||||
assert 0 == len(dao.find_published_comments_by_url("/"))
|
||||
assert 1 == len(dao.find_published_comments_by_url("/post1"))
|
||||
assert 2 == len(dao.find_published_comments_by_url("/post2"))
|
||||
|
||||
|
||||
def test_dao_notified(setup_db):
|
||||
assert 0 == len(dao.find_not_notified_comments())
|
||||
c1 = dao.create_comment("/post1", "Yax", "", "", "Comment 1")
|
||||
assert 1 == len(dao.find_not_notified_comments())
|
||||
c2 = dao.create_comment("/post2", "Yax", "", "", "Comment 2")
|
||||
assert 2 == len(dao.find_not_notified_comments())
|
||||
dao.notify_comment(c1)
|
||||
dao.notify_comment(c2)
|
||||
assert 0 == len(dao.find_not_notified_comments())
|
||||
c3 = dao.create_comment("/post2", "Yax", "", "", "Comment 3")
|
||||
assert 1 == len(dao.find_not_notified_comments())
|
||||
dao.notify_comment(c3)
|
||||
assert 0 == len(dao.find_not_notified_comments())
|
||||
|
||||
|
||||
def create_comment(url, author_name, content):
|
||||
return dao.create_comment(url, author_name, "", "", content)
|
||||
|
||||
|
||||
def test_find_recent_published_comments(setup_db):
|
||||
comments = [
|
||||
create_comment("/post", "Adam", "Comment 1"),
|
||||
create_comment("/post", "Arf", "Comment 2"),
|
||||
create_comment("/post", "Arwin", "Comment 3"),
|
||||
create_comment("/post", "Bill", "Comment 4"),
|
||||
create_comment("/post", "Bo", "Comment 5"),
|
||||
create_comment("/post", "Charles", "Comment 6"),
|
||||
create_comment("/post", "Dan", "Comment 7"),
|
||||
create_comment("/post", "Dwayne", "Comment 8"),
|
||||
create_comment("/post", "Erl", "Comment 9"),
|
||||
create_comment("/post", "Jay", "Comment 10"),
|
||||
create_comment("/post", "Kenny", "Comment 11"),
|
||||
create_comment("/post", "Lord", "Comment 12"),
|
||||
]
|
||||
|
||||
rows = dao.find_recent_published_comments()
|
||||
assert len(rows) == 0
|
||||
|
||||
# publish every second
|
||||
for comment in comments:
|
||||
dao.publish_comment(comment)
|
||||
time.sleep(1)
|
||||
|
||||
rows = dao.find_recent_published_comments()
|
||||
assert len(rows) == 10
|
||||
|
||||
authors = [row.author_name for row in rows]
|
||||
assert authors == [
|
||||
"Lord",
|
||||
"Kenny",
|
||||
"Jay",
|
||||
"Erl",
|
||||
"Dwayne",
|
||||
"Dan",
|
||||
"Charles",
|
||||
"Bo",
|
||||
"Bill",
|
||||
"Arwin",
|
||||
]
|
||||
|
|
|
@ -1,22 +1,48 @@
|
|||
import unittest
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from stacosys.interface import form
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from stacosys.db import database
|
||||
from stacosys.interface import app, form
|
||||
from stacosys.service.configuration import Config
|
||||
from stacosys.service.mail import Mailer
|
||||
from stacosys.service.rssfeed import Rss
|
||||
|
||||
|
||||
class FormInterfaceTestCase(unittest.TestCase):
|
||||
|
||||
def test_check_form_data_ok(self):
|
||||
d = {"url": "/", "message": "", "site": "", "remarque": "", "author": "", "token": "", "email": ""}
|
||||
self.assertTrue(form.check_form_data(d))
|
||||
d = {"url": "/"}
|
||||
self.assertTrue(form.check_form_data(d))
|
||||
d = {}
|
||||
self.assertTrue(form.check_form_data(d))
|
||||
|
||||
def test_check_form_data_ko(self):
|
||||
d = {"url": "/", "message": "", "site": "", "remarque": "", "author": "", "token": "", "email": "", "bonus": ""}
|
||||
self.assertFalse(form.check_form_data(d))
|
||||
@pytest.fixture
|
||||
def client():
|
||||
logger = logging.getLogger(__name__)
|
||||
database.configure("sqlite:memory://db.sqlite")
|
||||
logger.info(f"start interface {form}")
|
||||
app.config["CONFIG"] = Config()
|
||||
app.config["MAILER"] = Mailer()
|
||||
app.config["RSS"] = Rss()
|
||||
return app.test_client()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
def test_new_comment_honeypot(client):
|
||||
resp = client.post(
|
||||
"/newcomment", content_type="multipart/form-data", data={"remarque": "trapped"}
|
||||
)
|
||||
assert resp.status == "400 BAD REQUEST"
|
||||
|
||||
|
||||
def test_new_comment_success(client):
|
||||
resp = client.post(
|
||||
"/newcomment",
|
||||
content_type="multipart/form-data",
|
||||
data={"author": "Jack", "url": "/site3", "message": "comment 3"},
|
||||
)
|
||||
assert resp.status == "302 FOUND"
|
||||
|
||||
|
||||
def test_check_form_data():
|
||||
from stacosys.interface.form import check_form_data
|
||||
|
||||
assert check_form_data({"author": "Jack", "url": "/site3", "message": "comment 3"})
|
||||
assert not check_form_data(
|
||||
{"author": "Jack", "url": "/site3", "message": "comment 3", "extra": "ball"}
|
||||
)
|
||||
|
|
14
tests/test_mail.py
Normal file
14
tests/test_mail.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import pytest
|
||||
|
||||
from stacosys.service.mail import Mailer
|
||||
|
||||
|
||||
def test_configure_and_check():
|
||||
mailer = Mailer()
|
||||
mailer.configure_smtp("localhost", 2525, "admin", "admin")
|
||||
mailer.configure_destination("admin@mydomain.com")
|
||||
with pytest.raises(ConnectionRefusedError):
|
||||
mailer.check()
|
9
tests/test_rssfeed.py
Normal file
9
tests/test_rssfeed.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from stacosys.service.rssfeed import Rss
|
||||
|
||||
|
||||
def test_configure():
|
||||
rss = Rss()
|
||||
rss.configure("comments.xml", "blog", "http", "blog.mydomain.com")
|
|
@ -1,9 +0,0 @@
|
|||
import unittest
|
||||
|
||||
from stacosys import __version__
|
||||
|
||||
|
||||
class StacosysTestCase(unittest.TestCase):
|
||||
|
||||
def test_version(self):
|
||||
self.assertEqual("2.0", __version__)
|
|
@ -1,52 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from stacosys.core.templater import Templater, Template
|
||||
|
||||
|
||||
class TemplateTestCase(unittest.TestCase):
|
||||
|
||||
def get_template_content(self, lang, template_name, **kwargs):
|
||||
current_path = os.path.dirname(__file__)
|
||||
template_path = os.path.abspath(os.path.join(current_path, "../stacosys/templates"))
|
||||
template = Templater(template_path).get_template(lang, template_name)
|
||||
return template.render(kwargs)
|
||||
|
||||
def test_approve_comment(self):
|
||||
content = self.get_template_content("fr", Template.APPROVE_COMMENT, original="[texte]")
|
||||
self.assertTrue(content.startswith("Bonjour,\n\nLe commentaire sera bientôt publié."))
|
||||
self.assertTrue(content.endswith("[texte]"))
|
||||
content = self.get_template_content("en", Template.APPROVE_COMMENT, original="[texte]")
|
||||
self.assertTrue(content.startswith("Hi,\n\nThe comment should be published soon."))
|
||||
self.assertTrue(content.endswith("[texte]"))
|
||||
|
||||
def test_drop_comment(self):
|
||||
content = self.get_template_content("fr", Template.DROP_COMMENT, original="[texte]")
|
||||
self.assertTrue(content.startswith("Bonjour,\n\nLe commentaire ne sera pas publié."))
|
||||
self.assertTrue(content.endswith("[texte]"))
|
||||
content = self.get_template_content("en", Template.DROP_COMMENT, original="[texte]")
|
||||
self.assertTrue(content.startswith("Hi,\n\nThe comment will not be published."))
|
||||
self.assertTrue(content.endswith("[texte]"))
|
||||
|
||||
def test_new_comment(self):
|
||||
content = self.get_template_content("fr", Template.NEW_COMMENT, comment="[comment]")
|
||||
self.assertTrue(content.startswith("Bonjour,\n\nUn nouveau commentaire a été posté"))
|
||||
self.assertTrue(content.endswith("[comment]\n\n--\nStacosys"))
|
||||
content = self.get_template_content("en", Template.NEW_COMMENT, comment="[comment]")
|
||||
self.assertTrue(content.startswith("Hi,\n\nA new comment has been submitted"))
|
||||
self.assertTrue(content.endswith("[comment]\n\n--\nStacosys"))
|
||||
|
||||
def test_notify_message(self):
|
||||
content = self.get_template_content("fr", Template.NOTIFY_MESSAGE)
|
||||
self.assertEqual("Nouveau commentaire", content)
|
||||
content = self.get_template_content("en", Template.NOTIFY_MESSAGE)
|
||||
self.assertEqual("New comment", content)
|
||||
|
||||
def test_rss_title(self):
|
||||
content = self.get_template_content("fr", Template.RSS_TITLE_MESSAGE, site="[site]")
|
||||
self.assertEqual("[site] : commentaires", content)
|
||||
content = self.get_template_content("en", Template.RSS_TITLE_MESSAGE, site="[site]")
|
||||
self.assertEqual("[site] : comments", content)
|
567
uv.lock
generated
Normal file
567
uv.lock
generated
Normal file
|
@ -0,0 +1,567 @@
|
|||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.13.1"
|
||||
|
||||
[[package]]
|
||||
name = "astroid"
|
||||
version = "3.3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/40/e028137cb19ed577001c76b91c5c50fee5a9c85099f45820b69385574ac5/astroid-3.3.6.tar.gz", hash = "sha256:6aaea045f938c735ead292204afdb977a36e989522b7833ef6fea94de743f442", size = 397452 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d2/82c8ccef22ea873a2b0da9636e47d45137eeeb2fb9320c5dbbdd3627bab0/astroid-3.3.6-py3-none-any.whl", hash = "sha256:db676dc4f3ae6bfe31cda227dc60e03438378d7a896aec57422c95634e8d722f", size = 274644 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "background"
|
||||
version = "0.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/41/d6122c8e4bb280b2182098d77554d00016b6ffe54201cd3fac7f52fe9df2/background-0.2.1.tar.gz", hash = "sha256:4a5ed40b4a2a9f3340b1402862725d35016dc2490f95d89a2de47c3ddf215b91", size = 3141 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/45/01a33c692ce9f22214cad440f34704ed74e56b6f21d90e71aa595b3c2b72/background-0.2.1-py3-none-any.whl", hash = "sha256:c230e2813c773f93ecae54281ce6b1b425c895c24599cc203b7f137e4d7c4802", size = 2209 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "24.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.8.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.6.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/d2/c25011f4d036cf7e8acbbee07a8e09e9018390aee25ba085596c4b83d510/coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d", size = 801710 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/35/26/9abab6539d2191dbda2ce8c97b67d74cbfc966cc5b25abb880ffc7c459bc/coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664", size = 207356 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/da/d49f19402240c93453f606e660a6676a2a1fbbaa6870cc23207790aa9697/coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c", size = 207614 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e6/93bb9bf85497816082ec8da6124c25efa2052bd4c887dd3b317b91990c9e/coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014", size = 240129 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/65/6a824b9406fe066835c1274a9949e06f084d3e605eb1a602727a27ec2fe3/coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00", size = 237276 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/79/6c7a800913a9dd23ac8c8da133ebb556771a5a3d4df36b46767b1baffd35/coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d", size = 239267 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/e7/834d530293fdc8a63ba8ff70033d5182022e569eceb9aec7fc716b678a39/coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a", size = 238887 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/05/ec9d6080852984f7163c96984444e7cd98b338fd045b191064f943ee1c08/coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077", size = 236970 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/d8/775937670b93156aec29f694ce37f56214ed7597e1a75b4083ee4c32121c/coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb", size = 238831 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/58/88551cb7fdd5ec98cb6044e8814e38583436b14040a5ece15349c44c8f7c/coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba", size = 210000 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/12/cfbf49b95120872785ff8d56ab1c7fe3970a65e35010c311d7dd35c5fd00/coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1", size = 210753 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/68/c1cb31445599b04bde21cbbaa6d21b47c5823cdfef99eae470dfce49c35a/coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419", size = 208091 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/73/84b02c6b19c4a11eb2d5b5eabe926fb26c21c080e0852f5e5a4f01165f9e/coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a", size = 208369 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/e0/ae5d878b72ff26df2e994a5c5b1c1f6a7507d976b23beecb1ed4c85411ef/coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4", size = 251089 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/9c/0aaac011aef95a93ef3cb2fba3fde30bc7e68a6635199ed469b1f5ea355a/coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae", size = 246806 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/19/4d5d3ae66938a7dcb2f58cef3fa5386f838f469575b0bb568c8cc9e3a33d/coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030", size = 249164 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/0b/4ee8a7821f682af9ad440ae3c1e379da89a998883271f088102d7ca2473d/coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be", size = 248642 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/12/36ff1d52be18a16b4700f561852e7afd8df56363a5edcfb04cf26a0e19e0/coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e", size = 246516 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/d0/8e258f6c3a527c1655602f4f576215e055ac704de2d101710a71a2affac2/coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9", size = 247783 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/0d/1e4a48d289429d38aae3babdfcadbf35ca36bdcf3efc8f09b550a845bdb5/coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b", size = 210646 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/74/b0729f196f328ac55e42b1e22ec2f16d8bcafe4b8158a26ec9f1cdd1d93e/coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611", size = 211815 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coveralls"
|
||||
version = "4.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coverage" },
|
||||
{ name = "docopt" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/61/75/a454fb443eb6a053833f61603a432ffbd7dd6ae53a11159bacfadb9d6219/coveralls-4.0.1.tar.gz", hash = "sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69", size = 12419 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/e5/6708c75e2a4cfca929302d4d9b53b862c6dc65bd75e6933ea3d20016d41d/coveralls-4.0.1-py3-none-any.whl", hash = "sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809", size = 13599 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defusedxml"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dill"
|
||||
version = "0.3.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/70/43/86fe3f9e130c4137b0f1b50784dd70a5087b911fe07fa81e53e0c4c47fea/dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c", size = 187000 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a", size = 119418 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docopt"
|
||||
version = "0.6.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901 }
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "blinker" },
|
||||
{ name = "click" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/50/dff6380f1c7f84135484e176e0cac8690af72fa90e932ad2a0a60e28c69b/flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", size = 680824 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136", size = 102979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "genbadge"
|
||||
version = "1.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "pillow" },
|
||||
{ name = "requests" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/2b/75c50383f951f36334635715819f89d1b4dae1de0ff7d510970bbf137994/genbadge-1.1.2.tar.gz", hash = "sha256:987ed2feaf6e9cc2850fc3883320d8706b3849eb6c9f436156254dcac438515c", size = 137188 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/5e/91f2340d7a51ce0b7a59e5caa1cccd61131d8d5163cc02f3563c819cb49c/genbadge-1.1.2-py2.py3-none-any.whl", hash = "sha256:4e3073cb56c2745fbef4b7da97eb85b28a18a22af519b66acb6706b6546279f1", size = 100945 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "3.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mccabe"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "11.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydal"
|
||||
version = "20241204.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/78/7ddf9aacea5cd2e63423d278d26465c63ecdae87cf1c503d8fc1f7dfcfa5/pydal-20241204.1.tar.gz", hash = "sha256:1ba1f9e528b985e234f5b3acfd9d549998b44f7ed7ae747b9e8d4ad3047bf511", size = 623731 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/de/30f6ee6c8f333a00969fb4d5cd3c8cb8ca69feeeb2518d69b69d9bbe732b/pydal-20241204.1-py2.py3-none-any.whl", hash = "sha256:416f06de17ab0a5340e11195a0583abfe484eceb067cd3ab92208d3dc5aa7683", size = 246873 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pylint"
|
||||
version = "3.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "astroid" },
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "dill" },
|
||||
{ name = "isort" },
|
||||
{ name = "mccabe" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "tomlkit" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/d8/4471b2cb4ad18b4af717918c468209bd2bd5a02c52f60be5ee8a71b5af2c/pylint-3.3.2.tar.gz", hash = "sha256:9ec054ec992cd05ad30a6df1676229739a73f8feeabf3912c995d17601052b01", size = 1516485 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/61/55/5eaf6c415f6ddb09b9b039278823a8e27fb81ea7a34ec80c6d9223b17f2e/pylint-3.3.2-py3-none-any.whl", hash = "sha256:77f068c287d49b8683cd7c6e624243c74f92890f767f106ffa1ddf3c0a54cb7a", size = 521873 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyrss2gen"
|
||||
version = "1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/01/fd610d5fc86f7dbdbefc4baa8f7fe15a2e5484244c41dcf363ca7e89f60c/PyRSS2Gen-1.1.tar.gz", hash = "sha256:7960aed7e998d2482bf58716c316509786f596426f879b05f8d84e98b82c6ee7", size = 6854 }
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "6.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coverage" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "78.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/0db4da3bc908df06e5efae42b44e75c81dd52716e10192ff36d0c1c8e379/setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54", size = 1367827 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/21/f43f0a1fa8b06b32812e0975981f4677d28e0f3271601dc88ac5a5b83220/setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8", size = 1256108 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stacosys"
|
||||
version = "3.4"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "background" },
|
||||
{ name = "defusedxml" },
|
||||
{ name = "flask" },
|
||||
{ name = "genbadge" },
|
||||
{ name = "markdown" },
|
||||
{ name = "pydal" },
|
||||
{ name = "pyrss2gen" },
|
||||
{ name = "requests" },
|
||||
{ name = "types-markdown" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "black" },
|
||||
{ name = "coveralls" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pylint" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cov" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "background", specifier = ">=0.2.1" },
|
||||
{ name = "defusedxml", specifier = ">=0.7.1" },
|
||||
{ name = "flask", specifier = ">=3.1.0" },
|
||||
{ name = "genbadge", specifier = ">=1.1.2" },
|
||||
{ name = "markdown", specifier = ">=3.7" },
|
||||
{ name = "pydal", specifier = ">=20241204.1" },
|
||||
{ name = "pyrss2gen", specifier = ">=1.1" },
|
||||
{ name = "requests", specifier = ">=2.32.3" },
|
||||
{ name = "types-markdown", specifier = ">=3.7.0.20241204" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "black", specifier = ">=24.10.0" },
|
||||
{ name = "coveralls", specifier = ">=4.0.1" },
|
||||
{ name = "mypy", specifier = ">=1.13.0" },
|
||||
{ name = "pylint", specifier = ">=3.3.2" },
|
||||
{ name = "pytest", specifier = ">=8.3.4" },
|
||||
{ name = "pytest-cov", specifier = ">=6.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomlkit"
|
||||
version = "0.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-markdown"
|
||||
version = "3.7.0.20241204"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/3c/874ac6ce93f4e6bd0283a5df2c8065f4e623c6c3bc0b2fb98c098313cb73/types_markdown-3.7.0.20241204.tar.gz", hash = "sha256:ecca2b25cd23163fd28ed5ba34d183d731da03e8a5ed3a20b60daded304c5410", size = 17820 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/26/3c9730e845cfd0d587e0dfa9c1975f02f9f49407afbf30800094bdac0286/types_Markdown-3.7.0.20241204-py3-none-any.whl", hash = "sha256:f96146c367ea9c82bfe9903559d72706555cc2a1a3474c58ebba03b418ab18da", size = 23572 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 },
|
||||
]
|
Loading…
Add table
Reference in a new issue