Compare commits

...

33 Commits
3.0.12 ... main

Author SHA1 Message Date
BJ Dierkes
3527ade7b5
Merge pull request #766 from datafolklabs/feat/python-3.14
feat(dev): Python 3.14 default development target, drop 3.8 support
2025-11-02 20:51:58 -06:00
BJ Dierkes
dad85d287a feat(dev): Python 3.14 default development target, drop 3.8 support 2025-11-02 20:27:38 -06:00
BJ Dierkes
7c347abe43 fix(dev): add requests to dev dependencies
Resolves Issue #765
2025-11-02 18:45:21 -06:00
BJ Dierkes
bfb3b8c01b fix(ext.smtp): fix test related to mailpit api update 2025-11-02 18:42:22 -06:00
BJ Dierkes
3ee6b5157b feat(dev): update devbox 2025-11-02 18:41:48 -06:00
BJ Dierkes
cc857e70a7
Merge pull request #761 from datafolklabs/dep/update-pdm-lock
chore: Update pdm.lock
2025-11-02 17:49:34 -06:00
github-actions[bot]
ac410db146
chore: Update pdm.lock 2025-10-27 03:34:29 +00:00
BJ Dierkes
8b038170d8 feat: add direnv/devbox configurations and fix tests 2025-06-10 01:44:15 -05:00
BJ Dierkes
23b9b95d93 chore: add claude config 2025-06-09 23:32:38 -05:00
BJ Dierkes
9df6b3a3d3
Merge pull request #757 from datafolklabs/feat/github-actions
feat: setup github actions
2025-05-06 12:52:40 -05:00
BJ Dierkes
bd0d5eb878 ci: add minimal permissions for github actions 2025-05-06 12:29:15 -05:00
BJ Dierkes
80da0029ed ci: execute github actions on pull_request 2025-05-06 12:27:05 -05:00
BJ Dierkes
b46ce15833 feat: setup github actions 2025-05-06 12:23:13 -05:00
BJ Dierkes
41f2180976
Merge pull request #756 from sigma67/fix-all-exports
fix __all__
2025-05-06 09:16:11 -05:00
BJ Dierkes
8f5eaa817d chore: bump development version 2025-05-06 09:14:11 -05:00
sigma67
2bc559a30d fix __all__ 2025-05-06 10:07:03 +02:00
BJ Dierkes
c314892fb3 feat: bump version to 3.0.14 2025-05-05 11:30:13 -05:00
BJ Dierkes
822c22a1ff fix(ext_smtp): misc fixes and updates to better support content types
Ref: PR #742
2025-05-05 11:08:35 -05:00
BJ Dierkes
a7d004b82d resolve merge conflict 2025-05-05 08:24:49 -05:00
BJ Dierkes
ae763cf098 Merge branch 'pr753' 2025-05-05 08:07:41 -05:00
BJ Dierkes
2fb2940e60 chore: add contributors 2025-05-05 08:07:06 -05:00
BJ Dierkes
4669c7ad2e chore: update pdm 2025-05-05 08:05:34 -05:00
BJ Dierkes
12e4e62fe9 chore: resolve mypy errors 2025-05-05 08:05:34 -05:00
BJ Dierkes
9d51ed79b7 chore: update pdm 2025-05-05 07:58:39 -05:00
BJ Dierkes
32fe2685f0 chore: resolve mypy errors 2025-05-05 07:54:44 -05:00
blakejameson
ac887016c4 cleaning up some usage of "its" and "it's" 2025-04-14 11:40:44 -05:00
BJ Dierkes
a5a6a081f3 Merge branch 'main' of github.com:datafolklabs/cement 2025-04-13 10:28:09 -05:00
BJ Dierkes
aeb9715247 chore: update pdm deps 2025-04-13 10:27:52 -05:00
BJ Dierkes
67a1cd3030
Merge pull request #750 from sigma67/add-py-typed
add py.typed marker
2025-04-13 09:22:55 -05:00
Benedikt Putz
5314d21a5f add py.typed marker 2025-03-27 12:49:47 +01:00
BJ Dierkes
91953d07da fix(ext_jinja2): refactor hard-coded reference to jinja2 template handler
Issue: #749
2025-03-11 11:54:09 -05:00
BJ Dierkes
d8bd90b925 Bump version to 3.0.13 (dev) 2025-02-19 13:52:51 -06:00
BJ Dierkes
a4ce9e760f Tweak Docker py312 2025-01-21 12:50:29 -06:00
42 changed files with 2960 additions and 388 deletions

14
.envrc Normal file
View File

@ -0,0 +1,14 @@
# Automatically sets up your devbox environment whenever you cd into this
# directory via our direnv integration:
eval "$(devbox generate direnv --print-envrc)"
# check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/
# for more details
source_env_if_exists .envrc.local
export SMTP_HOST=localhost
export SMTP_PORT=1025
export MEMCACHED_HOST=localhost
export REDIS_HOST=localhost

98
.github/workflows/build_and_test.yml vendored Normal file
View File

@ -0,0 +1,98 @@
name: Build & Test
permissions:
contents: read
pull-requests: write
on: [pull_request]
env:
SMTP_HOST: localhost
SMTP_PORT: 1025
MEMCACHED_HOST: localhost
REDIS_HOST: localhost
jobs:
comply:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ConorMacBride/install-package@v1
with:
apt: libmemcached-dev
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
architecture: "x64"
- name: Setup PDM
uses: pdm-project/setup-pdm@v4
- name: Install dependencies
run: pdm install
- name: Make Comply
run: make comply
test:
needs: comply
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ConorMacBride/install-package@v1
with:
apt: libmemcached-dev
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
architecture: "x64"
- uses: hoverkraft-tech/compose-action@v2.0.1
with:
compose-file: "./docker/compose-services-only.yml"
- name: Setup PDM
uses: pdm-project/setup-pdm@v4
- name: Install dependencies
run: pdm install
- name: Make Test
run: make test
test-all:
needs: test
runs-on: ${{ matrix.os }}
strategy:
matrix:
# FIXME ?
# os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.10"]
steps:
- uses: actions/checkout@v4
- uses: ConorMacBride/install-package@v1
with:
apt: libmemcached-dev
- uses: hoverkraft-tech/compose-action@v2.0.1
with:
compose-file: "./docker/compose-services-only.yml"
- name: Setup PDM
uses: pdm-project/setup-pdm@v4
- name: Install dependencies
run: pdm install
- name: Make Test
run: make test
cli-smoke-test:
needs: test-all
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hoverkraft-tech/compose-action@v2.0.1
with:
compose-file: "./docker-compose.yml"
- name: CLI Smoke Tests
run: ./scripts/cli-smoke-test.sh
- if: always()
name: Review Output
run: cat ./tmp/cli-smoke-test.out

2
.gitignore vendored
View File

@ -52,7 +52,7 @@ pip-log.txt
# Documentation
doc/build
# Unit test / coverage reports
.coverage
.coverage*
htmlcov
coverage-report
.tox

View File

@ -1,5 +1,55 @@
# ChangeLog
## 3.0.15 - DEVELOPMENT (will be released as stable/3.0.16)
Bugs:
- None
Features:
- None
Refactoring:
- `[dev]` Python 3.14 Default Development Target
- `[dev]` Remove Support for Python 3.8 (EOL)
Misc:
- None
Deprecations:
- None
## 3.0.14 - May 5, 2025
Bugs:
- `[ext_jinja2]` Refactor hard-coded reference to `jinja2` template handler.
- [Issue #749](https://github.com/datafolklabs/cement/issues/749)
- `[ext_smtp]` Misc fixes and updates to better support content types.
- [PR #742](https://github.com/datafolklabs/cement/pull/742)
Features:
- None
Refactoring:
- None
Misc:
- None
Deprecations:
- None
## 3.0.12 - Nov 10, 2024
Bugs:
@ -23,7 +73,7 @@ Refactoring:
- `[dev]` Implement Ruff for Code Compliance (replaces Flake8)
- [Issue #671](https://github.com/datafolklabs/cement/issues/671)
- [PR #681](https://github.com/datafolklabs/cement/pull/681)
- `[dev]` Remove Python 3.5, 3.6, 3.7 Docker Dev Targets
- `[dev]` Remove Python 3.5, 3.6, 3.7 Docker Dev Targets
- `[dev]` Added Python 3.13 Dev Target
- `[dev]` Testing now requires typing compliance (`make test` -> `make comply-mypy`)
- `[dev]` Type Annotations (related: [PR #628](https://github.com/datafolklabs/cement/pull/628))
@ -71,7 +121,7 @@ Refactoring:
Misc:
Misc:
- [cli] Move CLI dependencies to `cement[cli]` extras package, and remove included/nexted `contrib` sources. See note on 'Potential Upgrade Incompatibility'
- [Issue #679](https://github.com/datafolklabs/cement/issues/679)
@ -104,7 +154,7 @@ pip install cement[cli]
## 3.0.10 - Feb 28, 2024
Bugs:
Bugs:
- `[ext.logging]` Support `logging.propagate` to avoid duplicate log entries
- [Issue #310](https://github.com/datafolklabs/cement/issues/310)
@ -126,7 +176,7 @@ Features:
- [PR #669](https://github.com/datafolklabs/cement/pull/669)
Refactoring:
Refactoring:
- `[core.plugin]` Deprecate the use of `imp` in favor of `importlib`
- [Issue #386](https://github.com/datafolklabs/cement/issues/386)
@ -205,7 +255,7 @@ Bugs:
- `[ext.argparse]` Parser (`self._parser`) not accessible inside `_pre_argument_parsing` when `stacked_type = 'embedded'`
- [Issue #569](https://github.com/datafolklabs/cement/issues/569)
- `[ext.configparser]` Overriding config options with environment variables doesn't work correctly with surrounding underscore characters
- `[ext.configparser]` Overriding config options with environment variables doesn't work correctly with surrounding underscore characters
- [Issue #590](https://github.com/datafolklabs/cement/issues/590)
- `[utils.fs]` Fix bug where trailing slash was not removed in `fs.backup()` of a directory.
- [Issue #610](https://github.com/datafolklabs/cement/issues/610)

78
CLAUDE.md Normal file
View File

@ -0,0 +1,78 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
**Testing and Compliance:**
- `make test` - Run full test suite with coverage and PEP8 compliance
- `make test-core` - Run only core library tests
- `make comply` - Run both ruff and mypy compliance checks
- `make comply-ruff` - Run ruff linting
- `make comply-ruff-fix` - Auto-fix ruff issues
- `make comply-mypy` - Run mypy type checking
- `pdm run pytest --cov=cement tests/` - Direct pytest execution
- `pdm run pytest --cov=cement.core tests/core` - Test only core components
**Development Environment:**
- `pdm venv create && pdm install` - Set up local development environment
- `pdm run cement --help` - Run the cement CLI
**Documentation:**
- `make docs` - Build Sphinx documentation
**Build and Distribution:**
- `pdm build` - Build distribution packages
## Architecture Overview
Cement is a CLI application framework built around a handler/interface pattern with the following core concepts:
**Core Application (`cement.core.foundation.App`):**
- The main `App` class in `cement/core/foundation.py` is the central orchestrator
- Uses a Meta class pattern for configuration
- Manages lifecycle through setup(), run(), and close() methods
- Supports signal handling and application reloading
**Handler System:**
- Interface/Handler pattern where interfaces define contracts and handlers provide implementations
- Core handlers: arg, config, log, output, cache, controller, extension, plugin, template
- Handlers are registered and resolved through `HandlerManager`
- Located in `cement/core/` with corresponding modules (arg.py, config.py, etc.)
**Extensions System:**
- Extensions in `cement/ext/` provide additional functionality
- Examples: ext_yaml.py, ext_jinja2.py, ext_argparse.py, etc.
- Optional dependencies managed through pyproject.toml extras
**CLI Structure:**
- Main CLI application in `cement/cli/main.py`
- Uses CementApp class that extends core App
- Includes code generation templates in `cement/cli/templates/`
**Controllers:**
- MVC-style controllers handle command routing
- Base controller pattern in controllers/base.py files
- Support nested sub-commands and argument parsing
## Key Development Practices
- 100% test coverage required (pytest with coverage reporting)
- 100% PEP8 compliance enforced via ruff
- Type annotation compliance via mypy
- PDM for dependency management
- Zero external dependencies for core framework (optional for extensions)
## Testing Notes
- Tests located in `tests/` directory mirroring source structure
- Core tests can run independently via `make test-core`
- Coverage reports generated in `coverage-report/` directory
## Extension Development
When working with extensions:
- Check `cement/ext/` for existing extension patterns
- Optional dependencies declared in pyproject.toml under `[project.optional-dependencies]`
- Extensions follow naming pattern `ext_<name>.py`
- Must implement proper interface contracts

View File

@ -22,3 +22,4 @@ documentation, or testing:
- Mudassir Chapra (muddi900)
- Christian Hengl (rednar)
- sigma67
- Blake Jameson (blakejameson)

View File

@ -1,4 +1,4 @@
FROM python:3.13-alpine
FROM python:3.14-alpine
LABEL MAINTAINER="BJ Dierkes <derks@datafolklabs.com>"
ENV PS1="\[\e[0;33m\]|> cement <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# "
ENV PATH="${PATH}:/root/.local/bin"

View File

@ -3,11 +3,11 @@
dev:
docker compose up -d
docker compose exec cement pdm install
docker compose exec cement-py38 pdm install
docker compose exec cement-py39 pdm install
docker compose exec cement-py310 pdm install
docker compose exec cement-py311 pdm install
docker compose exec cement-py312 pdm install
docker compose exec cement-py313 pdm install
docker compose exec cement /bin/bash
test: comply

View File

@ -5,9 +5,9 @@
[![Continuous Integration Status](https://app.travis-ci.com/datafolklabs/cement.svg?branch=master)](https://app.travis-ci.com/github/datafolklabs/cement/)
Cement is an advanced Application Framework for Python, with a primary focus on Command Line Interfaces (CLI). Its goal is to introduce a standard, and feature-full platform for both simple and complex command line applications as well as support rapid development needs without sacrificing quality. Cement is flexible, and it's use cases span from the simplicity of a micro-framework to the complexity of a mega-framework. Whether it's a single file script, or a multi-tier application, Cement is the foundation you've been looking for.
Cement is an advanced Application Framework for Python, with a primary focus on Command Line Interfaces (CLI). Its goal is to introduce a standard and feature-full platform for both simple and complex command line applications as well as support rapid development needs without sacrificing quality. Cement is flexible, and its use cases span from the simplicity of a micro-framework to the complexity of a mega-framework. Whether it's a single file script or a multi-tier application, Cement is the foundation you've been looking for.
The first commit to Git was on Dec 4, 2009. Since then, the framework has seen several iterations in design, and has continued to grow and improve since it's inception. Cement is the most stable, and complete framework for command line and backend application development.
The first commit to Git was on Dec 4, 2009. Since then, the framework has seen several iterations in design and has continued to grow and improve since its inception. Cement is the most stable and complete framework for command line and backend application development.
## Installation
@ -42,7 +42,7 @@ Cement core features include (but are not limited to):
- 100% PEP8 compliance (`ruff`)
- Type annotation compliance (`mypy`)
- Extensive API Reference (`sphinx`)
- Tested on Python 3.8+
- Tested on Python 3.9+
## Optional Extensions
@ -92,17 +92,17 @@ All execution is done *inside the docker containers*.
**Testing Alternative Versions of Python**
The latest stable version of Python 3 is the default, and target version accessible as the `cement` container within Docker Compose. For testing against alternative versions of python, additional containers are created (ex: `cement-py38`, `cement-py39`, etc). You can access these containers via:
The latest stable version of Python 3 is the default, and target version accessible as the `cement` container within Docker Compose. For testing against alternative versions of python, additional containers are created (ex: `cement-py39`, `cement-py310`, etc). You can access these containers via:
```
$ docker-compose ps
Name Command State Ports
-------------------------------------------------------------------------
cement_cement-py38_1 /bin/bash Up
cement_cement-py39_1 /bin/bash Up
cement_cement-py310_1 /bin/bash Up
cement_cement-py311_1 /bin/bash Up
cement_cement-py312_1 /bin/bash Up
cement_cement-py313_1 /bin/bash Up
cement_cement_1 /bin/bash Up
cement_memcached_1 docker-entrypoint.sh memcached Up 11211/tcp
cement_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp

View File

@ -10,3 +10,21 @@ from .ext.ext_argparse import expose as ex
from .utils.misc import init_defaults, minimal_logger
from .utils import misc, fs, shell
from .utils.version import get_version
__all__ = [
"App",
"TestApp",
"Interface",
"Handler",
"FrameworkError",
"InterfaceError",
"CaughtSignal",
"Controller",
"ex",
"init_defaults",
"minimal_logger",
"misc",
"fs",
"shell",
"get_version",
]

View File

@ -1,3 +1,3 @@
"""Cement core backend module."""
VERSION = (3, 0, 12, 'final', 0) # pragma: nocover
VERSION = (3, 0, 15, 'final', 0) # pragma: nocover

View File

@ -116,7 +116,7 @@ class ExtensionHandler(ExtensionInterface, Handler):
loaded.
"""
# If its not a full module path then preppend our default path
# If it's not a full module path then preppend our default path
if ext_module.find('.') == -1:
ext_module = f'cement.ext.ext_{ext_module}'

View File

@ -651,10 +651,10 @@ class App(meta.MetaMixin):
extensions. Developers can optionally use the
``App.__import__()`` method to import simple modules, and if
that module exists in this mapping it will import the alternative
library in it's place.
library in its place.
This is a low-level feature, and may not produce the results you are
expecting. It's purpose is to allow the developer to replace specific
This is a low-level feature and may not produce the results you are
expecting. Its purpose is to allow the developer to replace specific
modules at a high level. Example: For an application wanting to use
``ujson`` in place of ``json``, the developer could set the following:

View File

@ -599,7 +599,7 @@ class ArgparseController(ControllerHandler):
elif stacked_type == 'embedded':
# if it's embedded, then just set it to use the same as the
# controller its stacked on
# controller it's stacked on
parents[label] = parents[stacked_on]
parsers[label] = parsers[stacked_on]
contr._parser = parsers[stacked_on]
@ -812,7 +812,7 @@ class ArgparseController(ControllerHandler):
# if no __dispatch__ is set then that means we have hit a
# controller with not sub-command (argparse doesn't support
# default sub-command yet... so we rely on
# __controller_namespace__ and it's default func
# __controller_namespace__ and its default func
# We never get here on Python < 3 as Argparse would have already
# complained about too few arguments

View File

@ -126,7 +126,7 @@ class Environment(object):
os._exit(os.EX_OK)
except OSError as e:
sys.stderr.write("Fork #1 failed: (%d) %s\n" %
(e.errno, e.strerror))
(e.errno, e.strerror)) # type: ignore
sys.exit(1)
# Decouple from parent environment.
@ -142,7 +142,7 @@ class Environment(object):
os._exit(os.EX_OK)
except OSError as e:
sys.stderr.write("Fork #2 failed: (%d) %s\n" %
(e.errno, e.strerror))
(e.errno, e.strerror)) # type: ignore
sys.exit(1)
# Redirect standard file descriptors.

View File

@ -143,7 +143,7 @@ def setup_template_items(app: App) -> None:
if os.path.exists(subpath) and subpath not in template_dirs:
template_dirs.append(subpath)
# use app template module, find it's path on filesystem
# use app template module, find its path on filesystem
if app._meta.template_module is not None:
mod_parts = app._meta.template_module.split('.')
mod_name = mod_parts.pop()

View File

@ -34,6 +34,9 @@ class Jinja2OutputHandler(OutputHandler):
Please see the developer documentation on
:cement:`Output Handling <dev/output>`.
This class has an assumed depency on it's associated Jinja2TemplateHandler.
If sub-classing, you must also sub-class/implement the Jinja2TemplateHandler
and give it the same label.
"""
class Meta(OutputHandler.Meta):
@ -48,7 +51,7 @@ class Jinja2OutputHandler(OutputHandler):
def _setup(self, app: App) -> None:
super(Jinja2OutputHandler, self)._setup(app)
self.templater = self.app.handler.resolve('template', 'jinja2', setup=True) # type: ignore
self.templater = self.app.handler.resolve('template', self._meta.label, setup=True) # type: ignore
def render(self, data: Dict[str, Any], template: str = None, **kw: Any) -> str: # type: ignore
"""

View File

@ -218,7 +218,7 @@ class LoggingLogHandler(log.LogHandler):
else:
console_handler = NullHandler()
# FIXME: self._clear_loggers() should be preventing this but its not!
# FIXME: self._clear_loggers() should be preventing this but it's not!
for i in logging.getLogger(f"cement:app:{namespace}").handlers:
if isinstance(i, logging.StreamHandler):
self.backend.removeHandler(i)
@ -259,7 +259,7 @@ class LoggingLogHandler(log.LogHandler):
else:
file_handler = NullHandler()
# FIXME: self._clear_loggers() should be preventing this but its not!
# FIXME: self._clear_loggers() should be preventing this but it's not!
for i in logging.getLogger(f"cement:app:{namespace}").handlers:
if isinstance(i, file_handler.__class__): # pragma: nocover
self.backend.removeHandler(i) # pragma: nocover

View File

@ -58,7 +58,7 @@ class MemcachedCacheHandler(cache.CacheHandler):
comma-separated string into a list). This function does not return
anything, however it is expected to set the `hosts` value of the
``[cache.memcached]`` section (which is what this extension reads for
it's host configution).
its host configution).
:returns: ``None``

View File

@ -5,7 +5,6 @@ Cement plugin extension module.
from __future__ import annotations
import os
import sys
import importlib
import importlib.util
import importlib.machinery
import re

View File

@ -92,7 +92,7 @@ class RedisCacheHandler(cache.CacheHandler):
if res is None:
return fallback
else:
return res.decode('utf-8') # type: ignore
return res.decode('utf-8')
def set(self, key: str, value: Any, time: Optional[int] = None, **kw: Any) -> None:
"""
@ -128,7 +128,7 @@ class RedisCacheHandler(cache.CacheHandler):
otherwise
"""
res = self.r.delete(key)
return int(res) > 0 # type: ignore
return int(res) > 0
def purge(self, **kw: Any) -> None:
"""
@ -139,7 +139,7 @@ class RedisCacheHandler(cache.CacheHandler):
"""
keys = self.r.keys('*')
if keys:
self.r.delete(*keys) # type: ignore
self.r.delete(*keys)
def load(app: App) -> None:

View File

@ -4,13 +4,17 @@ Cement smtp extension module.
from __future__ import annotations
import os
from datetime import datetime, timezone
import smtplib
from email.header import Header
from email.charset import Charset, BASE64, QP
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email import encoders
from typing import Any, Dict, Union, Tuple, TYPE_CHECKING
from email.utils import format_datetime, make_msgid
from typing import Any, Optional, Dict, Union, Tuple, TYPE_CHECKING
from ..core import mail
from ..utils import fs
from ..utils.misc import minimal_logger, is_true
@ -54,6 +58,14 @@ class SMTPMailHandler(mail.MailHandler):
'username': None,
'password': None,
'files': None,
# define controlling of mail encoding
'charset': 'utf-8',
'header_encoding': None,
'body_encoding': None,
'date_enforce': True,
'msgid_enforce': True,
'msgid_str': None,
'msgid_domain': None,
}
_meta: Meta # type: ignore
@ -62,17 +74,33 @@ class SMTPMailHandler(mail.MailHandler):
params = dict()
# some keyword args override configuration defaults
for item in ['to', 'from_addr', 'cc', 'bcc', 'subject',
'subject_prefix', 'files']:
for item in [
'to', 'from_addr', 'cc', 'bcc', 'subject', 'subject_prefix', 'files',
'charset', 'header_encoding', 'body_encoding',
'date_enforce', 'msgid_enforce', 'msgid_str',
]:
config_item = self.app.config.get(self._meta.config_section, item)
params[item] = kw.get(item, config_item)
# others don't
other_params = ['ssl', 'tls', 'host', 'port', 'auth', 'username',
'password', 'timeout']
for item in other_params:
params[item] = self.app.config.get(self._meta.config_section,
item)
for item in [
'ssl', 'tls', 'host', 'port', 'auth', 'username', 'password',
'timeout', 'msgid_domain'
]:
params[item] = self.app.config.get(self._meta.config_section, item)
# some are only set by message
for item in ['date', 'message_id', 'return_path', 'reply_to']:
value = kw.get(item, None)
if value is not None and str.strip(f'{value}') != '':
params[item] = kw.get(item, config_item)
# take all X-headers as is
for item in kw.keys():
if len(item) > 2 and item.startswith(('x-', 'X-', 'x_', 'X_')):
value = kw.get(item, None)
if value is not None:
params[f'X-{item[2:]}'] = value
return params
@ -143,91 +171,231 @@ class SMTPMailHandler(mail.MailHandler):
if is_true(params['auth']):
server.login(params['username'], params['password'])
res = self._send_message(server, body, **params)
msg = self._make_message(body, **params)
res = server.send_message(msg)
server.quit()
return res
def _send_message(self,
server: Union[smtplib.SMTP, smtplib.SMTP_SSL],
body: Union[str, Tuple[str, str]],
**params: Any) -> bool:
msg = MIMEMultipart('alternative')
msg.set_charset('utf-8')
# FIXME: should deprecate for 3.0 and change in 3.2
# For smtplib this would be "senderrs" (dict), but for backward compat
# we need to return bool
# https://github.com/python/cpython/blob/3.13/Lib/smtplib.py#L899
self.app.log.error(f"SMTPHandler Errors: {res}")
if len(res) > 0:
# this will be difficult to test with Mailpit as it accepts everything... no cover
return False # pragma: nocover
else:
return True
msg['From'] = params['from_addr']
def _header(self, value: Optional[str] = None, _charset: Optional[Charset] = None,
**params: Dict[str, Any]) -> Header:
header = Header(value, charset=_charset) if params['header_encoding'] else value
return header # type: ignore
def _make_message(self, body: Union[str, Tuple[str, str]], **params: Dict[str, Any]) \
-> MIMEMultipart:
# use encoding for header parts
cs_header = Charset(params['charset']) # type: ignore
if params['header_encoding'] == 'base64':
cs_header.header_encoding = BASE64
elif params['header_encoding'] == 'qp' or params['body_encoding'] == 'quoted-printable':
cs_header.header_encoding = QP
# use encoding for body parts
cs_body = Charset(params['charset']) # type: ignore
if params['body_encoding'] == 'base64':
cs_body.body_encoding = BASE64
elif params['body_encoding'] == 'qp' or params['body_encoding'] == 'quoted-printable':
cs_body.body_encoding = QP
# setup body parts
partText = None
partHtml = None
if type(body) not in [str, tuple, dict]:
error_msg = "Message body must be string, " \
"tuple ('<text>', '<html>') or " \
"dict {'text': '<text>', 'html': '<html>'}"
raise TypeError(error_msg)
if isinstance(body, str):
partText = MIMEText(body, 'plain', _charset=cs_body) # type: ignore
elif isinstance(body, tuple):
# handle plain text
if len(body) >= 1 and body[0] and str.strip(body[0]) != '':
partText = MIMEText(str.strip(body[0]),
'plain',
_charset=cs_body) # type: ignore
# handle html
if len(body) >= 2 and body[1] and str.strip(body[1]) != '':
partHtml = MIMEText(str.strip(body[1]),
'html',
_charset=cs_body) # type: ignore
elif isinstance(body, dict):
# handle plain text
if 'text' in body and str.strip(body['text']) != '':
partText = MIMEText(str.strip(body['text']),
'plain',
_charset=cs_body)
# handle html
if 'html' in body and str.strip(body['html']) != '':
partHtml = MIMEText(str.strip(body['html']), 'html', _charset=cs_body)
# To define the correct message content-type
# we need to indentify the content of this mail.
# If only "text" exists => text/plain, if only
# "html" exists => text/html, if "text" and
# "html" exists => multipart/alternative. In
# any case that files exists => multipart/mixed.
# Set message charset and encoding based on parts
if params['files']:
msg = MIMEMultipart('mixed')
msg.set_charset(params['charset']) # type: ignore
elif partText and partHtml:
msg = MIMEMultipart('alternative')
msg.set_charset(params['charset']) # type: ignore
elif partHtml:
msg = MIMEBase('text', 'html') # type: ignore
msg.set_charset(cs_body)
else:
msg = MIMEBase('text', 'plain') # type: ignore
msg.set_charset(cs_body)
# create message
msg['From'] = params['from_addr'] # type: ignore
msg['To'] = ', '.join(params['to'])
if params['cc']:
msg['Cc'] = ', '.join(params['cc'])
if params['bcc']:
msg['Bcc'] = ', '.join(params['bcc'])
if params['subject_prefix'] not in [None, '']:
subject = f"{params['subject_prefix']} {params['subject']}"
msg['Subject'] = self._header(f"{params['subject_prefix']} {params['subject']}",
_charset=cs_header, **params) # type: ignore
else:
subject = params['subject']
msg['Subject'] = Header(subject) # type: ignore
msg['Subject'] = self._header(params['subject'], # type: ignore
_charset=cs_header,
**params)
# add body as text and/or as html
partText = None
partHtml = None
# check for date
if is_true(params['date_enforce']) and not params.get('date', None):
params['date'] = format_datetime(datetime.now(timezone.utc)) # type: ignore
# check for message-id
if is_true(params['msgid_enforce']) and not params.get('message_id', None):
params['message_id'] = make_msgid(params['msgid_str'], # type: ignore
params['msgid_domain']) # type: ignore
if type(body) not in [str, tuple]:
error_msg = "Message body must be string or tuple " \
"('<text>', '<html>')"
raise TypeError(error_msg)
# check for message headers
if params.get('date', None):
msg['Date'] = params['date'] # type: ignore
if params.get('message_id', None):
msg['Message-Id'] = params['message_id'] # type: ignore
if params.get('return_path', None):
msg['Return-Path'] = params['return_path'] # type: ignore
if params.get('reply_to', None):
msg['Reply-To'] = params['reply_to'] # type: ignore
if isinstance(body, str):
partText = MIMEText(body)
elif isinstance(body, tuple):
# handle plain text
if len(body) >= 1:
partText = MIMEText(body[0], 'plain')
# check for X-headers
for item in params.keys():
if item.startswith('X-'):
msg.add_header(item.title(),
self._header(f'{params[item]}', # type: ignore
_charset=cs_header, **params))
# handle html
if len(body) >= 2:
partHtml = MIMEText(body[1], 'html')
if partText:
msg.attach(partText)
if partHtml:
msg.attach(partHtml)
# append the body parts
if params['files']:
# multipart/mixed
if partHtml:
# when html exists, create always a related part to include
# the body alternatives and eventually files as related
# attachments (e.g. images).
rel = MIMEMultipart('related')
# create an alternative part to include bodies for text and html
alt = MIMEMultipart('alternative')
# body text and body html
if partText:
alt.attach(partText)
alt.attach(partHtml)
rel.attach(alt)
msg.attach(rel)
else:
# only body text or no body
if partText:
msg.attach(partText)
else:
# no body no files = empty message = just headers
pass # pragma: no cover
else:
# multipart/alternative
if partText and partHtml:
# plain/text and plain/html
msg.attach(partText)
msg.attach(partHtml)
else:
# plain/text or plain/html only so just append payload
if partText:
msg.set_payload(partText.get_payload(), charset=cs_body)
elif partHtml:
msg.set_payload(partHtml.get_payload(), charset=cs_body)
else:
# no body no files = empty message = just headers
pass # pragma: no cover
# attach files
if params['files']:
for in_path in params['files']:
part = MIMEBase('application', 'octet-stream')
# support for alternative file name if its tuple
# like ('alt-name.ext', '/path/to/file.ext')
# support for alternative file name if its tuple or dict
# like [
# 'path/simple.ext',
# ('altname.ext', 'path/filename.ext'),
# ('altname.ext', 'path/filename.ext', 'content_id'),
# {'name': 'altname', 'path': 'path/filename.ext', cid: 'cidname'},
# ]
if isinstance(in_path, tuple):
if in_path[0] == in_path[1]:
# protect against the full path being passed in
alt_name = os.path.basename(in_path[0])
else:
alt_name = in_path[0]
altname = os.path.basename(in_path[0])
path = in_path[1]
cid = in_path[2] if len(in_path) >= 3 else None
elif isinstance(in_path, dict):
altname = os.path.basename(in_path.get('name', None))
path = in_path.get('path')
cid = in_path.get('cid', None)
else:
alt_name = os.path.basename(in_path)
altname = None
path = in_path
cid = None
path = fs.abspath(path)
if not altname:
altname = os.path.basename(path)
# add attachment
# add attachment payload from file
with open(path, 'rb') as file:
part.set_payload(file.read())
# check for embedded image or regular attachments
if cid:
part = MIMEImage(file.read())
else:
part = MIMEBase('application', 'octet-stream')
part.set_payload(file.read())
# encode and name
# encoder
encoders.encode_base64(part)
part.add_header(
'Content-Disposition',
f'attachment; filename={alt_name}',
)
msg.attach(part)
server.send_message(msg)
# embedded inline or attachment
if cid:
part.add_header(
'Content-Disposition',
f'inline; filename={altname}',
)
part.add_header('Content-ID', f'<{cid}>')
msg.attach(part)
else:
# altname header
part.add_header(
'Content-Disposition',
f'attachment; filename={altname}',
)
msg.attach(part)
# FIXME: how to check success? docs don't say return type
# - `[ext.scrub]` [Issue #724](https://github.com/datafolklabs/cement/issues/724)
return True
return msg
def load(app: App) -> None:

0
cement/py.typed Normal file
View File

View File

@ -34,7 +34,7 @@
# in this file.
# Note: Nothing is covered here because this file is imported before nose and
# coverage take over.. and so its a false positive that nothing is covered.
# coverage take over.. and so it's a false positive that nothing is covered.
import datetime
import os

1316
devbox.d/redis/redis.conf Normal file

File diff suppressed because it is too large Load Diff

24
devbox.json Normal file
View File

@ -0,0 +1,24 @@
{
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.14.0/.schema/devbox.schema.json",
"packages": {
"python": "3.14",
"pdm": "latest",
"libmemcached": "latest",
"zlib": {
"version": "latest",
"outputs": ["out", "dev"]
},
"redis": "latest",
"memcached": {
"version": "latest",
"outputs": ["out"]
},
"mailpit": "latest"
},
"shell": {
"init_hook": [],
"scripts": {
"test": ["echo \"Error: no test specified\" && exit 1"]
}
}
}

402
devbox.lock Normal file
View File

@ -0,0 +1,402 @@
{
"lockfile_version": "1",
"packages": {
"github:NixOS/nixpkgs/nixpkgs-unstable": {
"resolved": "github:NixOS/nixpkgs/a7fc11be66bdfb5cdde611ee5ce381c183da8386?lastModified=1761880412&narHash=sha256-QoJjGd4NstnyOG4mm4KXF%2BweBzA2AH%2F7gn1Pmpfcb0A%3D"
},
"libmemcached@latest": {
"last_modified": "2025-10-07T08:41:47Z",
"resolved": "github:NixOS/nixpkgs/bce5fe2bb998488d8e7e7856315f90496723793c#libmemcached",
"source": "devbox-search",
"version": "1.0.18",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/hvq13f4g2yzg4ir8zms3qljdg70kx2nk-libmemcached-1.0.18",
"default": true
}
],
"store_path": "/nix/store/hvq13f4g2yzg4ir8zms3qljdg70kx2nk-libmemcached-1.0.18"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/pgbw4r2l1k6r4jkj3rx9z1sjvffkzlgj-libmemcached-1.0.18",
"default": true
}
],
"store_path": "/nix/store/pgbw4r2l1k6r4jkj3rx9z1sjvffkzlgj-libmemcached-1.0.18"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/22p6w02llxgwwvzgrjhiawfhprxs0gn1-libmemcached-1.0.18",
"default": true
}
],
"store_path": "/nix/store/22p6w02llxgwwvzgrjhiawfhprxs0gn1-libmemcached-1.0.18"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/1zqgqb0iv0q53q0laxmwbhbvzrpid72d-libmemcached-1.0.18",
"default": true
}
],
"store_path": "/nix/store/1zqgqb0iv0q53q0laxmwbhbvzrpid72d-libmemcached-1.0.18"
}
}
},
"mailpit@latest": {
"last_modified": "2025-10-13T09:56:54Z",
"resolved": "github:NixOS/nixpkgs/c12c63cd6c5eb34c7b4c3076c6a99e00fcab86ec#mailpit",
"source": "devbox-search",
"version": "1.27.10",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/hygcxbx8nbzgmzj2861pq8371sbk9vxx-mailpit-1.27.10",
"default": true
}
],
"store_path": "/nix/store/hygcxbx8nbzgmzj2861pq8371sbk9vxx-mailpit-1.27.10"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/i562vdjasq196kw3zxsq2faqxk0jia35-mailpit-1.27.10",
"default": true
}
],
"store_path": "/nix/store/i562vdjasq196kw3zxsq2faqxk0jia35-mailpit-1.27.10"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/iw496lj2zl7yyhr584gl3gmlpwfgwywz-mailpit-1.27.10",
"default": true
}
],
"store_path": "/nix/store/iw496lj2zl7yyhr584gl3gmlpwfgwywz-mailpit-1.27.10"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/31p7d3r6ngcyy19asv92590aihcpcw2y-mailpit-1.27.10",
"default": true
}
],
"store_path": "/nix/store/31p7d3r6ngcyy19asv92590aihcpcw2y-mailpit-1.27.10"
}
}
},
"memcached@latest": {
"last_modified": "2025-10-07T08:41:47Z",
"resolved": "github:NixOS/nixpkgs/bce5fe2bb998488d8e7e7856315f90496723793c#memcached",
"source": "devbox-search",
"version": "1.6.39",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/ihnmlzfs4bj1iza5jrhd69j7cfxk98c9-memcached-1.6.39",
"default": true
}
],
"store_path": "/nix/store/ihnmlzfs4bj1iza5jrhd69j7cfxk98c9-memcached-1.6.39"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/ikqynsvdx49dyfyvjc9dzal3793x3q12-memcached-1.6.39",
"default": true
}
],
"store_path": "/nix/store/ikqynsvdx49dyfyvjc9dzal3793x3q12-memcached-1.6.39"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/pmljf231q2z42c3fqphy16nwb1238rvw-memcached-1.6.39",
"default": true
}
],
"store_path": "/nix/store/pmljf231q2z42c3fqphy16nwb1238rvw-memcached-1.6.39"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/mr6k45aw0srb2vl5azd2ndyh77fdqkp7-memcached-1.6.39",
"default": true
}
],
"store_path": "/nix/store/mr6k45aw0srb2vl5azd2ndyh77fdqkp7-memcached-1.6.39"
}
}
},
"pdm@latest": {
"last_modified": "2025-10-07T08:41:47Z",
"resolved": "github:NixOS/nixpkgs/bce5fe2bb998488d8e7e7856315f90496723793c#pdm",
"source": "devbox-search",
"version": "2.25.9",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/fali4xk6c9dv1cfp6yshc83k7knn4jkm-pdm-2.25.9",
"default": true
},
{
"name": "dist",
"path": "/nix/store/84h5gp6xqz6ivgwdv1hs72nkrhr1jp6w-pdm-2.25.9-dist"
}
],
"store_path": "/nix/store/fali4xk6c9dv1cfp6yshc83k7knn4jkm-pdm-2.25.9"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/d444f8wq1zf4w5vmm95lsh25m60dp3b0-pdm-2.25.9",
"default": true
},
{
"name": "dist",
"path": "/nix/store/dhx4hdxizq8zrvwajs4qiz26n5bk4g71-pdm-2.25.9-dist"
}
],
"store_path": "/nix/store/d444f8wq1zf4w5vmm95lsh25m60dp3b0-pdm-2.25.9"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/b3628r7jbv1i70jajx3z25q4pwxrq1dg-pdm-2.25.9",
"default": true
},
{
"name": "dist",
"path": "/nix/store/i4349788frmbv2kcpnnh6dh91hhgcbss-pdm-2.25.9-dist"
}
],
"store_path": "/nix/store/b3628r7jbv1i70jajx3z25q4pwxrq1dg-pdm-2.25.9"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/w9mvh1lpwwyxh1h2z5bypvvvvxwdjbyd-pdm-2.25.9",
"default": true
},
{
"name": "dist",
"path": "/nix/store/q4dp9wx7g79hknqgly1nccrdwvxr274j-pdm-2.25.9-dist"
}
],
"store_path": "/nix/store/w9mvh1lpwwyxh1h2z5bypvvvvxwdjbyd-pdm-2.25.9"
}
}
},
"python@3.14": {
"last_modified": "2025-10-08T01:30:28Z",
"plugin_version": "0.0.4",
"resolved": "github:NixOS/nixpkgs/8b5c9dd8856f0c0cf46cc91f2c21c106a9d42e25#python314",
"source": "devbox-search",
"version": "3.14.0",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/2sqv05h8017f38w5rvppb2f5wbbisnwp-python3-3.14.0",
"default": true
}
],
"store_path": "/nix/store/2sqv05h8017f38w5rvppb2f5wbbisnwp-python3-3.14.0"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/zlph3shgrkfmrhkxbgmi6qa26gfzl58q-python3-3.14.0",
"default": true
},
{
"name": "debug",
"path": "/nix/store/3zmfrpjfpqaxcl68hlg5nfzvx49awjks-python3-3.14.0-debug"
}
],
"store_path": "/nix/store/zlph3shgrkfmrhkxbgmi6qa26gfzl58q-python3-3.14.0"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/bq9ss2vlr05zdrhcfmvclm0gsrc7i6xb-python3-3.14.0",
"default": true
}
],
"store_path": "/nix/store/bq9ss2vlr05zdrhcfmvclm0gsrc7i6xb-python3-3.14.0"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/76lchhz5hhik0j5hjy6lwwn3ik0x54aa-python3-3.14.0",
"default": true
},
{
"name": "debug",
"path": "/nix/store/rjm597f2d9bjllyjnv3y20261bwxd108-python3-3.14.0-debug"
}
],
"store_path": "/nix/store/76lchhz5hhik0j5hjy6lwwn3ik0x54aa-python3-3.14.0"
}
}
},
"redis@latest": {
"last_modified": "2025-10-07T08:41:47Z",
"plugin_version": "0.0.2",
"resolved": "github:NixOS/nixpkgs/bce5fe2bb998488d8e7e7856315f90496723793c#redis",
"source": "devbox-search",
"version": "8.2.2",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/cnwxss9snn773vwr2vb0b6jx7lznmac2-redis-8.2.2",
"default": true
}
],
"store_path": "/nix/store/cnwxss9snn773vwr2vb0b6jx7lznmac2-redis-8.2.2"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/nml062djj2nfc7zgn2q93g0swgrbdqbc-redis-8.2.2",
"default": true
}
],
"store_path": "/nix/store/nml062djj2nfc7zgn2q93g0swgrbdqbc-redis-8.2.2"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/vixhsgdlv2d8hm66jkclqdgyfi3vi86b-redis-8.2.2",
"default": true
}
],
"store_path": "/nix/store/vixhsgdlv2d8hm66jkclqdgyfi3vi86b-redis-8.2.2"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/z6sw7nrc62l9z1mm05dqx4vz3a65lgb6-redis-8.2.2",
"default": true
}
],
"store_path": "/nix/store/z6sw7nrc62l9z1mm05dqx4vz3a65lgb6-redis-8.2.2"
}
}
},
"zlib@latest": {
"last_modified": "2025-10-08T10:03:27Z",
"resolved": "github:NixOS/nixpkgs/fb5cf53218b987f2703a5bbc292a030c0fe33443#zlib",
"source": "devbox-search",
"version": "1.3.1",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/5vnba43n1w87cs2i2dd242zy88k4dwf9-zlib-1.3.1",
"default": true
},
{
"name": "dev",
"path": "/nix/store/86xjpgsysygxdhfczssrlj24mi22wxiq-zlib-1.3.1-dev"
},
{
"name": "static",
"path": "/nix/store/39mpcp31vw48raa6lj7k2di0ln922bjp-zlib-1.3.1-static"
}
],
"store_path": "/nix/store/5vnba43n1w87cs2i2dd242zy88k4dwf9-zlib-1.3.1"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/h5gyhqv452n16izdh86s3rvdq1fcg2pb-zlib-1.3.1",
"default": true
},
{
"name": "dev",
"path": "/nix/store/c7jl10xakizqzj2wv5arnjggwv1vh3r0-zlib-1.3.1-dev"
},
{
"name": "static",
"path": "/nix/store/plix72ay1sq7l91qm97a0ls7haa0hmv8-zlib-1.3.1-static"
}
],
"store_path": "/nix/store/h5gyhqv452n16izdh86s3rvdq1fcg2pb-zlib-1.3.1"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/yy41r1jfz748y6xq4dc54qm4xlravz95-zlib-1.3.1",
"default": true
},
{
"name": "dev",
"path": "/nix/store/kw5wnbgivfxif9m2g8i4ks0whdgnh158-zlib-1.3.1-dev"
},
{
"name": "static",
"path": "/nix/store/l6mprw6il6i4dx78pcnn7as09287f9nh-zlib-1.3.1-static"
}
],
"store_path": "/nix/store/yy41r1jfz748y6xq4dc54qm4xlravz95-zlib-1.3.1"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/6qi8skh85ci2k9gvl27nnh3kiy32qnsz-zlib-1.3.1",
"default": true
},
{
"name": "dev",
"path": "/nix/store/6fqij3sy1mlsnq1n679bzbl5bkqs7yvk-zlib-1.3.1-dev"
},
{
"name": "static",
"path": "/nix/store/b1l0v5czhw4c410nxvk5lbjfv9r8h7h7-zlib-1.3.1-static"
}
],
"store_path": "/nix/store/6qi8skh85ci2k9gvl27nnh3kiy32qnsz-zlib-1.3.1"
}
}
}
}
}

View File

@ -53,20 +53,13 @@ services:
- memcached
- mailpit
cement-py38:
<<: *DEFAULTS
image: "cement:dev-py38"
build:
context: .
dockerfile: docker/Dockerfile.dev-py38
cement-py39:
<<: *DEFAULTS
image: "cement:dev-py39"
build:
context: .
dockerfile: docker/Dockerfile.dev-py39
cement-py310:
<<: *DEFAULTS
image: "cement:dev-py310"
@ -87,3 +80,10 @@ services:
build:
context: .
dockerfile: docker/Dockerfile.dev-py312
cement-py313:
<<: *DEFAULTS
image: "cement:dev-py313"
build:
context: .
dockerfile: docker/Dockerfile.dev-py313

View File

@ -1,6 +1,6 @@
FROM python:3.13-alpine
FROM python:3.14-alpine
LABEL MAINTAINER="BJ Dierkes <derks@datafolklabs.com>"
ENV PS1="\[\e[0;33m\]|> cement-py313 <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# "
ENV PS1="\[\e[0;33m\]|> cement-py314 <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# "
ENV PATH="${PATH}:/root/.local/bin"
WORKDIR /src

View File

@ -1,4 +1,4 @@
FROM python:3.12.3-alpine
FROM python:3.12-alpine
LABEL MAINTAINER="BJ Dierkes <derks@datafolklabs.com>"
ENV PS1="\[\e[0;33m\]|> cement-py312 <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# "
ENV PATH="${PATH}:/root/.local/bin"

View File

@ -1,4 +1,4 @@
FROM python:3.13-rc-alpine
FROM python:3.13-alpine
LABEL MAINTAINER="BJ Dierkes <derks@datafolklabs.com>"
ENV PS1="\[\e[0;33m\]|> cement-py313 <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# "
ENV PATH="${PATH}:/root/.local/bin"

View File

@ -1,6 +1,6 @@
FROM python:3.8-alpine
FROM python:3.14-alpine
LABEL MAINTAINER="BJ Dierkes <derks@datafolklabs.com>"
ENV PS1="\[\e[0;33m\]|> cement-py38 <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# "
ENV PS1="\[\e[0;33m\]|> cement-py314 <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# "
ENV PATH="${PATH}:/root/.local/bin"
WORKDIR /src
@ -22,7 +22,7 @@ RUN apk update \
&& ln -sf /usr/bin/vim /usr/bin/vi
RUN pipx install pdm
COPY . /src
COPY docker/vimrc /root/.vimrc
COPY docker/bashrc /root/.bashrc
COPY ./docker/vimrc /root/.vimrc
COPY ./docker/bashrc /root/.bashrc
RUN pdm install
CMD ["/bin/bash"]

View File

@ -0,0 +1,33 @@
volumes:
mailpit-data:
services:
redis:
image: redis:latest
hostname: redis
ports:
- 6379:6379
memcached:
image: memcached:latest
hostname: memcached
ports:
- 11211:11211
mailpit:
image: axllent/mailpit
hostname: mailpit
restart: always
volumes:
- mailpit-data:/data
- ../docker/mailpit:/certificates
ports:
- 8025:8025
- 1025:1025
environment:
MP_MAX_MESSAGES: 5000
MP_DATA_FILE: /data/mailpit.db
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
MP_SMTP_TLS_CERT: /certificates/dev-cert.pem
MP_SMTP_TLS_KEY: /certificates/dev-key.pem

325
pdm.lock
View File

@ -3,9 +3,12 @@
[metadata]
groups = ["default", "alarm", "argparse", "cli", "colorlog", "configparser", "daemon", "dev", "docs", "dummy", "generate", "jinja2", "json", "logging", "memcached", "mustache", "plugin", "print", "redis", "scrub", "smtp", "tabulate", "watchdog", "yaml"]
strategy = ["cross_platform", "inherit_metadata"]
lock_version = "4.4.1"
content_hash = "sha256:93bbcbfd6b1a08c9a37b42a42d1300b6f9ede3eb6e643a085a8152fb81a60d88"
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:88e91afb3db1d8d71ebff4abd3d4d3e29b3415592657bd9fe104948d542d1ee5"
[[metadata.targets]]
requires_python = ">=3.9"
[[package]]
name = "alabaster"
@ -25,6 +28,9 @@ requires_python = ">=3.7"
summary = "Timeout context manager for asyncio programs"
groups = ["redis"]
marker = "python_full_version < \"3.11.3\""
dependencies = [
"typing-extensions>=3.6.5; python_version < \"3.8\"",
]
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
@ -49,7 +55,7 @@ name = "certifi"
version = "2024.2.2"
requires_python = ">=3.6"
summary = "Python package for providing Mozilla's CA Bundle."
groups = ["docs"]
groups = ["dev", "docs"]
files = [
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
@ -60,7 +66,7 @@ name = "charset-normalizer"
version = "3.3.2"
requires_python = ">=3.7.0"
summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
groups = ["docs"]
groups = ["dev", "docs"]
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
@ -155,7 +161,7 @@ files = [
[[package]]
name = "colorlog"
version = "6.9.0"
version = "6.10.1"
requires_python = ">=3.6"
summary = "Add colours to the output of Python's logging module."
groups = ["colorlog"]
@ -163,8 +169,8 @@ dependencies = [
"colorama; sys_platform == \"win32\"",
]
files = [
{file = "colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff"},
{file = "colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2"},
{file = "colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c"},
{file = "colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321"},
]
[[package]]
@ -374,7 +380,7 @@ name = "idna"
version = "3.6"
requires_python = ">=3.5"
summary = "Internationalized Domain Names in Applications (IDNA)"
groups = ["docs"]
groups = ["dev", "docs"]
files = [
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
@ -399,6 +405,7 @@ summary = "Read metadata from Python packages"
groups = ["docs", "mustache"]
marker = "python_version < \"3.10\""
dependencies = [
"typing-extensions>=3.6.4; python_version < \"3.8\"",
"zipp>=0.5",
]
files = [
@ -419,7 +426,7 @@ files = [
[[package]]
name = "jinja2"
version = "3.1.4"
version = "3.1.6"
requires_python = ">=3.7"
summary = "A very fast and expressive template engine."
groups = ["cli", "docs", "jinja2"]
@ -427,8 +434,8 @@ dependencies = [
"MarkupSafe>=2.0",
]
files = [
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
]
[[package]]
@ -493,18 +500,18 @@ files = [
[[package]]
name = "mock"
version = "5.1.0"
version = "5.2.0"
requires_python = ">=3.6"
summary = "Rolling backport of unittest.mock for all Pythons"
groups = ["dev"]
files = [
{file = "mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744"},
{file = "mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d"},
{file = "mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"},
{file = "mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"},
]
[[package]]
name = "mypy"
version = "1.13.0"
version = "1.14.1"
requires_python = ">=3.8"
summary = "Optional static typing for Python"
groups = ["dev"]
@ -514,38 +521,44 @@ dependencies = [
"typing-extensions>=4.6.0",
]
files = [
{file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
{file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
{file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"},
{file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"},
{file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"},
{file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"},
{file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"},
{file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"},
{file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"},
{file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"},
{file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"},
{file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"},
{file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"},
{file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"},
{file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"},
{file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"},
{file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"},
{file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"},
{file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"},
{file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"},
{file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"},
{file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"},
{file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"},
{file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"},
{file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"},
{file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"},
{file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"},
{file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
{file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
{file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
{file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
{file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
{file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"},
{file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"},
{file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"},
{file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"},
{file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"},
{file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"},
{file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"},
{file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"},
{file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"},
{file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"},
{file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"},
{file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"},
{file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"},
{file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"},
{file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"},
{file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"},
{file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"},
{file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"},
{file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"},
{file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"},
{file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"},
{file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"},
{file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"},
{file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"},
{file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"},
{file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"},
{file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"},
{file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"},
{file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"},
{file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"},
{file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"},
{file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"},
{file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"},
{file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"},
{file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"},
{file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"},
{file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"},
{file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"},
]
[[package]]
@ -631,9 +644,19 @@ files = [
{file = "pylibmc-1.6.3.tar.gz", hash = "sha256:eefa46115537abad65fbe2e032acd1b3463d9bf9e335af4b0916df4e4d3206e0"},
]
[[package]]
name = "pypng"
version = "0.20220715.0"
summary = "Pure Python library for saving and loading PNG images"
groups = ["dev"]
files = [
{file = "pypng-0.20220715.0-py3-none-any.whl", hash = "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c"},
{file = "pypng-0.20220715.0.tar.gz", hash = "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1"},
]
[[package]]
name = "pystache"
version = "0.6.5"
version = "0.6.8"
requires_python = ">=3.8"
summary = "Mustache for Python"
groups = ["mustache"]
@ -641,13 +664,13 @@ dependencies = [
"importlib-metadata>=4.6; python_version < \"3.10\"",
]
files = [
{file = "pystache-0.6.5-py3-none-any.whl", hash = "sha256:84546278219431f1a2ecc86a627cc1b0fe3c83b5612f8a7d9c81ea0119ac8f49"},
{file = "pystache-0.6.5.tar.gz", hash = "sha256:9f238d5a06f18843e0d491d8e4e292dc03fed6a54cb0a5c34be37a3faa973174"},
{file = "pystache-0.6.8-py3-none-any.whl", hash = "sha256:7211e000974a6e06bce2d4d5cad8df03bcfffefd367209117376e4527a1c3cb8"},
{file = "pystache-0.6.8.tar.gz", hash = "sha256:3707518e6a4d26dd189b07c10c669b1fc17df72684617c327bd3550e7075c72c"},
]
[[package]]
name = "pytest"
version = "8.3.3"
version = "8.3.5"
requires_python = ">=3.8"
summary = "pytest: simple powerful testing with Python"
groups = ["dev"]
@ -660,8 +683,8 @@ dependencies = [
"tomli>=1; python_version < \"3.11\"",
]
files = [
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
{file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
{file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
]
[[package]]
@ -679,82 +702,91 @@ files = [
{file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
]
[[package]]
name = "pytz"
version = "2024.1"
summary = "World timezone definitions, modern and historical"
groups = ["docs"]
marker = "python_version < \"3.9\""
files = [
{file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
{file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
]
[[package]]
name = "pyyaml"
version = "6.0.2"
version = "6.0.3"
requires_python = ">=3.8"
summary = "YAML parser and emitter for Python"
groups = ["cli", "generate", "yaml"]
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
{file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
{file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
{file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
{file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
{file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
{file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
{file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
{file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
{file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
{file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
{file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
{file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
{file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
{file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
{file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"},
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"},
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"},
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"},
{file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"},
{file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"},
{file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"},
{file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"},
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"},
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"},
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"},
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"},
{file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"},
{file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"},
{file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"},
{file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"},
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"},
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"},
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"},
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"},
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"},
{file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"},
{file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"},
{file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"},
{file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"},
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"},
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"},
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"},
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"},
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"},
{file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"},
{file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"},
{file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"},
{file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"},
{file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"},
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"},
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"},
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"},
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"},
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"},
{file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"},
{file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"},
{file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"},
{file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"},
{file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"},
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"},
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"},
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"},
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"},
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"},
{file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"},
{file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"},
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"},
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"},
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"},
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"},
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"},
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"},
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"},
{file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"},
{file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"},
{file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"},
{file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"},
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"},
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"},
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"},
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"},
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"},
{file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"},
{file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"},
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
]
[[package]]
name = "redis"
version = "5.2.0"
version = "6.1.1"
requires_python = ">=3.8"
summary = "Python client for Redis database and key-value store"
groups = ["redis"]
@ -762,8 +794,8 @@ dependencies = [
"async-timeout>=4.0.3; python_full_version < \"3.11.3\"",
]
files = [
{file = "redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897"},
{file = "redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0"},
{file = "redis-6.1.1-py3-none-any.whl", hash = "sha256:ed44d53d065bbe04ac6d76864e331cfe5c5353f86f6deccc095f8794fd15bb2e"},
{file = "redis-6.1.1.tar.gz", hash = "sha256:88c689325b5b41cedcbdbdfd4d937ea86cf6dab2222a83e86d8a466e4b3d2600"},
]
[[package]]
@ -771,7 +803,7 @@ name = "requests"
version = "2.31.0"
requires_python = ">=3.7"
summary = "Python HTTP for Humans."
groups = ["docs"]
groups = ["dev", "docs"]
dependencies = [
"certifi>=2017.4.17",
"charset-normalizer<4,>=2",
@ -785,29 +817,30 @@ files = [
[[package]]
name = "ruff"
version = "0.7.3"
version = "0.14.2"
requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust."
groups = ["dev"]
files = [
{file = "ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344"},
{file = "ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0"},
{file = "ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16"},
{file = "ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc"},
{file = "ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088"},
{file = "ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c"},
{file = "ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313"},
{file = "ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1"},
{file = "ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11"},
{file = "ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3"},
{file = "ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3"},
{file = "ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8"},
{file = "ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839"},
{file = "ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7"},
{file = "ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc"},
{file = "ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a"},
{file = "ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096"},
{file = "ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df"},
{file = "ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05"},
{file = "ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5"},
{file = "ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e"},
{file = "ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770"},
{file = "ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9"},
{file = "ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af"},
{file = "ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a"},
{file = "ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96"},
]
[[package]]
@ -863,7 +896,7 @@ files = [
[[package]]
name = "sphinx-rtd-theme"
version = "3.0.1"
version = "3.0.2"
requires_python = ">=3.8"
summary = "Read the Docs theme for Sphinx"
groups = ["docs"]
@ -873,8 +906,8 @@ dependencies = [
"sphinxcontrib-jquery<5,>=4",
]
files = [
{file = "sphinx_rtd_theme-3.0.1-py2.py3-none-any.whl", hash = "sha256:921c0ece75e90633ee876bd7b148cfaad136b481907ad154ac3669b6fc957916"},
{file = "sphinx_rtd_theme-3.0.1.tar.gz", hash = "sha256:a4c5745d1b06dfcb80b7704fe532eb765b44065a8fad9851e4258c8804140703"},
{file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"},
{file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"},
]
[[package]]
@ -1010,7 +1043,7 @@ name = "urllib3"
version = "2.2.1"
requires_python = ">=3.8"
summary = "HTTP library with thread-safe connection pooling, file post, and more."
groups = ["docs"]
groups = ["dev", "docs"]
files = [
{file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
{file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},

19
process-compose.yml Normal file
View File

@ -0,0 +1,19 @@
# Process compose for starting django
version: "0.5"
processes:
memcached:
command: memcached
availability:
restart: "always"
mailpit:
command: mailpit
availability:
restart: "always"
environment:
- "MP_MAX_MESSAGES=5000"
- "MP_SMTP_AUTH_ACCEPT_ANY=1"
- "MP_SMTP_AUTH_ALLOW_INSECURE=1"
- "MP_SMTP_TLS_CERT=docker/mailpit/dev-cert.pem"
- "MP_SMTP_TLS_KEY=docker/mailpit/dev-key.pem"

View File

@ -16,7 +16,8 @@ classifiers = [
dynamic = ["version", "README"]
requires-python = ">=3.8"
requires-python = ">=3.9"
dependencies = []
[project.optional-dependencies]
alarm = []
@ -42,9 +43,6 @@ watchdog = ["watchdog"]
yaml = ["pyYaml"]
cli = ["cement[yaml,jinja2]"]
[tool.pdm.scripts]
cement = {call = "cement.cli.main:main"}
[project.scripts]
cement = "cement.cli.main:main"
@ -70,32 +68,8 @@ python_files= "test_*.py"
precision = 2
[tool.pdm.build]
package-dir = "."
includes = [
"cement/",
"cement/cli/templates/generate/",
"CONTRIBUTORS.md",
"CHANGELOG.md"
]
excludes = ["tests/"]
[tool.pdm.version]
source = "call"
getter = "cement.utils.version:get_version"
[tool.pdm.dev-dependencies]
dev = [
"pytest>=4.3.1",
"pytest-cov>=2.6.1",
"coverage>=4.5.3",
"mypy>=1.9.0",
"ruff>=0.3.2",
"mock>=5.1.0",
]
[tool.ruff]
target-version = "py38"
target-version = "py39"
line-length = 100
indent-width = 4
exclude = [
@ -133,7 +107,7 @@ unfixable = []
# ignore_missing_imports = true
[tool.mypy]
python_version = "3.8"
python_version = "3.9"
disallow_untyped_calls = true
disallow_untyped_defs = true
disallow_any_unimported = false
@ -160,3 +134,33 @@ exclude = """(?x)(
^.git/ |
^tests
)"""
[tool.pdm.scripts]
cement = {call = "cement.cli.main:main"}
[tool.pdm.build]
package-dir = "."
includes = [
"cement/",
"cement/cli/templates/generate/",
"CONTRIBUTORS.md",
"CHANGELOG.md"
]
excludes = ["tests/"]
[tool.pdm.version]
source = "call"
getter = "cement.utils.version:get_version"
[dependency-groups]
dev = [
"pytest>=4.3.1",
"pytest-cov>=2.6.1",
"coverage>=4.5.3",
"mypy>=1.9.0",
"ruff>=0.3.2",
"mock>=5.1.0",
"pypng>=0.20220715.0",
"requests>=2.31.0",
]

View File

@ -2,7 +2,7 @@
set -e
[ -z "$CEMENT_VERSION" ] && CEMENT_VERSION="3.0"
[ -z "$PYTHON_VERSIONS" ] && PYTHON_VERSIONS="3.8 3.9 3.10 3.11 3.12 3.13"
[ -z "$PYTHON_VERSIONS" ] && PYTHON_VERSIONS="3.9 3.10 3.11 3.12 3.13 3.14"
function smoke-test {
pyver=$1
@ -19,15 +19,15 @@ function smoke-test {
python:$pyver \
/bin/bash
docker exec -it cement-cli-smoke-test /bin/bash -c "cd /src ; pip install `ls dist/cement-*.tar.gz`[cli]"
tmp=$(docker exec cement-cli-smoke-test /bin/bash -c "mktemp -d")
docker exec cement-cli-smoke-test /bin/bash -c "cd /src ; pip install `ls dist/cement-*.tar.gz`[cli]"
tmp=$(docker exec -t cement-cli-smoke-test /bin/bash -c "mktemp -d")
### verify help output
res=$(docker exec cement-cli-smoke-test /bin/bash -c "cement --version")
echo "$res" | grep "Cement Framework $CEMENT_VERSION.\d"
echo "$res" | grep "Python $pyver.\d"
echo "$res" | grep "Cement Framework $CEMENT_VERSION.[0-9]"
echo "$res" | grep "Python $pyver.[0-9]"
echo "$res" | grep "Platform Linux.*"
res=$(docker exec cement-cli-smoke-test /bin/bash -c "cement --help")
echo "$res" | grep "Cement Framework Developer Tools"
@ -45,37 +45,37 @@ function smoke-test {
echo "$res" | grep "destination directory path"
echo "$res" | grep -- "-D, --defaults"
### generate a project
docker exec cement-cli-smoke-test /bin/bash -c "cement generate project -D $tmp/myapp"
docker exec cement-cli-smoke-test /bin/bash -c "cd $tmp/myapp ; pip install -r requirements.txt"
docker exec cement-cli-smoke-test /bin/bash -c "cd $tmp/myapp ; pip install setuptools"
docker exec cement-cli-smoke-test /bin/bash -c "cd $tmp/myapp ; python setup.py install"
res=$(docker exec cement-cli-smoke-test /bin/bash -c "myapp --version")
echo "$res" | grep "Cement Framework $CEMENT_VERSION\.\d"
echo "$res" | grep "Python $pyver.\d"
res=$(docker exec -t cement-cli-smoke-test /bin/bash -c "myapp --version")
echo "$res" | grep "Cement Framework $CEMENT_VERSION\.[0-9]"
echo "$res" | grep "Python $pyver.[0-9]"
echo "$res" | grep "Platform Linux.*"
### generate a script
docker exec cement-cli-smoke-test /bin/bash -c "cement generate script -D $tmp/myscript"
res=$(docker exec cement-cli-smoke-test /bin/bash -c "python $tmp/myscript/myscript.py --version")
res=$(docker exec -t cement-cli-smoke-test /bin/bash -c "python $tmp/myscript/myscript.py --version")
echo "$res" | grep "myscript v0.0.1"
### generate an extension
docker exec cement-cli-smoke-test /bin/bash -c "cement generate extension -D $tmp/myapp/myapp/ext"
res=$(docker exec cement-cli-smoke-test /bin/bash -c "cat $tmp/myapp/myapp/ext/ext_myextension.py")
res=$(docker exec -t cement-cli-smoke-test /bin/bash -c "cat $tmp/myapp/myapp/ext/ext_myextension.py")
echo "$res" | grep "myextension_pre_run_hook"
### generate a plugin
docker exec cement-cli-smoke-test /bin/bash -c "cement generate plugin -D $tmp/myapp/myapp/plugins"
res=$(docker exec cement-cli-smoke-test /bin/bash -c "cat $tmp/myapp/myapp/plugins/myplugin/controllers/myplugin.py")
res=$(docker exec -t cement-cli-smoke-test /bin/bash -c "cat $tmp/myapp/myapp/plugins/myplugin/controllers/myplugin.py")
echo "$res" | grep "class MyPlugin(Controller)"
### finish
@ -93,4 +93,4 @@ for pyver in $PYTHON_VERSIONS; do
echo -n "python $pyver . . . "
smoke-test $pyver 2>> tmp/cli-smoke-test.out 1>> tmp/cli-smoke-test.out
echo "ok"
done
done

View File

@ -6,6 +6,6 @@ def test_version():
# ensure that we bump things properly on version changes
assert backend.VERSION[0] == 3
assert backend.VERSION[1] == 0
assert backend.VERSION[2] == 12
assert backend.VERSION[2] == 15
assert backend.VERSION[3] == 'final'
assert backend.VERSION[4] == 0

View File

@ -77,24 +77,26 @@ def test_bogus_group(rando):
env.switch()
def _daemon_target(pid_file):
"""Target function for daemon test subprocess."""
with TestApp(argv=['--daemon'], extensions=['daemon']) as app:
app.config.set('daemon', 'pid_file', pid_file)
try:
# FIX ME: Can't daemonize, because nose/pytest lose sight of it
app.daemonize()
app.run()
finally:
app.close()
ext_daemon.cleanup(app)
def test_daemon(tmp):
os.remove(tmp.file)
from cement.utils import shell
# Test in a sub-process to avoid hangup
def target():
with TestApp(argv=['--daemon'], extensions=['daemon']) as app:
app.config.set('daemon', 'pid_file', tmp.file)
try:
# FIX ME: Can't daemonize, because nose/pytest lose sight of it
app.daemonize()
app.run()
finally:
app.close()
ext_daemon.cleanup(app)
p = shell.spawn_process(target)
p = shell.spawn_process(_daemon_target, args=(tmp.file,))
p.join()
assert p.exitcode == 0

View File

@ -3,6 +3,7 @@ import os
import mock
import requests
import json
import png
from time import sleep
from pytest import raises
from cement.utils.test import TestApp
@ -63,8 +64,139 @@ def test_smtp_send(rando):
assert msg['To'][0]['Address'] == f'to-{rando}@localhost'
assert msg['Subject'] == f'UNIT TEST > {rando}'
assert msg['Attachments'] == 0
assert msg['Cc'] == []
assert msg['Bcc'] == []
assert msg['Cc'] in [None, []]
assert msg['Bcc'] in [None, []]
delete_msg(msg['ID'])
def test_smtp_send_with_message_id(rando):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
app.mail.send(f"{rando}",
to=[f'to-{rando}@localhost'],
from_addr=f'from-{rando}@localhost',
message_id=f'message_id_{rando}')
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
# FIXME: Mailpit doesn't support this??? See: PR #742
# assert msg['Message-Id'] == f'message_id_{rando}'
delete_msg(msg['ID'])
def test_smtp_send_with_return_path(rando):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
app.mail.send(f"{rando}",
to=[f'to-{rando}@localhost'],
from_addr=f'from-{rando}@localhost',
return_path=f'return_path_{rando}')
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
# FIXME: Mailpit doesn't support this??? See: PR #742
# assert msg['Return-Path'] == f'return_path_{rando}'
delete_msg(msg['ID'])
def test_smtp_send_with_reply_to(rando):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
app.mail.send(f"{rando}",
to=[f'to-{rando}@localhost'],
from_addr=f'from-{rando}@localhost',
reply_to=f'reply_to_{rando}@localhost')
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
assert msg['ReplyTo'][0]['Address'] == f'reply_to_{rando}@localhost'
delete_msg(msg['ID'])
def test_smtp_send_with_x_headers(rando):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
app.mail.send(f"{rando}",
to=[f'to-{rando}@localhost'],
from_addr=f'from-{rando}@localhost',
X_Test_Header=f'x_header_{rando}')
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
# FIXME: Mailpit doesn't support this??? See: PR #742
# assert msg['X_Test_Header'] == f'x_header_{rando}'
delete_msg(msg['ID'])
def test_smtp_send_with_base64_encoding(rando):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
app.mail.send(f"{rando}",
to=[f'to-{rando}@localhost'],
from_addr=f'from-{rando}@localhost',
header_encoding='base64',
body_encoding='base64')
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
# FIXME: Not sure how to test this? See: PR #742
delete_msg(msg['ID'])
def test_smtp_send_with_qp_encoding(rando):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
app.mail.send(f"{rando}",
to=[f'to-{rando}@localhost'],
from_addr=f'from-{rando}@localhost',
header_encoding='qp',
body_encoding='qp')
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
# FIXME: Not sure how to test this? See: PR #742
delete_msg(msg['ID'])
@ -93,13 +225,65 @@ def test_smtp_html(rando):
delete_msg(msg['ID'])
def test_smtp_dict_text_and_html(rando):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
body = dict(
text=rando,
html=f"<body>{rando}</body>"
)
app.mail.send(body)
sleep(3)
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
text_url = f"http://{smtp_host}:8025/view/{msg['ID']}.txt"
res_text = requests.get(text_url)
assert res_text.content.decode('utf-8') == rando
html_url = f"http://{smtp_host}:8025/view/{msg['ID']}.html"
res_html = requests.get(html_url)
assert res_html.content.decode('utf-8') == f"<body>{rando}</body>"
delete_msg(msg['ID'])
def test_smtp_dict_html_only(rando):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
body = dict(
html=f"<body>{rando}</body>"
)
app.mail.send(body)
sleep(3)
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
html_url = f"http://{smtp_host}:8025/view/{msg['ID']}.html"
res_html = requests.get(html_url)
assert res_html.content.decode('utf-8') == f"<body>{rando}</body>"
delete_msg(msg['ID'])
def test_smtp_html_bad_body_type(rando):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
error_msg = '(.*)Message body must be string or tuple(.*)'
# ruff: noqa: E501
error_msg = "(.*)Message body must be string, tuple(.*)"
with raises(TypeError, match=error_msg):
app.mail.send(['text', '<body>html</body>'])
@ -155,6 +339,35 @@ def test_smtp_files(rando, tmp):
delete_msg(msg['ID'])
def test_smtp_dict_and_files(rando, tmp):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
files = []
for iter in [1, 2, 3]:
_file = f"{tmp.file}-{iter}"
with open(_file, 'w') as _open_file:
_open_file.write(f"{rando}-{iter}")
files.append(_file)
body = dict(
text=rando,
html=f"<body>{rando}</body>"
)
app.mail.send(body, files=files)
sleep(3)
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
assert msg['Attachments'] == 3
delete_msg(msg['ID'])
def test_smtp_files_alt_name(rando, tmp):
defaults['mail.smtp']['subject'] = rando
@ -177,6 +390,73 @@ def test_smtp_files_alt_name(rando, tmp):
delete_msg(msg['ID'])
def test_smtp_image_files_as_dict(rando, tmp):
defaults['mail.smtp']['subject'] = rando
image_file = os.path.join(tmp.dir, 'gradient.png')
# create a png (coverage)
width = 255
height = 255
img = []
for y in range(height):
row = ()
for x in range(width):
row = row + (x, max(0, 255 - x - y), y)
img.append(row)
with open(image_file, 'wb') as f:
w = png.Writer(width, height, greyscale=False)
w.write(f, img)
with SMTPApp(config_defaults=defaults) as app:
app.run()
app.mail.send(f"{rando}",
files=[dict(name=f'alt-filename-{rando}', path=image_file)])
sleep(3)
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
assert msg['Attachments'] == 1
res_full = requests.get(f"{mailpit_api}/message/{msg['ID']}")
data = res_full.json()
assert data['Attachments'][0]['FileName'] == f"alt-filename-{rando}"
delete_msg(msg['ID'])
def test_smtp_image_files_as_dict_inline(rando, tmp):
defaults['mail.smtp']['subject'] = rando
image_file = os.path.join(tmp.dir, 'gradient.png')
# create a png (coverage)
width = 255
height = 255
img = []
for y in range(height):
row = ()
for x in range(width):
row = row + (x, max(0, 255 - x - y), y)
img.append(row)
with open(image_file, 'wb') as f:
w = png.Writer(width, height, greyscale=False)
w.write(f, img)
with SMTPApp(config_defaults=defaults) as app:
app.run()
app.mail.send(f"{rando}",
files=[dict(name=f'alt-filename-{rando}', path=image_file, cid=f'cid-{rando}')])
sleep(3)
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
delete_msg(msg['ID'])
def test_smtp_files_path_does_not_exist(rando, tmp):
defaults['mail.smtp']['subject'] = rando

View File

@ -21,34 +21,44 @@ class WatchdogApp(TestApp):
def test_watchdog(tmp):
# The exception is getting raised, but for some reason it's not being
# caught by a with raises() block, so I'm mocking it out instead.
# Clear any existing mock first
if hasattr(MyEventHandler, 'on_any_event'):
if hasattr(MyEventHandler.on_any_event, 'reset_mock'):
MyEventHandler.on_any_event.reset_mock()
MyEventHandler.on_any_event = Mock()
with WatchdogApp() as app:
app.watchdog.add(tmp.dir, event_handler=MyEventHandler)
app.run()
try:
with WatchdogApp() as app:
app.watchdog.add(tmp.dir, event_handler=MyEventHandler)
app.run()
file_path = fs.join(tmp.dir, 'test.file')
# trigger an event
f = open(file_path, 'w')
f.write('test data')
f.close()
time.sleep(1)
file_path = fs.join(tmp.dir, 'test.file')
# trigger an event
f = open(file_path, 'w')
f.write('test data')
f.close()
time.sleep(1)
# 5 or 6 separate calls: See print(MyEventHandler.on_any_event.mock_calls)
# 5 or 6 separate calls: See print(MyEventHandler.on_any_event.mock_calls)
# Python < 3.11
# Tmp File created
# Tmp Dir modified
# Tmp File modified
# Tmp File closed
# Tmp Dir modified
# Python < 3.11
# Tmp File created
# Tmp Dir modified
# Tmp File modified
# Tmp File closed
# Tmp Dir modified
# In Python >= 3.11 this has one additional
# Tmp File opened
# In Python >= 3.11 this has one additional
# Tmp File opened
# But on Travis... this isn't resulting in the same counts so
# fudging the test a little... it's 5 or 6
# But on Travis... this isn't resulting in the same counts so
# fudging the test a little... it's 3-6 depending on platform/timing
assert MyEventHandler.on_any_event.call_count in [5, 6]
assert MyEventHandler.on_any_event.call_count in [3, 4, 5, 6]
finally:
# Reset mock to avoid interfering with other tests
if (hasattr(MyEventHandler, 'on_any_event') and
hasattr(MyEventHandler.on_any_event, 'reset_mock')):
MyEventHandler.on_any_event.reset_mock()
def test_watchdog_app_paths(tmp):
@ -60,40 +70,50 @@ def test_watchdog_app_paths(tmp):
(tmp.dir, WatchdogEventHandler)
]
# Clear any existing mock first
if hasattr(WatchdogEventHandler, 'on_any_event'):
if hasattr(WatchdogEventHandler.on_any_event, 'reset_mock'):
WatchdogEventHandler.on_any_event.reset_mock()
WatchdogEventHandler.on_any_event = Mock()
with MyApp() as app:
app.run()
try:
with MyApp() as app:
app.run()
WatchdogEventHandler.on_any_event.reset_mock()
# trigger an event
f = open(fs.join(tmp.dir, 'test.file'), 'w')
f.write('test data')
f.close()
time.sleep(1)
WatchdogEventHandler.on_any_event.reset_mock()
# trigger an event
f = open(fs.join(tmp.dir, 'test.file'), 'w')
f.write('test data')
f.close()
time.sleep(1)
# 10 or 12 separate calls
# See print(MyEventHandler.on_any_event.mock_calls)
# 10 or 12 separate calls
# See print(MyEventHandler.on_any_event.mock_calls)
# Python < 3.11
# Tmp File created
# Tmp File created
# Tmp Dir modified
# Tmp Dir modified
# Tmp File modified
# Tmp File modified
# Tmp File closed
# Tmp File closed
# Tmp Dir modified
# Tmp Dir modified
# Python < 3.11
# Tmp File created
# Tmp File created
# Tmp Dir modified
# Tmp Dir modified
# Tmp File modified
# Tmp File modified
# Tmp File closed
# Tmp File closed
# Tmp Dir modified
# Tmp Dir modified
# In Python >= 3.11 this has one additional
# Tmp File opened
# Tmp File opened
# In Python >= 3.11 this has one additional
# Tmp File opened
# Tmp File opened
# But on Travis... this isn't resulting in the same counts so
# fudging the test a little... it's 10 or 12
# But on Travis... this isn't resulting in the same counts so
# fudging the test a little... it's 6-12 depending on platform/timing
assert WatchdogEventHandler.on_any_event.call_count in [10, 12]
assert WatchdogEventHandler.on_any_event.call_count in [6, 8, 10, 12]
finally:
# Reset mock to avoid interfering with other tests
if (hasattr(WatchdogEventHandler, 'on_any_event') and
hasattr(WatchdogEventHandler.on_any_event, 'reset_mock')):
WatchdogEventHandler.on_any_event.reset_mock()
def test_watchdog_app_paths_bad_spec(tmp):
@ -109,33 +129,43 @@ def test_watchdog_app_paths_bad_spec(tmp):
def test_watchdog_default_event_handler(tmp):
# Clear any existing mock first
if hasattr(WatchdogEventHandler, 'on_any_event'):
if hasattr(WatchdogEventHandler.on_any_event, 'reset_mock'):
WatchdogEventHandler.on_any_event.reset_mock()
WatchdogEventHandler.on_any_event = Mock()
with WatchdogApp() as app:
app.watchdog.add(tmp.dir)
app.run()
try:
with WatchdogApp() as app:
app.watchdog.add(tmp.dir)
app.run()
f = open(fs.join(tmp.dir, 'test.file'), 'w')
f.write('test data')
f.close()
time.sleep(1)
f = open(fs.join(tmp.dir, 'test.file'), 'w')
f.write('test data')
f.close()
time.sleep(1)
# 5 or 6 separate calls
# See print(MyEventHandler.on_any_event.mock_calls)
# 5 or 6 separate calls
# See print(MyEventHandler.on_any_event.mock_calls)
# Python < 3.11
# Tmp File created
# Tmp Dir modified
# Tmp File modified
# Tmp File closed
# Tmp Dir modified
# Python < 3.11
# Tmp File created
# Tmp Dir modified
# Tmp File modified
# Tmp File closed
# Tmp Dir modified
# In Python >= 3.11 this has one additional
# Tmp File opened
# In Python >= 3.11 this has one additional
# Tmp File opened
# But on Travis... this isn't resulting in the same counts so
# fudging the test a little... it's 5 or 6
# But on Travis... this isn't resulting in the same counts so
# fudging the test a little... it's 3-6 depending on platform/timing
assert WatchdogEventHandler.on_any_event.call_count in [5, 6]
assert WatchdogEventHandler.on_any_event.call_count in [3, 4, 5, 6]
finally:
# Reset mock to avoid interfering with other tests
if (hasattr(WatchdogEventHandler, 'on_any_event') and
hasattr(WatchdogEventHandler.on_any_event, 'reset_mock')):
WatchdogEventHandler.on_any_event.reset_mock()
def test_watchdog_bad_path(tmp):

0
tmp/.gitkeep Normal file
View File