Compare commits
41 commits
2023.01.16
...
master
Author | SHA1 | Date | |
---|---|---|---|
296618a34e | |||
Benjamin Renard | 28103836ac | ||
Benjamin Renard | 3cf6a2682c | ||
Benjamin Renard | eb87516e1a | ||
Benjamin Renard | 5dbdb0ffe6 | ||
Benjamin Renard | b45819428d | ||
Benjamin Renard | 85caf81ac2 | ||
09c422efe2 | |||
e368521a96 | |||
25cdf9d4dc | |||
4962b16099 | |||
371d194728 | |||
dcaec24ea4 | |||
2736fc30ae | |||
73795d27b8 | |||
07ab4490d2 | |||
68c2103c58 | |||
0064fa979c | |||
b92a814577 | |||
8a0a65465d | |||
8e0e75f30e | |||
14d82fe796 | |||
698fd52a03 | |||
71a49f7b2f | |||
e38e5b10a7 | |||
3a443e1fa5 | |||
44bd9a6446 | |||
f8602801d7 | |||
e8572e2eaa | |||
f597164305 | |||
72877dd13e | |||
ebd73812bc | |||
5693cf8f8a | |||
63d6a6e0ed | |||
73735b378f | |||
c93b3508ed | |||
d75a61b4e8 | |||
93b06d6127 | |||
e71fb28295 | |||
b5df95a2dd | |||
5aa6a0cea4 |
86
.forgejo/workflows/release.yaml
Normal file
86
.forgejo/workflows/release.yaml
Normal file
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
name: Build and publish Debian & Python packages
|
||||
on: ["create"]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker.io/brenard/debian-python-deb:latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build Debian & Python package
|
||||
env:
|
||||
MAINTAINER_NAME: ${{ vars.MAINTAINER_NAME }}
|
||||
MAINTAINER_EMAIL: ${{ vars.MAINTAINER_EMAIL }}
|
||||
DEBIAN_CODENAME: ${{ vars.DEBIAN_CODENAME }}
|
||||
run: |
|
||||
echo "${{ secrets.GPG_KEY }}"|base64 -d|gpg --import
|
||||
./build.sh
|
||||
rm -fr deb_dist/mylib-*
|
||||
- name: Upload Debian & Python package files
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: |
|
||||
dist
|
||||
deb_dist
|
||||
|
||||
publish-forgejo:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker.io/brenard/debian-python-deb:latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Download Debian & Python packages files
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
|
||||
- name: Create the release
|
||||
id: create-release
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir release
|
||||
mv dist/*.whl dist/*.tar.gz release/
|
||||
mv deb_dist/*.deb release/
|
||||
md5sum release/* > md5sum.txt
|
||||
sha512sum release/* > sha512sum.txt
|
||||
mv md5sum.txt sha512sum.txt release/
|
||||
{
|
||||
echo 'release_note<<EOF'
|
||||
cat dist/release_notes.md
|
||||
echo 'EOF'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish release on Forgejo
|
||||
uses: actions/forgejo-release@v1
|
||||
with:
|
||||
direction: upload
|
||||
url: https://gitea.zionetrix.net
|
||||
token: ${{ secrets.forgejo_token }}
|
||||
release-dir: release
|
||||
release-notes: ${{ steps.create-release.outputs.release_note }}
|
||||
|
||||
publish-aptly:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker.io/brenard/aptly-publish:latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: "Download Debian package files"
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
|
||||
- name: "Publish Debian package on Aptly repository"
|
||||
uses: https://gitea.zionetrix.net/bn8/aptly-publish@master
|
||||
with:
|
||||
api_url: ${{ vars.apt_api_url }}
|
||||
api_username: ${{ vars.apt_api_username }}
|
||||
api_password: ${{ secrets.apt_api_password }}
|
||||
repo_name: ${{ vars.apt_repo_name }}
|
||||
path: "deb_dist"
|
||||
source_name: ${{ vars.apt_source_name }}
|
14
.forgejo/workflows/tests.yaml
Normal file
14
.forgejo/workflows/tests.yaml
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
name: Run tests
|
||||
on: [push]
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker.io/brenard/mylib:dev-master
|
||||
options: "--workdir /src"
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
- name: Run tests.sh
|
||||
run: ./tests.sh --no-venv
|
|
@ -1,39 +1,71 @@
|
|||
# Pre-commit hooks to run tests and ensure code is cleaned.
|
||||
# See https://pre-commit.com for more information
|
||||
---
|
||||
repos:
|
||||
- repo: local
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.1.6
|
||||
hooks:
|
||||
- id: pytest
|
||||
name: pytest
|
||||
entry: python3 -m pytest tests
|
||||
language: system
|
||||
types: [python]
|
||||
pass_filenames: false
|
||||
- repo: local
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.15.0
|
||||
hooks:
|
||||
- id: pylint
|
||||
- id: pyupgrade
|
||||
args: ["--keep-percent-format", "--py37-plus"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.11.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ["--target-version", "py37", "--line-length", "100"]
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black", "--line-length", "100"]
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ["--max-line-length=100"]
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.2
|
||||
hooks:
|
||||
- id: codespell
|
||||
args:
|
||||
- --ignore-words-list=exten
|
||||
- --skip="./.*,*.csv,*.json,*.ini,*.subject,*.txt,*.html,*.log,*.conf"
|
||||
- --quiet-level=2
|
||||
- --ignore-regex=.*codespell-ignore$
|
||||
# - --write-changes # Uncomment to write changes
|
||||
exclude_types: [csv, json]
|
||||
- repo: https://github.com/adrienverge/yamllint
|
||||
rev: v1.32.0
|
||||
hooks:
|
||||
- id: yamllint
|
||||
ignore: .github/
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.7.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
args: ["--print-width", "100"]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: pylint --extension-pkg-whitelist=cx_Oracle
|
||||
entry: ./.pre-commit-pylint --extension-pkg-whitelist=cx_Oracle
|
||||
language: system
|
||||
types: [python]
|
||||
require_serial: true
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.5
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ['--max-line-length=100']
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
- id: bandit
|
||||
args: [--skip, "B101", --recursive, "mylib"]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ['--keep-percent-format', '--py37-plus']
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ['--target-version', 'py37', '--line-length', '100']
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.11.4
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ['--profile', 'black', '--line-length', '100']
|
||||
- id: pytest
|
||||
name: pytest
|
||||
entry: ./.pre-commit-pytest tests
|
||||
language: system
|
||||
types: [python]
|
||||
pass_filenames: false
|
||||
|
|
21
.pre-commit-pylint
Executable file
21
.pre-commit-pylint
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
|
||||
PWD=`pwd`
|
||||
|
||||
if [ -d "$PWD/venv" ]
|
||||
then
|
||||
echo "Run pylint inside venv ($PWD/venv)..."
|
||||
[ ! -e "$PWD/venv/bin/pylint" ] && $PWD/venv/bin/python -m pip install pylint
|
||||
$PWD/venv/bin/pylint "$@"
|
||||
exit $?
|
||||
elif [ -e "$PWD/pyproject.toml" ]
|
||||
then
|
||||
echo "Run pylint using poetry..."
|
||||
poetry run pylint --version > /dev/null 2>&1 || poetry run python -m pip install pylint
|
||||
poetry run pylint "$@"
|
||||
exit $?
|
||||
else
|
||||
echo "Run pylint at system scope..."
|
||||
pylint "$@"
|
||||
exit $?
|
||||
fi
|
21
.pre-commit-pytest
Executable file
21
.pre-commit-pytest
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
|
||||
PWD=`pwd`
|
||||
|
||||
if [ -d "$PWD/venv" ]
|
||||
then
|
||||
echo "Run pytest inside venv ($PWD/venv)..."
|
||||
[ ! -e "$PWD/venv/bin/pytest" ] && $PWD/venv/bin/python -m pip install pytest
|
||||
$PWD/venv/bin/pytest "$@"
|
||||
exit $?
|
||||
elif [ -e "$PWD/pyproject.toml" ]
|
||||
then
|
||||
echo "Run pytest using poetry..."
|
||||
poetry run pytest --version > /dev/null 2>&1 || poetry run python -m pip install pytest
|
||||
poetry run pytest "$@"
|
||||
exit $?
|
||||
else
|
||||
echo "Run pytest at system scope..."
|
||||
pytest "$@"
|
||||
exit $?
|
||||
fi
|
|
@ -8,6 +8,8 @@ disable=invalid-name,
|
|||
too-many-nested-blocks,
|
||||
too-many-instance-attributes,
|
||||
too-many-lines,
|
||||
too-many-statements,
|
||||
logging-too-many-args,
|
||||
duplicate-code,
|
||||
|
||||
[FORMAT]
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
clone:
|
||||
git:
|
||||
image: woodpeckerci/plugin-git
|
||||
tags: true
|
||||
|
||||
pipeline:
|
||||
test:
|
||||
image: brenard/mylib:dev-master
|
||||
commands:
|
||||
- ./tests.sh --no-venv
|
||||
|
||||
build:
|
||||
image: brenard/debian-python-deb
|
||||
when:
|
||||
event: tag
|
||||
commands:
|
||||
- echo "$GPG_KEY"|base64 -d|gpg --import
|
||||
- ./build.sh --quiet
|
||||
- rm -fr deb_dist/mylib-*
|
||||
secrets: [ maintainer_name, maintainer_email, gpg_key, debian_codename ]
|
||||
|
||||
publish-dryrun:
|
||||
group: publish
|
||||
image: alpine
|
||||
when:
|
||||
event: tag
|
||||
commands:
|
||||
- ls dist/*
|
||||
- ls deb_dist/*
|
||||
|
||||
publish-gitea:
|
||||
group: publish
|
||||
image: plugins/gitea-release
|
||||
when:
|
||||
event: tag
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: gitea_token
|
||||
base_url: https://gitea.zionetrix.net
|
||||
note: dist/release_notes.md
|
||||
files:
|
||||
- dist/*
|
||||
- deb_dist/*.deb
|
||||
checksum:
|
||||
- md5
|
||||
- sha512
|
||||
|
||||
publish-apt:
|
||||
group: publish
|
||||
image: brenard/aptly-publish
|
||||
when:
|
||||
event: tag
|
||||
settings:
|
||||
api_url:
|
||||
from_secret: apt_api_url
|
||||
api_username:
|
||||
from_secret: apt_api_username
|
||||
api_password:
|
||||
from_secret: apt_api_password
|
||||
repo_name:
|
||||
from_secret: apt_repo_name
|
||||
path: deb_dist
|
||||
source_name: mylib
|
25
README.md
25
README.md
|
@ -35,35 +35,35 @@ Just run `pip install git+https://gitea.zionetrix.net/bn8/python-mylib.git`
|
|||
|
||||
Just run `python setup.py install`
|
||||
|
||||
**Note:** This project could previously use as independent python files (not as module). This old version is keep in *legacy* git branch (not maintained).
|
||||
**Note:** This project could previously use as independent python files (not as module). This old version is keep in _legacy_ git branch (not maintained).
|
||||
|
||||
## Include libs
|
||||
|
||||
* **mylib.email.EmailClient:** An email client to forge (eventually using template) and send email via a SMTP server
|
||||
* **mylib.ldap.LdapServer:** A small lib to make requesting LDAP server easier. It's also provide some helper functions to deal with LDAP date string.
|
||||
* **mylib.mysql.MyDB:** An extra small lib to remember me how to interact with MySQL/MariaDB database
|
||||
* **mylib.pgsql.PgDB:** An small lib to remember me how to interact with PostgreSQL database. **Warning:** The insert/update/delete/select methods demonstrate how to forge raw SQL request, but **it's a bad idea**: Prefer using prepared query.
|
||||
* **mylib.opening_hours:** A set of helper functions to deal with french opening hours (including normal opening hours, exceptional closure and nonworking public holidays).
|
||||
* **mylib.pbar.Pbar:** A small lib for progress bar
|
||||
* **mylib.report.Report:** A small lib to implement logging based email report send at exit
|
||||
- **mylib.email.EmailClient:** An email client to forge (eventually using template) and send email via a SMTP server
|
||||
- **mylib.ldap.LdapServer:** A small lib to make requesting LDAP server easier. It's also provide some helper functions to deal with LDAP date string.
|
||||
- **mylib.mysql.MyDB:** An extra small lib to remember me how to interact with MySQL/MariaDB database
|
||||
- **mylib.pgsql.PgDB:** An small lib to remember me how to interact with PostgreSQL database. **Warning:** The insert/update/delete/select methods demonstrate how to forge raw SQL request, but **it's a bad idea**: Prefer using prepared query.
|
||||
- **mylib.opening_hours:** A set of helper functions to deal with french opening hours (including normal opening hours, exceptional closure and nonworking public holidays).
|
||||
- **mylib.pbar.Pbar:** A small lib for progress bar
|
||||
- **mylib.report.Report:** A small lib to implement logging based email report send at exit
|
||||
|
||||
To know how to use these libs, you can take a look on *mylib.scripts* content or in *tests* directory.
|
||||
To know how to use these libs, you can take a look on _mylib.scripts_ content or in _tests_ directory.
|
||||
|
||||
## Code Style
|
||||
|
||||
[pylint](https://pypi.org/project/pylint/) is used to check for errors and enforces a coding standard, using thoses parameters:
|
||||
[pylint](https://pypi.org/project/pylint/) is used to check for errors and enforces a coding standard, using those parameters:
|
||||
|
||||
```bash
|
||||
pylint --extension-pkg-whitelist=cx_Oracle
|
||||
```
|
||||
|
||||
[flake8](https://pypi.org/project/flake8/) is also used to check for errors and enforces a coding standard, using thoses parameters:
|
||||
[flake8](https://pypi.org/project/flake8/) is also used to check for errors and enforces a coding standard, using those parameters:
|
||||
|
||||
```bash
|
||||
flake8 --max-line-length=100
|
||||
```
|
||||
|
||||
[black](https://pypi.org/project/black/) is used to format the code, using thoses parameters:
|
||||
[black](https://pypi.org/project/black/) is used to format the code, using those parameters:
|
||||
|
||||
```bash
|
||||
black --target-version py37 --line-length 100
|
||||
|
@ -83,7 +83,6 @@ pyupgrade --keep-percent-format --py37-plus
|
|||
|
||||
**Note:** There is `.pre-commit-config.yaml` to use [pre-commit](https://pre-commit.com/) to automatically run these tools before commits. After cloning the repository, execute `pre-commit install` to install the git hook.
|
||||
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright (c) 2013-2021 Benjamin Renard <brenard@zionetrix.net>
|
||||
|
|
37
build.sh
37
build.sh
|
@ -9,11 +9,27 @@ cd $( dirname $0 )
|
|||
echo "Clean previous build..."
|
||||
rm -fr dist deb_dist
|
||||
|
||||
if [ -n "$CI" -a $UID -eq 0 ]
|
||||
then
|
||||
echo "CI environment detected, set current directory as git safe for root"
|
||||
git config --global --add safe.directory $(pwd)
|
||||
fi
|
||||
|
||||
echo "Detect version using git describe..."
|
||||
VERSION="$( git describe --tags|sed 's/^[^0-9]*//' )"
|
||||
|
||||
echo "Set version=$VERSION in setup.py using sed..."
|
||||
sed -i "s/^version *=.*$/version = '$VERSION'/" setup.py
|
||||
echo "Computing python version..."
|
||||
if [ $( echo "$VERSION"|grep -c "-" ) -gt 0 ]
|
||||
then
|
||||
echo "Development version detected ($VERSION), compute custom python dev version"
|
||||
PY_VERSION="$( echo "$VERSION"|sed 's/-\([0-9]\)\+-.*$/.dev\1/' )"
|
||||
else
|
||||
echo "Clean tagged version detected, use it"
|
||||
PY_VERSION="$VERSION"
|
||||
fi
|
||||
|
||||
echo "Set version=$PY_VERSION in setup.py using sed..."
|
||||
sed -i "s/^version *=.*$/version = '$PY_VERSION'/" setup.py
|
||||
|
||||
if [ -d venv ]
|
||||
then
|
||||
|
@ -65,13 +81,17 @@ cd deb_dist/mylib-$VERSION
|
|||
|
||||
if [ -z "$DEBIAN_CODENAME" ]
|
||||
then
|
||||
echo "Retreive debian codename using lsb_release..."
|
||||
echo "Retrieve debian codename using lsb_release..."
|
||||
DEBIAN_CODENAME=$( lsb_release -c -s )
|
||||
[ $( lsb_release -r -s ) -ge 9 ] && DEBIAN_CODENAME="${DEBIAN_CODENAME}-ee"
|
||||
else
|
||||
echo "Use debian codename from environment ($DEBIAN_CODENAME)"
|
||||
fi
|
||||
|
||||
# Compute debian package version
|
||||
DEB_VERSION_SUFFIX="-1"
|
||||
DEB_VERSION="$VERSION$DEB_VERSION_SUFFIX"
|
||||
|
||||
echo "Generate debian changelog using gitdch..."
|
||||
GITDCH_ARGS=('--verbose')
|
||||
[ -n "$QUIET_ARG" ] && GITDCH_ARGS=('--warning')
|
||||
|
@ -87,11 +107,20 @@ then
|
|||
fi
|
||||
$GITDCH \
|
||||
--package-name mylib \
|
||||
--version "${VERSION}" \
|
||||
--version "${DEB_VERSION}" \
|
||||
--version-suffix "${DEB_VERSION_SUFFIX}" \
|
||||
--code-name $DEBIAN_CODENAME \
|
||||
--output debian/changelog \
|
||||
--release-notes ../../dist/release_notes.md \
|
||||
--path ../../ \
|
||||
--exclude "^CI: " \
|
||||
--exclude "^Docker: " \
|
||||
--exclude "^pre-commit: " \
|
||||
--exclude "\.?woodpecker(\.yml)?" \
|
||||
--exclude "build(\.sh)?" \
|
||||
--exclude "tests(\.sh)?" \
|
||||
--exclude "README(\.md)?" \
|
||||
--exclude "^Merge branch " \
|
||||
"${GITDCH_ARGS[@]}"
|
||||
|
||||
echo "Add custom package name for dependencies..."
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
FROM brenard/mylib:latest
|
||||
RUN apt-get remove -y python3-mylib
|
||||
RUN python3 -m pip install -U git+https://gitea.zionetrix.net/bn8/python-mylib.git
|
||||
RUN git clone https://gitea.zionetrix.net/bn8/python-mylib.git /usr/local/src/python-mylib && pip install /usr/local/src/python-mylib[dev]
|
||||
RUN cd /usr/local/src/python-mylib && pre-commit run --all-files
|
||||
RUN apt-get remove -y python3-mylib && \
|
||||
git clone https://gitea.zionetrix.net/bn8/python-mylib.git /src && \
|
||||
pip install --break-system-packages /src[dev] && \
|
||||
cd /src && \
|
||||
pre-commit run --all-files
|
||||
|
|
|
@ -1,5 +1,26 @@
|
|||
FROM debian:latest
|
||||
RUN echo "deb http://debian.zionetrix.net stable main" > /etc/apt/sources.list.d/zionetrix.list && apt-get -o Acquire::AllowInsecureRepositories=true -o Acquire::AllowDowngradeToInsecureRepositories=true update && apt-get -o APT::Get::AllowUnauthenticated=true install --yes zionetrix-archive-keyring && apt-get clean && rm -fr rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y python3-all python3-dev python3-pip python3-venv python3-mylib build-essential git libldap2-dev libsasl2-dev pkg-config libsystemd-dev libpq-dev libmariadb-dev wget unzip && apt-get clean && rm -fr rm -rf /var/lib/apt/lists/*
|
||||
RUN python3 -m pip install pylint pytest flake8 flake8-junit-report pylint-junit junitparser pre-commit
|
||||
RUN wget --no-verbose -O /opt/instantclient-basic-linux.x64-21.4.0.0.0dbru.zip https://download.oracle.com/otn_software/linux/instantclient/214000/instantclient-basic-linux.x64-21.4.0.0.0dbru.zip && unzip -qq -d /opt /opt/instantclient-basic-linux.x64-21.4.0.0.0dbru.zip && echo /opt/instantclient_* > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
|
||||
FROM node:16-bookworm-slim
|
||||
RUN echo "deb http://debian.zionetrix.net stable main" > /etc/apt/sources.list.d/zionetrix.list && \
|
||||
apt-get \
|
||||
-o Acquire::AllowInsecureRepositories=true \
|
||||
-o Acquire::AllowDowngradeToInsecureRepositories=true \
|
||||
update && \
|
||||
apt-get \
|
||||
-o APT::Get::AllowUnauthenticated=true \
|
||||
install --yes zionetrix-archive-keyring && \
|
||||
apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y \
|
||||
python3-all python3-dev python3-pip python3-venv python3-mylib build-essential git \
|
||||
libldap2-dev libsasl2-dev \
|
||||
pkg-config libsystemd-dev \
|
||||
libpq-dev libmariadb-dev \
|
||||
wget unzip && \
|
||||
apt-get clean && \
|
||||
rm -fr rm -rf /var/lib/apt/lists/*
|
||||
RUN python3 -m pip install --break-system-packages pylint pytest flake8 flake8-junit-report pylint-junit junitparser pre-commit
|
||||
RUN wget --no-verbose \
|
||||
-O /opt/instantclient-basic-linux.x64-21.4.0.0.0dbru.zip \
|
||||
https://download.oracle.com/otn_software/linux/instantclient/214000/instantclient-basic-linux.x64-21.4.0.0.0dbru.zip && \
|
||||
unzip -qq -d /opt /opt/instantclient-basic-linux.x64-21.4.0.0.0dbru.zip && \
|
||||
echo /opt/instantclient_* > /etc/ld.so.conf.d/oracle-instantclient.conf && \
|
||||
ldconfig
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
""" Some really common helper functions """
|
||||
|
||||
#
|
||||
# Pretty formating helpers
|
||||
# Pretty formatting helpers
|
||||
#
|
||||
|
||||
|
||||
|
@ -11,7 +11,7 @@ def increment_prefix(prefix):
|
|||
|
||||
|
||||
def pretty_format_value(value, encoding="utf8", prefix=None):
|
||||
"""Returned pretty formated value to display"""
|
||||
"""Returned pretty formatted value to display"""
|
||||
if isinstance(value, dict):
|
||||
return pretty_format_dict(value, encoding=encoding, prefix=prefix)
|
||||
if isinstance(value, list):
|
||||
|
@ -27,10 +27,10 @@ def pretty_format_value(value, encoding="utf8", prefix=None):
|
|||
|
||||
def pretty_format_value_in_list(value, encoding="utf8", prefix=None):
|
||||
"""
|
||||
Returned pretty formated value to display in list
|
||||
Returned pretty formatted value to display in list
|
||||
|
||||
That method will prefix value with line return and incremented prefix
|
||||
if pretty formated value contains line return.
|
||||
if pretty formatted value contains line return.
|
||||
"""
|
||||
prefix = prefix if prefix else ""
|
||||
value = pretty_format_value(value, encoding, prefix)
|
||||
|
@ -41,7 +41,7 @@ def pretty_format_value_in_list(value, encoding="utf8", prefix=None):
|
|||
|
||||
|
||||
def pretty_format_dict(value, encoding="utf8", prefix=None):
|
||||
"""Returned pretty formated dict to display"""
|
||||
"""Returned pretty formatted dict to display"""
|
||||
prefix = prefix if prefix else ""
|
||||
result = []
|
||||
for key in sorted(value.keys()):
|
||||
|
@ -53,7 +53,7 @@ def pretty_format_dict(value, encoding="utf8", prefix=None):
|
|||
|
||||
|
||||
def pretty_format_list(row, encoding="utf8", prefix=None):
|
||||
"""Returned pretty formated list to display"""
|
||||
"""Returned pretty formatted list to display"""
|
||||
prefix = prefix if prefix else ""
|
||||
result = []
|
||||
for idx, values in enumerate(row):
|
||||
|
@ -62,3 +62,26 @@ def pretty_format_list(row, encoding="utf8", prefix=None):
|
|||
+ pretty_format_value_in_list(values, encoding=encoding, prefix=prefix)
|
||||
)
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def pretty_format_timedelta(timedelta):
|
||||
"""Format timedelta object"""
|
||||
seconds = int(timedelta.total_seconds())
|
||||
if seconds < 1:
|
||||
return "less than one second"
|
||||
periods = [
|
||||
("year", 60 * 60 * 24 * 365),
|
||||
("month", 60 * 60 * 24 * 30),
|
||||
("day", 60 * 60 * 24),
|
||||
("hour", 60 * 60),
|
||||
("minute", 60),
|
||||
("second", 1),
|
||||
]
|
||||
|
||||
strings = []
|
||||
for period_name, period_seconds in periods:
|
||||
if seconds >= period_seconds:
|
||||
period_value, seconds = divmod(seconds, period_seconds)
|
||||
strings.append(f'{period_value} {period_name}{"s" if period_value > 1 else ""}')
|
||||
|
||||
return ", ".join(strings)
|
||||
|
|
486
mylib/config.py
486
mylib/config.py
|
@ -6,7 +6,6 @@ import argparse
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
import textwrap
|
||||
import traceback
|
||||
|
@ -24,7 +23,14 @@ log = logging.getLogger(__name__)
|
|||
# Constants
|
||||
DEFAULT_ENCODING = "utf-8"
|
||||
DEFAULT_CONFIG_DIRPATH = os.path.expanduser("./")
|
||||
DEFAULT_CONSOLE_LOG_FORMAT = "%(asctime)s - %(module)s:%(lineno)d - %(levelname)s - %(message)s"
|
||||
DEFAULT_CONFIG_FILE_MODE = 0o600
|
||||
DEFAULT_LOG_FORMAT = "%(asctime)s - %(module)s:%(lineno)d - %(levelname)s - %(message)s"
|
||||
DEFAULT_CONSOLE_LOG_FORMAT = DEFAULT_LOG_FORMAT
|
||||
DEFAULT_FILELOG_FORMAT = DEFAULT_LOG_FORMAT
|
||||
|
||||
|
||||
class ConfigException(BaseException):
|
||||
"""Configuration exception"""
|
||||
|
||||
|
||||
class BaseOption: # pylint: disable=too-many-instance-attributes
|
||||
|
@ -116,6 +122,10 @@ class BaseOption: # pylint: disable=too-many-instance-attributes
|
|||
|
||||
self._set = True
|
||||
|
||||
def set_default(self, default_value):
|
||||
"""Set option default value"""
|
||||
self.default = default_value
|
||||
|
||||
@property
|
||||
def parser_action(self):
|
||||
"""Get action as accept by argparse.ArgumentParser"""
|
||||
|
@ -157,12 +167,12 @@ class BaseOption: # pylint: disable=too-many-instance-attributes
|
|||
args = [self.parser_argument_name]
|
||||
if self.short_arg:
|
||||
args.append(self.short_arg)
|
||||
kwargs = dict(
|
||||
action=self.parser_action,
|
||||
dest=self.parser_dest,
|
||||
help=self.parser_help,
|
||||
default=self.default,
|
||||
)
|
||||
kwargs = {
|
||||
"action": self.parser_action,
|
||||
"dest": self.parser_dest,
|
||||
"help": self.parser_help,
|
||||
"default": self.default,
|
||||
}
|
||||
if self.parser_type: # pylint: disable=using-constant-test
|
||||
kwargs["type"] = self.parser_type
|
||||
|
||||
|
@ -239,12 +249,16 @@ class BaseOption: # pylint: disable=too-many-instance-attributes
|
|||
|
||||
def ask_value(self, set_it=True):
|
||||
"""
|
||||
Ask to user to enter value of this option and set or
|
||||
return it regarding set parameter
|
||||
Ask to user to enter value of this option and set it if set_it parameter is True
|
||||
|
||||
:param set_it: If True (default), option value will be updated with user input
|
||||
|
||||
:return: The configuration option value.
|
||||
:rtype: mixed
|
||||
"""
|
||||
value = self._ask_value()
|
||||
if set_it:
|
||||
return self.set(value)
|
||||
self.set(value)
|
||||
return value
|
||||
|
||||
|
||||
|
@ -375,6 +389,59 @@ class IntegerOption(BaseOption):
|
|||
print("Invalid answer. Must a integer value")
|
||||
|
||||
|
||||
class OctalOption(BaseOption):
|
||||
"""Octal configuration option class"""
|
||||
|
||||
@staticmethod
|
||||
def octal(value):
|
||||
"""Convert configuration octal string as integer"""
|
||||
return int(str(value), 8)
|
||||
|
||||
@staticmethod
|
||||
def octal_string(value):
|
||||
"""Convert integer to configuration octal string"""
|
||||
return oct(value)[2:]
|
||||
|
||||
@property
|
||||
def _from_config(self):
|
||||
"""Get option value from ConfigParser"""
|
||||
return self.octal(self.config.config_parser.getint(self.section.name, self.name))
|
||||
|
||||
def to_config(self, value=None):
|
||||
"""Format value as stored in configuration file"""
|
||||
value = value if value is not None else self.get()
|
||||
return self.octal_string(value) if value is not None else ""
|
||||
|
||||
@property
|
||||
def parser_type(self):
|
||||
return self.octal
|
||||
|
||||
@property
|
||||
def parser_help(self):
|
||||
"""Get option help message in arguments parser options"""
|
||||
if self.arg_help and self.default is not None:
|
||||
# pylint: disable=consider-using-f-string
|
||||
return "{} (Default: {})".format(
|
||||
self.arg_help,
|
||||
re.sub(r"%([^%])", r"%%\1", self.octal_string(self._default_in_config)),
|
||||
)
|
||||
if self.arg_help:
|
||||
return self.arg_help
|
||||
return None
|
||||
|
||||
def _ask_value(self, prompt=None, **kwargs):
|
||||
"""Ask to user to enter value of this option and return it"""
|
||||
default_value = kwargs.pop("default_value", self.get())
|
||||
while True:
|
||||
value = super()._ask_value(prompt, default_value=default_value, **kwargs)
|
||||
if value in ["", None, default_value]:
|
||||
return default_value
|
||||
try:
|
||||
return self.octal(value)
|
||||
except ValueError:
|
||||
print("Invalid answer. Must an octal value")
|
||||
|
||||
|
||||
class PasswordOption(StringOption):
|
||||
"""Password configuration option class"""
|
||||
|
||||
|
@ -402,7 +469,7 @@ class PasswordOption(StringOption):
|
|||
|
||||
service_name = self._keyring_service_name
|
||||
username = self._keyring_username
|
||||
log.debug("Retreive password %s for username=%s from keyring", service_name, username)
|
||||
log.debug("Retrieve password %s for username=%s from keyring", service_name, username)
|
||||
value = keyring.get_password(service_name, username)
|
||||
|
||||
if value is None:
|
||||
|
@ -447,8 +514,12 @@ class PasswordOption(StringOption):
|
|||
|
||||
def ask_value(self, set_it=True):
|
||||
"""
|
||||
Ask to user to enter value of this option and set or
|
||||
return it regarding set parameter
|
||||
Ask to user to enter value of this option and set it if set_it parameter is True
|
||||
|
||||
:param set_it: If True (default), option value will be updated with user input
|
||||
|
||||
:return: The configuration option value.
|
||||
:rtype: mixed
|
||||
"""
|
||||
value = self._ask_value()
|
||||
if set_it:
|
||||
|
@ -467,7 +538,7 @@ class PasswordOption(StringOption):
|
|||
use_keyring = False
|
||||
else:
|
||||
print("Invalid answer. Possible values: Y or N (case insensitive)")
|
||||
return self.set(value, use_keyring=use_keyring)
|
||||
self.set(value, use_keyring=use_keyring)
|
||||
return value
|
||||
|
||||
|
||||
|
@ -511,6 +582,16 @@ class ConfigSection:
|
|||
assert self.defined(option), f"Option {option} unknown"
|
||||
return self.options[option].set(value)
|
||||
|
||||
def set_default(self, option, default_value):
|
||||
"""Set default option value"""
|
||||
assert self.defined(option), f"Option {option} unknown"
|
||||
return self.options[option].set_default(default_value)
|
||||
|
||||
def set_defaults(self, **default_values):
|
||||
"""Set default options value"""
|
||||
for option, default_value in default_values.items():
|
||||
self.set_default(option, default_value)
|
||||
|
||||
def add_options_to_parser(self, parser):
|
||||
"""Add section to argparse.ArgumentParser"""
|
||||
assert isinstance(parser, argparse.ArgumentParser)
|
||||
|
@ -537,28 +618,32 @@ class ConfigSection:
|
|||
|
||||
:param set_it: If True (default), option value will be updated with user input
|
||||
|
||||
:return: If set_it is True, return True if valid value for each configuration
|
||||
option have been retrieved and set. If False, return a dict of configuration
|
||||
options and their value.
|
||||
:return: a dict of configuration options and their value.
|
||||
:rtype: bool of dict
|
||||
"""
|
||||
if self.comment:
|
||||
print(f"# {self.comment}")
|
||||
print(f"[{self.name}]\n")
|
||||
result = {}
|
||||
error = False
|
||||
for name, option in self.options.items():
|
||||
option_result = option.ask_value(set_it=set_it)
|
||||
if set_it:
|
||||
result[name] = option_result
|
||||
elif not option_result:
|
||||
error = True
|
||||
result[name] = option.ask_value(set_it=set_it)
|
||||
print()
|
||||
print()
|
||||
if set_it:
|
||||
return not error
|
||||
return result
|
||||
|
||||
def ask_value(self, option, set_it=True):
|
||||
"""
|
||||
Ask user to enter value for the specified configuration option of the section
|
||||
|
||||
:param options: The configuration option name
|
||||
:param set_it: If True (default), option value will be updated with user input
|
||||
|
||||
:return: The configuration option value.
|
||||
:rtype: mixed
|
||||
"""
|
||||
assert self.defined(option), f"Option {option} unknown"
|
||||
return self.options[option].ask_value(set_it=set_it)
|
||||
|
||||
|
||||
class RawWrappedTextHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
||||
"""
|
||||
|
@ -593,6 +678,8 @@ class Config: # pylint: disable=too-many-instance-attributes
|
|||
encoding=None,
|
||||
config_file_env_variable=None,
|
||||
default_config_dirpath=None,
|
||||
default_config_filename=None,
|
||||
default_config_file_mode=None,
|
||||
):
|
||||
self.appname = appname
|
||||
self.shortname = shortname
|
||||
|
@ -607,8 +694,68 @@ class Config: # pylint: disable=too-many-instance-attributes
|
|||
self._filepath = None
|
||||
self.config_file_env_variable = config_file_env_variable
|
||||
self.default_config_dirpath = default_config_dirpath
|
||||
self.default_config_filename = default_config_filename
|
||||
self.default_config_file_mode = default_config_file_mode or DEFAULT_CONFIG_FILE_MODE
|
||||
self.add_logging_sections()
|
||||
self._init_config_parser()
|
||||
|
||||
def add_logging_sections(self):
|
||||
"""Add logging sections"""
|
||||
console_section = self.add_section("console", comment="Console logging", order=998)
|
||||
console_section.add_option(
|
||||
BooleanOption,
|
||||
"enabled",
|
||||
default=False,
|
||||
arg="--console",
|
||||
short_arg="-C",
|
||||
comment="Enable/disable console log",
|
||||
)
|
||||
console_section.add_option(
|
||||
BooleanOption,
|
||||
"force_stderr",
|
||||
default=False,
|
||||
arg="--console-stderr",
|
||||
comment="Force console log on stderr",
|
||||
)
|
||||
console_section.add_option(
|
||||
StringOption,
|
||||
"log_format",
|
||||
default=DEFAULT_CONSOLE_LOG_FORMAT,
|
||||
arg="--console-log-format",
|
||||
comment="Console log format",
|
||||
)
|
||||
console_section.add_option(
|
||||
StringOption,
|
||||
"log_level",
|
||||
comment=(
|
||||
"Console log level limit : by default, all logged messages (according to main log "
|
||||
"level) will be logged to the console, but you can set a minimal level if you "
|
||||
# logging.getLevelNamesMapping() not available in python 3.9
|
||||
# pylint: disable=protected-access
|
||||
f"want. Possible values: {', '.join(logging._nameToLevel)}."
|
||||
),
|
||||
)
|
||||
|
||||
logfile_section = self.add_section("logfile", comment="Logging file", order=999)
|
||||
logfile_section.add_option(StringOption, "path", comment="File log path")
|
||||
logfile_section.add_option(
|
||||
StringOption,
|
||||
"format",
|
||||
default=DEFAULT_FILELOG_FORMAT,
|
||||
comment="File log format",
|
||||
)
|
||||
logfile_section.add_option(
|
||||
StringOption,
|
||||
"level",
|
||||
comment=(
|
||||
"File log level limit : by default, all logged messages (according to main log "
|
||||
"level) will be logged to the log file, but you can set a minimal level if you "
|
||||
# logging.getLevelNamesMapping() not available in python 3.9
|
||||
# pylint: disable=protected-access
|
||||
f"want. Possible values: {', '.join(logging._nameToLevel)}."
|
||||
),
|
||||
)
|
||||
|
||||
def add_section(self, name, loaded_callback=None, **kwargs):
|
||||
"""
|
||||
Add section
|
||||
|
@ -624,7 +771,7 @@ class Config: # pylint: disable=too-many-instance-attributes
|
|||
self.sections[name] = ConfigSection(self, name, **kwargs)
|
||||
if loaded_callback:
|
||||
self._loaded_callbacks.append(loaded_callback)
|
||||
# If configuration is already loaded, execute callback immediatly
|
||||
# If configuration is already loaded, execute callback immediately
|
||||
if self._filepath or self.options:
|
||||
self._loaded()
|
||||
return self.sections[name]
|
||||
|
@ -660,6 +807,18 @@ class Config: # pylint: disable=too-many-instance-attributes
|
|||
self._init_config_parser()
|
||||
self.sections[section].set(option, value)
|
||||
|
||||
def set_default(self, section, option, default_value):
|
||||
"""Set default option value"""
|
||||
assert self.defined(section, option), f"Unknown option {section}.{option}"
|
||||
self._init_config_parser()
|
||||
self.sections[section].set_default(option, default_value)
|
||||
|
||||
def set_defaults(self, section, **default_values):
|
||||
"""Set default options value"""
|
||||
assert section in self.sections, f"Unknown section {section}"
|
||||
self._init_config_parser()
|
||||
self.sections[section].set_defaults(**default_values)
|
||||
|
||||
def _init_config_parser(self, force=False):
|
||||
"""Initialize ConfigParser object"""
|
||||
if not self.config_parser or force:
|
||||
|
@ -711,7 +870,7 @@ class Config: # pylint: disable=too-many-instance-attributes
|
|||
self._loaded_callbacks_executed.append(callback)
|
||||
return not error
|
||||
|
||||
def save(self, filepath=None):
|
||||
def save(self, filepath=None, reload=True):
|
||||
"""Save configuration file"""
|
||||
filepath = filepath if filepath else self._filepath
|
||||
assert filepath, "Configuration filepath is not set or provided"
|
||||
|
@ -737,11 +896,12 @@ class Config: # pylint: disable=too-many-instance-attributes
|
|||
fd.write("\n".join(lines).encode(self.encoding))
|
||||
|
||||
# Privacy!
|
||||
os.chmod(filepath, stat.S_IRUSR | stat.S_IWUSR)
|
||||
os.chmod(filepath, self.default_config_file_mode)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.exception("Failed to write generated configuration file %s", filepath)
|
||||
return False
|
||||
self.load_file(filepath)
|
||||
if reload:
|
||||
return self.load_file(filepath)
|
||||
return True
|
||||
|
||||
@property
|
||||
|
@ -794,30 +954,6 @@ class Config: # pylint: disable=too-many-instance-attributes
|
|||
"-v", "--verbose", action="store_true", help="Show verbose messages"
|
||||
)
|
||||
|
||||
section = self.add_section("console", comment="Console logging")
|
||||
section.add_option(
|
||||
BooleanOption,
|
||||
"enabled",
|
||||
default=False,
|
||||
arg="--console",
|
||||
short_arg="-C",
|
||||
comment="Enable/disable console log",
|
||||
)
|
||||
section.add_option(
|
||||
BooleanOption,
|
||||
"force_stderr",
|
||||
default=False,
|
||||
arg="--console-stderr",
|
||||
comment="Force console log on stderr",
|
||||
)
|
||||
section.add_option(
|
||||
StringOption,
|
||||
"log_format",
|
||||
default=DEFAULT_CONSOLE_LOG_FORMAT,
|
||||
arg="--console-log-format",
|
||||
comment="Console log format",
|
||||
)
|
||||
|
||||
self.add_options_to_parser(self.options_parser)
|
||||
|
||||
return self.options_parser
|
||||
|
@ -898,29 +1034,60 @@ class Config: # pylint: disable=too-many-instance-attributes
|
|||
self.set(*opt_info)
|
||||
|
||||
if self.get("console", "enabled"):
|
||||
stdout_console_handler = logging.StreamHandler(
|
||||
sys.stderr if self.get("console", "force_stderr") else sys.stdout
|
||||
console_log_level = (
|
||||
# logging.getLevelNamesMapping() not available in python 3.9
|
||||
# pylint: disable=protected-access
|
||||
logging._nameToLevel.get(self.get("console", "log_level"))
|
||||
if self.get("console", "log_level")
|
||||
else logging.DEBUG
|
||||
)
|
||||
stdout_console_handler.addFilter(StdoutInfoFilter())
|
||||
stdout_console_handler.setLevel(logging.DEBUG)
|
||||
if console_log_level < logging.WARNING:
|
||||
stdout_console_handler = logging.StreamHandler(
|
||||
sys.stderr if self.get("console", "force_stderr") else sys.stdout
|
||||
)
|
||||
stdout_console_handler.addFilter(StdoutInfoFilter())
|
||||
stdout_console_handler.setLevel(console_log_level)
|
||||
|
||||
stderr_console_handler = logging.StreamHandler(sys.stderr)
|
||||
stderr_console_handler.setLevel(logging.WARNING)
|
||||
stderr_console_handler.setLevel(
|
||||
console_log_level if console_log_level > logging.WARNING else logging.WARNING
|
||||
)
|
||||
|
||||
if self.get("console", "log_format"):
|
||||
console_formater = logging.Formatter(self.get("console", "log_format"))
|
||||
stdout_console_handler.setFormatter(console_formater)
|
||||
if console_log_level < logging.WARNING:
|
||||
stdout_console_handler.setFormatter(console_formater)
|
||||
stderr_console_handler.setFormatter(console_formater)
|
||||
|
||||
logging.getLogger().addHandler(stdout_console_handler)
|
||||
if console_log_level < logging.WARNING:
|
||||
logging.getLogger().addHandler(stdout_console_handler)
|
||||
logging.getLogger().addHandler(stderr_console_handler)
|
||||
|
||||
if self.get("logfile", "path"):
|
||||
logfile_handler = logging.FileHandler(self.get("logfile", "path"))
|
||||
logfile_level = (
|
||||
# logging.getLevelNamesMapping() not available in python 3.9
|
||||
# pylint: disable=protected-access
|
||||
logging._nameToLevel.get(self.get("logfile", "level"))
|
||||
if self.get("logfile", "level")
|
||||
else logging.DEBUG
|
||||
)
|
||||
if logfile_level is None:
|
||||
log.fatal("Invalid log file level specified (%s)", self.get("logfile", "level"))
|
||||
sys.exit(1)
|
||||
logfile_handler.setLevel(logfile_level)
|
||||
|
||||
if self.get("logfile", "format"):
|
||||
logfile_formater = logging.Formatter(self.get("logfile", "format"))
|
||||
logfile_handler.setFormatter(logfile_formater)
|
||||
|
||||
logging.getLogger().addHandler(logfile_handler)
|
||||
|
||||
if execute_callback:
|
||||
self._loaded()
|
||||
|
||||
if self.get_option("mylib_config_reconfigure", default=False):
|
||||
if self.ask_values(set_it=True) and self.save():
|
||||
sys.exit(0)
|
||||
self.ask_values(set_it=True)
|
||||
sys.exit(1)
|
||||
|
||||
return options
|
||||
|
@ -946,27 +1113,32 @@ class Config: # pylint: disable=too-many-instance-attributes
|
|||
:param execute_callback: Sections's loaded callbacks will be finally executed
|
||||
(only if set_it is True, default: False)
|
||||
|
||||
:return: If set_it is True, return True if valid value for each configuration
|
||||
option have been retrieved and set. If False, return a dict of configuration
|
||||
section and their options value.
|
||||
:rtype: bool of dict
|
||||
:return: a dict of configuration section and their options value.
|
||||
:rtype: dict
|
||||
"""
|
||||
result = {}
|
||||
error = False
|
||||
for name, section in self.sections.items():
|
||||
section_result = section.ask_values(set_it=set_it)
|
||||
if not set_it:
|
||||
result[name] = section_result
|
||||
elif not section_result:
|
||||
error = True
|
||||
if set_it:
|
||||
if error:
|
||||
return False
|
||||
if execute_callback:
|
||||
self._loaded()
|
||||
return True
|
||||
result[name] = section.ask_values(set_it=set_it)
|
||||
|
||||
if set_it and execute_callback:
|
||||
self._loaded()
|
||||
|
||||
return result
|
||||
|
||||
def ask_value(self, section, option, set_it=True):
|
||||
"""
|
||||
Ask user to enter value for the specified configuration option
|
||||
|
||||
:param section: The configuration section name
|
||||
:param option: The configuration option name
|
||||
:param set_it: If True (default), option value will be updated with user input
|
||||
|
||||
:return: The configuration option value.
|
||||
:rtype: mixed
|
||||
"""
|
||||
assert self.defined(section, option), f"Unknown option {section}.{option}"
|
||||
return self.sections[section].ask_value(option, set_it=set_it)
|
||||
|
||||
def configure(self, argv=None, description=False):
|
||||
"""
|
||||
Entry point of a script you could use to created your configuration file
|
||||
|
@ -1001,41 +1173,49 @@ class Config: # pylint: disable=too-many-instance-attributes
|
|||
dest="validate",
|
||||
help=(
|
||||
"Validate configuration: initialize application to test if provided parameters"
|
||||
" works.\n\nNote: Validation will occured after configuration file creation or"
|
||||
" works.\n\nNote: Validation will occurred after configuration file creation or"
|
||||
" update. On error, re-run with -O/--overwrite parameter to fix it."
|
||||
),
|
||||
)
|
||||
|
||||
options = self.parse_arguments_options(argv, create=False, execute_callback=False)
|
||||
|
||||
def validate():
|
||||
"""Validate configuration file"""
|
||||
print("Validate your configuration...")
|
||||
try:
|
||||
if self.load_file(options.config):
|
||||
print("Your configuration seem valid.")
|
||||
else:
|
||||
print("Error(s) occurred validating your configuration. See logs for details.")
|
||||
sys.exit(1)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
print(
|
||||
"Exception occurred validating your configuration:\n"
|
||||
f"{traceback.format_exc()}"
|
||||
"\n\nSee logs for details."
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
if os.path.exists(options.config) and not options.overwrite:
|
||||
print(f"Configuration file {options.config} already exists")
|
||||
print(
|
||||
f"Configuration file {options.config} already exists. "
|
||||
"Use -O/--overwrite parameter to overwrite it."
|
||||
)
|
||||
if options.validate:
|
||||
validate()
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
|
||||
if options.interactive:
|
||||
self.ask_values(set_it=True)
|
||||
|
||||
if self.save(options.config):
|
||||
if self.save(options.config, reload=False):
|
||||
print(f"Configuration file {options.config} created.")
|
||||
if options.validate:
|
||||
print("Validate your configuration...")
|
||||
try:
|
||||
if self._loaded():
|
||||
print("Your configuration seem valid.")
|
||||
else:
|
||||
print(
|
||||
"Error(s) occurred validating your configuration. See logs for details."
|
||||
)
|
||||
sys.exit(1)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
print(
|
||||
"Exception occurred validating your configuration:\n"
|
||||
f"{traceback.format_exc()}"
|
||||
"\n\nSee logs for details."
|
||||
)
|
||||
sys.exit(2)
|
||||
validate()
|
||||
else:
|
||||
print(f"Error occured creating configuration file {options.config}")
|
||||
print(f"Error occurred creating configuration file {options.config}")
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0)
|
||||
|
@ -1056,9 +1236,11 @@ class Config: # pylint: disable=too-many-instance-attributes
|
|||
return self._filepath
|
||||
if self.config_file_env_variable and os.environ.get(self.config_file_env_variable):
|
||||
return os.environ.get(self.config_file_env_variable)
|
||||
return os.path.join(
|
||||
self.config_dir, f"{self.shortname}.ini" if self.shortname else "config.ini"
|
||||
)
|
||||
if self.default_config_filename:
|
||||
filename = self.default_config_filename
|
||||
else:
|
||||
filename = f"{self.shortname}.ini" if self.shortname else "config.ini"
|
||||
return os.path.join(self.config_dir, filename)
|
||||
|
||||
|
||||
class ConfigurableObject:
|
||||
|
@ -1081,7 +1263,9 @@ class ConfigurableObject:
|
|||
|
||||
# Default options value
|
||||
# Important: all supported options MUST HAVE a default value defined
|
||||
_defaults = {}
|
||||
_defaults = {
|
||||
"just_try": None,
|
||||
}
|
||||
|
||||
# Store options passed throuht __init__ method
|
||||
_kwargs = {}
|
||||
|
@ -1104,7 +1288,7 @@ class ConfigurableObject:
|
|||
elif self._config_name:
|
||||
self._options_prefix = self._config_name + "_"
|
||||
else:
|
||||
raise Exception(f"No configuration name defined for {__name__}")
|
||||
raise ConfigException(f"No configuration name defined for {__name__}")
|
||||
|
||||
if config:
|
||||
self._config = config
|
||||
|
@ -1113,10 +1297,10 @@ class ConfigurableObject:
|
|||
elif self._config_name:
|
||||
self._config_section = self._config_name
|
||||
else:
|
||||
raise Exception(f"No configuration name defined for {__name__}")
|
||||
raise ConfigException(f"No configuration name defined for {__name__}")
|
||||
|
||||
def _get_option(self, option, default=None, required=False):
|
||||
"""Retreive option value"""
|
||||
"""Retrieve option value"""
|
||||
if self._kwargs and option in self._kwargs:
|
||||
return self._kwargs[option]
|
||||
|
||||
|
@ -1130,25 +1314,107 @@ class ConfigurableObject:
|
|||
|
||||
return default if default is not None else self._defaults.get(option)
|
||||
|
||||
def configure(self, comment=None, **kwargs):
|
||||
"""Configure options on registered mylib.Config object"""
|
||||
def _set_option(self, option, value):
|
||||
"""Set option value"""
|
||||
self._kwargs[option] = value
|
||||
|
||||
def set_default(self, option, default_value):
|
||||
"""Set option default value"""
|
||||
assert option in self._defaults, f"Unknown option {option}"
|
||||
self._defaults[option] = default_value
|
||||
|
||||
def set_defaults(self, **default_values):
|
||||
"""Set options default value"""
|
||||
for option, default_value in default_values.items():
|
||||
self.set_default(option, default_value)
|
||||
|
||||
def configure(
|
||||
self,
|
||||
comment=None,
|
||||
just_try=False,
|
||||
just_try_default=False,
|
||||
just_try_help="Just-try mode",
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Configure options on registered mylib.Config object
|
||||
:param comment: Configuration section comment (default: self._config_comment)
|
||||
:param just_try: Add just-try mode option (default: False)
|
||||
:param just_try_default: Default just-try mode option value (default: False)
|
||||
:param just_try_help: Default just-try mode option help message (default: "Just-try mode")
|
||||
:param kwargs: Other provided parameters are directly passed to Config.add_section() method
|
||||
"""
|
||||
assert self._config, (
|
||||
"mylib.Config object not registered. Must be passed to __init__ as config keyword"
|
||||
" argument."
|
||||
)
|
||||
|
||||
return self._config.add_section(
|
||||
section = self._config.add_section(
|
||||
self._config_section,
|
||||
comment=comment if comment else self._config_comment,
|
||||
loaded_callback=self.initialize,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if just_try:
|
||||
self._defaults["just_try"] = just_try_default
|
||||
section.add_option(
|
||||
BooleanOption,
|
||||
"just_try",
|
||||
default=self._defaults["just_try"],
|
||||
comment=just_try_help if just_try_help else "Just-try mode",
|
||||
)
|
||||
|
||||
return section
|
||||
|
||||
def initialize(self, loaded_config=None):
|
||||
"""Configuration initialized hook"""
|
||||
if loaded_config:
|
||||
self.config = loaded_config # pylint: disable=attribute-defined-outside-init
|
||||
|
||||
@property
|
||||
def _just_try(self):
|
||||
"""Check if just-try mode is enabled"""
|
||||
# If "just_try" provided to constructor, use it value
|
||||
if "just_try" in self._kwargs:
|
||||
log.debug(
|
||||
"Just-try mode is %s by value passed to constructor",
|
||||
"enabled" if self._kwargs["just_try"] else "disabled",
|
||||
)
|
||||
return self._kwargs["just_try"]
|
||||
|
||||
# If options provided and just-try option exist and is enabled, just-try mode enabled
|
||||
if (
|
||||
self._options
|
||||
and hasattr(self._options, f"{self._options_prefix}just_try")
|
||||
and getattr(self._options, f"{self._options_prefix}just_try")
|
||||
):
|
||||
log.debug("Just-try mode for %s is enabled", __class__.__name__)
|
||||
return True
|
||||
|
||||
# If options provided and a just_try option exist and is enabled, just-try mode enabled
|
||||
if (
|
||||
self._options
|
||||
and hasattr(self._options, "just_try")
|
||||
and getattr(self._options, "just_try")
|
||||
):
|
||||
log.debug("Just-try mode is globally enabled")
|
||||
return True
|
||||
|
||||
# If Config provided, config section defined and just-try enabled in config, just-try mode
|
||||
# enabled
|
||||
if (
|
||||
self._config
|
||||
and self._config.defined(self._config_section, "just_try")
|
||||
and self._config.get(self._config_section, "just_try")
|
||||
):
|
||||
log.debug("Just-try mode for %s is enabled in configuration", self._config_section)
|
||||
return True
|
||||
|
||||
# If Config provided, use it's get_option() method to obtain a global just_try parameter
|
||||
# value with a default to False, otherwise always false
|
||||
return self._config.get_option("just_try", default=False) if self._config else False
|
||||
|
||||
|
||||
class ConfigSectionAsDictWrapper:
|
||||
"""
|
||||
|
@ -1168,7 +1434,7 @@ class ConfigSectionAsDictWrapper:
|
|||
self.__section.set(key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise Exception("Deleting a configuration option is not supported")
|
||||
raise ConfigException("Deleting a configuration option is not supported")
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
|
42
mylib/db.py
42
mylib/db.py
|
@ -38,7 +38,7 @@ class DBFailToConnect(DBException, RuntimeError):
|
|||
"""
|
||||
|
||||
def __init__(self, uri):
|
||||
super().__init__("An error occured during database connection ({uri})", uri=uri)
|
||||
super().__init__("An error occurred during database connection ({uri})", uri=uri)
|
||||
|
||||
|
||||
class DBDuplicatedSQLParameter(DBException, KeyError):
|
||||
|
@ -77,6 +77,19 @@ class DBInvalidOrderByClause(DBException, TypeError):
|
|||
)
|
||||
|
||||
|
||||
class DBInvalidLimitClause(DBException, TypeError):
|
||||
"""
|
||||
Raised when trying to select on table with invalid
|
||||
LIMIT clause provided
|
||||
"""
|
||||
|
||||
def __init__(self, limit):
|
||||
super().__init__(
|
||||
"Invalid LIMIT clause: {limit}. Must be a non-zero positive integer.",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
class DB:
|
||||
"""Database client"""
|
||||
|
||||
|
@ -261,7 +274,7 @@ class DB:
|
|||
def insert(self, table, values, just_try=False):
|
||||
"""Run INSERT SQL query"""
|
||||
# pylint: disable=consider-using-f-string
|
||||
sql = "INSERT INTO {} ({}) VALUES ({})".format(
|
||||
sql = "INSERT INTO {} ({}) VALUES ({})".format( # nosec
|
||||
self._quote_table_name(table),
|
||||
", ".join([self._quote_field_name(field) for field in values.keys()]),
|
||||
", ".join([self.format_param(key) for key in values]),
|
||||
|
@ -280,7 +293,7 @@ class DB:
|
|||
def update(self, table, values, where_clauses, where_op=None, just_try=False):
|
||||
"""Run UPDATE SQL query"""
|
||||
# pylint: disable=consider-using-f-string
|
||||
sql = "UPDATE {} SET {}".format(
|
||||
sql = "UPDATE {} SET {}".format( # nosec
|
||||
self._quote_table_name(table),
|
||||
", ".join(
|
||||
[f"{self._quote_field_name(key)} = {self.format_param(key)}" for key in values]
|
||||
|
@ -306,7 +319,7 @@ class DB:
|
|||
|
||||
def delete(self, table, where_clauses, where_op="AND", just_try=False):
|
||||
"""Run DELETE SQL query"""
|
||||
sql = f"DELETE FROM {self._quote_table_name(table)}"
|
||||
sql = f"DELETE FROM {self._quote_table_name(table)}" # nosec
|
||||
params = {}
|
||||
|
||||
try:
|
||||
|
@ -327,7 +340,7 @@ class DB:
|
|||
|
||||
def truncate(self, table, just_try=False):
|
||||
"""Run TRUNCATE SQL query"""
|
||||
sql = f"TRUNCATE TABLE {self._quote_table_name(table)}"
|
||||
sql = f"TRUNCATE TABLE {self._quote_table_name(table)}" # nosec
|
||||
|
||||
if just_try:
|
||||
log.debug("Just-try mode: execute TRUNCATE query: %s", sql)
|
||||
|
@ -340,7 +353,14 @@ class DB:
|
|||
return True
|
||||
|
||||
def select(
|
||||
self, table, where_clauses=None, fields=None, where_op="AND", order_by=None, just_try=False
|
||||
self,
|
||||
table,
|
||||
where_clauses=None,
|
||||
fields=None,
|
||||
where_op="AND",
|
||||
order_by=None,
|
||||
limit=None,
|
||||
just_try=False,
|
||||
):
|
||||
"""Run SELECT SQL query"""
|
||||
sql = "SELECT "
|
||||
|
@ -374,6 +394,16 @@ class DB:
|
|||
else:
|
||||
raise DBInvalidOrderByClause(order_by)
|
||||
|
||||
if limit:
|
||||
if not isinstance(limit, int):
|
||||
try:
|
||||
limit = int(limit)
|
||||
except ValueError as err:
|
||||
raise DBInvalidLimitClause(limit) from err
|
||||
if limit <= 0:
|
||||
raise DBInvalidLimitClause(limit)
|
||||
sql += f" LIMIT {limit}"
|
||||
|
||||
if just_try:
|
||||
log.debug("Just-try mode: execute SELECT query : %s", sql)
|
||||
return just_try
|
||||
|
|
177
mylib/email.py
177
mylib/email.py
|
@ -46,20 +46,26 @@ class EmailClient(
|
|||
"encoding": "utf-8",
|
||||
"catch_all_addr": None,
|
||||
"just_try": False,
|
||||
"templates_path": None,
|
||||
}
|
||||
|
||||
templates = {}
|
||||
|
||||
def __init__(self, templates=None, **kwargs):
|
||||
def __init__(self, templates=None, initialize=False, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
assert templates is None or isinstance(templates, dict)
|
||||
self.templates = templates if templates else {}
|
||||
if initialize:
|
||||
self.initialize()
|
||||
|
||||
# pylint: disable=arguments-differ,arguments-renamed
|
||||
def configure(self, use_smtp=True, just_try=True, **kwargs):
|
||||
def configure(self, use_smtp=True, **kwargs):
|
||||
"""Configure options on registered mylib.Config object"""
|
||||
section = super().configure(**kwargs)
|
||||
section = super().configure(
|
||||
just_try_help=kwargs.pop("just_try_help", "Just-try mode: do not really send emails"),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if use_smtp:
|
||||
section.add_option(
|
||||
|
@ -129,19 +135,46 @@ class EmailClient(
|
|||
comment="Catch all sent emails to this specified email address",
|
||||
)
|
||||
|
||||
if just_try:
|
||||
section.add_option(
|
||||
BooleanOption,
|
||||
"just_try",
|
||||
default=self._defaults["just_try"],
|
||||
comment="Just-try mode: do not really send emails",
|
||||
)
|
||||
section.add_option(
|
||||
StringOption,
|
||||
"templates_path",
|
||||
default=self._defaults["templates_path"],
|
||||
comment="Path to templates directory",
|
||||
)
|
||||
|
||||
return section
|
||||
|
||||
def initialize(self, *args, **kwargs): # pylint: disable=arguments-differ
|
||||
"""Configuration initialized hook"""
|
||||
super().initialize(*args, **kwargs)
|
||||
self.load_templates_directory()
|
||||
|
||||
def load_templates_directory(self, templates_path=None):
|
||||
"""Load templates from specified directory"""
|
||||
if templates_path is None:
|
||||
templates_path = self._get_option("templates_path")
|
||||
if not templates_path:
|
||||
return
|
||||
log.debug("Load email templates from %s directory", templates_path)
|
||||
for filename in os.listdir(templates_path):
|
||||
filepath = os.path.join(templates_path, filename)
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
template_name, template_type = os.path.splitext(filename)
|
||||
if template_type not in [".html", ".txt", ".subject"]:
|
||||
continue
|
||||
template_type = "text" if template_type == ".txt" else template_type[1:]
|
||||
if template_name not in self.templates:
|
||||
self.templates[template_name] = {}
|
||||
log.debug("Load email template %s %s from %s", template_name, template_type, filepath)
|
||||
with open(filepath, encoding="utf8") as file_desc:
|
||||
self.templates[template_name][template_type] = MakoTemplate(
|
||||
file_desc.read()
|
||||
) # nosec
|
||||
|
||||
def forge_message(
|
||||
self,
|
||||
rcpt_to,
|
||||
recipients,
|
||||
subject=None,
|
||||
html_body=None,
|
||||
text_body=None, # pylint: disable=too-many-arguments,too-many-locals
|
||||
|
@ -151,13 +184,14 @@ class EmailClient(
|
|||
sender_email=None,
|
||||
encoding=None,
|
||||
template=None,
|
||||
cc=None,
|
||||
**template_vars,
|
||||
):
|
||||
"""
|
||||
Forge a message
|
||||
|
||||
:param rcpt_to: The recipient of the email. Could be a tuple(name, email) or
|
||||
just the email of the recipient.
|
||||
:param recipients: The recipient(s) of the email. List of tuple(name, email) or
|
||||
just the email of the recipients.
|
||||
:param subject: The subject of the email.
|
||||
:param html_body: The HTML body of the email
|
||||
:param text_body: The plain text body of the email
|
||||
|
@ -167,11 +201,29 @@ class EmailClient(
|
|||
:param sender_email: Custom sender email (default: as defined on initialization)
|
||||
:param encoding: Email content encoding (default: as defined on initialization)
|
||||
:param template: The name of a template to use to forge this email
|
||||
:param cc: Optional list of CC recipient addresses.
|
||||
List of tuple(name, email) or just the email of the recipients.
|
||||
|
||||
All other parameters will be consider as template variables.
|
||||
"""
|
||||
recipients = [recipients] if not isinstance(recipients, list) else recipients
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["To"] = email.utils.formataddr(rcpt_to) if isinstance(rcpt_to, tuple) else rcpt_to
|
||||
msg["To"] = ", ".join(
|
||||
[
|
||||
email.utils.formataddr(recipient) if isinstance(recipient, tuple) else recipient
|
||||
for recipient in recipients
|
||||
]
|
||||
)
|
||||
|
||||
if cc:
|
||||
cc = [cc] if not isinstance(cc, list) else cc
|
||||
msg["Cc"] = ", ".join(
|
||||
[
|
||||
email.utils.formataddr(recipient) if isinstance(recipient, tuple) else recipient
|
||||
for recipient in cc
|
||||
]
|
||||
)
|
||||
|
||||
msg["From"] = email.utils.formataddr(
|
||||
(
|
||||
sender_name or self._get_option("sender_name"),
|
||||
|
@ -179,19 +231,27 @@ class EmailClient(
|
|||
)
|
||||
)
|
||||
if subject:
|
||||
msg["Subject"] = subject.format(**template_vars)
|
||||
msg["Subject"] = (
|
||||
subject.render(**template_vars)
|
||||
if isinstance(subject, MakoTemplate)
|
||||
else subject.format(**template_vars)
|
||||
)
|
||||
msg["Date"] = email.utils.formatdate(None, True)
|
||||
encoding = encoding if encoding else self._get_option("encoding")
|
||||
if template:
|
||||
assert template in self.templates, f"Unknwon template {template}"
|
||||
assert template in self.templates, f"Unknown template {template}"
|
||||
# Handle subject from template
|
||||
if not subject:
|
||||
assert self.templates[template].get(
|
||||
"subject"
|
||||
), f"No subject defined in template {template}"
|
||||
msg["Subject"] = self.templates[template]["subject"].format(**template_vars)
|
||||
msg["Subject"] = (
|
||||
self.templates[template]["subject"].render(**template_vars)
|
||||
if isinstance(self.templates[template]["subject"], MakoTemplate)
|
||||
else self.templates[template]["subject"].format(**template_vars)
|
||||
)
|
||||
|
||||
# Put HTML part in last one to prefered it
|
||||
# Put HTML part in last one to preferred it
|
||||
parts = []
|
||||
if self.templates[template].get("text"):
|
||||
if isinstance(self.templates[template]["text"], MakoTemplate):
|
||||
|
@ -236,36 +296,63 @@ class EmailClient(
|
|||
msg.attach(part)
|
||||
return msg
|
||||
|
||||
def send(self, rcpt_to, msg=None, subject=None, just_try=False, **forge_args):
|
||||
def send(
|
||||
self, recipients, msg=None, subject=None, just_try=None, cc=None, bcc=None, **forge_args
|
||||
):
|
||||
"""
|
||||
Send an email
|
||||
|
||||
:param rcpt_to: The recipient of the email. Could be a tuple(name, email)
|
||||
or just the email of the recipient.
|
||||
:param recipients: The recipient(s) of the email. List of tuple(name, email) or
|
||||
just the email of the recipients.
|
||||
:param msg: The message of this email (as MIMEBase or derivated classes)
|
||||
:param subject: The subject of the email (only if the message is not provided
|
||||
using msg parameter)
|
||||
:param just_try: Enable just try mode (do not really send email, default: as defined on
|
||||
initialization)
|
||||
:param cc: Optional list of CC recipient addresses. List of tuple(name, email) or
|
||||
just the email of the recipients.
|
||||
:param bcc: Optional list of BCC recipient addresses. List of tuple(name, email) or
|
||||
just the email of the recipients.
|
||||
|
||||
All other parameters will be consider as parameters to forge the message
|
||||
(only if the message is not provided using msg parameter).
|
||||
"""
|
||||
msg = msg if msg else self.forge_message(rcpt_to, subject, **forge_args)
|
||||
recipients = [recipients] if not isinstance(recipients, list) else recipients
|
||||
msg = msg if msg else self.forge_message(recipients, subject, cc=cc, **forge_args)
|
||||
catch_addr = self._get_option("catch_all_addr")
|
||||
if catch_addr:
|
||||
log.debug(
|
||||
"Catch email originally send to %s (CC:%s, BCC:%s) to %s",
|
||||
", ".join(recipients),
|
||||
", ".join(cc) if isinstance(cc, list) else cc,
|
||||
", ".join(bcc) if isinstance(bcc, list) else bcc,
|
||||
catch_addr,
|
||||
)
|
||||
recipients = catch_addr if isinstance(catch_addr, list) else [catch_addr]
|
||||
else:
|
||||
if cc:
|
||||
recipients.extend(
|
||||
[
|
||||
recipient[1] if isinstance(recipient, tuple) else recipient
|
||||
for recipient in (cc if isinstance(cc, list) else [cc])
|
||||
]
|
||||
)
|
||||
if bcc:
|
||||
recipients.extend(
|
||||
[
|
||||
recipient[1] if isinstance(recipient, tuple) else recipient
|
||||
for recipient in (bcc if isinstance(bcc, list) else [bcc])
|
||||
]
|
||||
)
|
||||
|
||||
if just_try or self._get_option("just_try"):
|
||||
if just_try if just_try is not None else self._just_try:
|
||||
log.debug(
|
||||
'Just-try mode: do not really send this email to %s (subject="%s")',
|
||||
rcpt_to,
|
||||
", ".join(recipients),
|
||||
subject or msg.get("subject", "No subject"),
|
||||
)
|
||||
return True
|
||||
|
||||
catch_addr = self._get_option("catch_all_addr")
|
||||
if catch_addr:
|
||||
log.debug("Catch email originaly send to %s to %s", rcpt_to, catch_addr)
|
||||
rcpt_to = catch_addr
|
||||
|
||||
smtp_host = self._get_option("smtp_host")
|
||||
smtp_port = self._get_option("smtp_port")
|
||||
try:
|
||||
|
@ -303,15 +390,18 @@ class EmailClient(
|
|||
|
||||
error = False
|
||||
try:
|
||||
log.info("Sending email to %s", rcpt_to)
|
||||
log.info("Sending email to %s", ", ".join(recipients))
|
||||
server.sendmail(
|
||||
self._get_option("sender_email"),
|
||||
[rcpt_to[1] if isinstance(rcpt_to, tuple) else rcpt_to],
|
||||
[
|
||||
recipient[1] if isinstance(recipient, tuple) else recipient
|
||||
for recipient in recipients
|
||||
],
|
||||
msg.as_string(),
|
||||
)
|
||||
except smtplib.SMTPException:
|
||||
error = True
|
||||
log.error("Error sending email to %s", rcpt_to, exc_info=True)
|
||||
log.error("Error sending email to %s", ", ".join(recipients), exc_info=True)
|
||||
finally:
|
||||
server.quit()
|
||||
|
||||
|
@ -476,23 +566,24 @@ if __name__ == "__main__":
|
|||
catch_all_addr=options.email_catch_all,
|
||||
just_try=options.just_try,
|
||||
encoding=options.email_encoding,
|
||||
templates=dict(
|
||||
test=dict(
|
||||
subject="Test email",
|
||||
text=(
|
||||
templates={
|
||||
"test": {
|
||||
"subject": "Test email",
|
||||
"text": (
|
||||
"Just a test email sent at {sent_date}."
|
||||
if not options.test_mako
|
||||
else MakoTemplate("Just a test email sent at ${sent_date}.")
|
||||
else MakoTemplate("Just a test email sent at ${sent_date | h}.") # nosec
|
||||
),
|
||||
html=(
|
||||
"<strong>Just a test email.</strong> <small>(sent at {sent_date})</small>"
|
||||
"html": (
|
||||
"<strong>Just a test email.</strong> <small>(sent at {sent_date | h})</small>"
|
||||
if not options.test_mako
|
||||
else MakoTemplate(
|
||||
"<strong>Just a test email.</strong> <small>(sent at ${sent_date})</small>"
|
||||
else MakoTemplate( # nosec
|
||||
"<strong>Just a test email.</strong> "
|
||||
"<small>(sent at ${sent_date | h})</small>"
|
||||
)
|
||||
),
|
||||
)
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
logging.info("Send a test email to %s", options.test_to)
|
||||
|
|
|
@ -120,7 +120,7 @@ class LdapServer:
|
|||
return ldap.SCOPE_ONELEVEL # pylint: disable=no-member
|
||||
if scope == "sub":
|
||||
return ldap.SCOPE_SUBTREE # pylint: disable=no-member
|
||||
raise Exception(f'Unknown LDAP scope "{scope}"')
|
||||
raise LdapServerException(f'Unknown LDAP scope "{scope}"')
|
||||
|
||||
def search(self, basedn, filterstr=None, attrs=None, sizelimit=None, scope=None):
|
||||
"""Run a search on LDAP server"""
|
||||
|
@ -211,7 +211,7 @@ class LdapServer:
|
|||
result_page_control = rctrl
|
||||
break
|
||||
|
||||
# If PagedResultsControl answer not detected, paged serach
|
||||
# If PagedResultsControl answer not detected, paged search
|
||||
if not result_page_control:
|
||||
self._error(
|
||||
"LdapServer - Server ignores RFC2696 control, paged search can not works",
|
||||
|
@ -238,7 +238,7 @@ class LdapServer:
|
|||
page_control.cookie = result_page_control.cookie
|
||||
|
||||
self.logger.debug(
|
||||
"LdapServer - Paged search end: %d object(s) retreived in %d page(s) of %d object(s)",
|
||||
"LdapServer - Paged search end: %d object(s) retrieved in %d page(s) of %d object(s)",
|
||||
len(ret),
|
||||
pages_count,
|
||||
pagesize,
|
||||
|
@ -281,14 +281,10 @@ class LdapServer:
|
|||
)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def update_need(old, new, ignore_attrs=None, encode=False):
|
||||
@classmethod
|
||||
def update_need(cls, old, new, ignore_attrs=None, encode=False):
|
||||
"""Check if an update is need on a LDAP object based on its old and new attributes values"""
|
||||
ldif = modlist.modifyModlist(
|
||||
encode_ldap_value(old) if encode else old,
|
||||
encode_ldap_value(new) if encode else new,
|
||||
ignore_attr_types=ignore_attrs if ignore_attrs else [],
|
||||
)
|
||||
ldif = cls.get_changes(old, new, ignore_attrs=ignore_attrs, encode=encode)
|
||||
if not ldif:
|
||||
return False
|
||||
return True
|
||||
|
@ -302,19 +298,23 @@ class LdapServer:
|
|||
ignore_attr_types=ignore_attrs if ignore_attrs else [],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def format_changes(old, new, ignore_attrs=None, prefix=None, encode=False):
|
||||
@classmethod
|
||||
def format_changes(cls, old, new, ignore_attrs=None, prefix=None, encode=False):
|
||||
"""
|
||||
Format changes (modlist) on an object based on its old and new attributes values to
|
||||
display/log it
|
||||
"""
|
||||
return cls.format_modify_modlist(
|
||||
cls.get_changes(old, new, ignore_attrs=ignore_attrs, encode=encode),
|
||||
prefix=prefix,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def format_modify_modlist(ldif, prefix=None):
|
||||
"""Format modify modlist to display/log it"""
|
||||
msg = []
|
||||
prefix = prefix if prefix else ""
|
||||
for op, attr, val in modlist.modifyModlist(
|
||||
encode_ldap_value(old) if encode else old,
|
||||
encode_ldap_value(new) if encode else new,
|
||||
ignore_attr_types=ignore_attrs if ignore_attrs else [],
|
||||
):
|
||||
for op, attr, val in ldif:
|
||||
if op == ldap.MOD_ADD: # pylint: disable=no-member
|
||||
op = "ADD"
|
||||
elif op == ldap.MOD_DELETE: # pylint: disable=no-member
|
||||
|
@ -379,12 +379,12 @@ class LdapServer:
|
|||
|
||||
@staticmethod
|
||||
def get_dn(obj):
|
||||
"""Retreive an on object DN from its entry in LDAP search result"""
|
||||
"""Retrieve an on object DN from its entry in LDAP search result"""
|
||||
return obj[0][0]
|
||||
|
||||
@staticmethod
|
||||
def get_attr(obj, attr, all_values=None, default=None, decode=False):
|
||||
"""Retreive an on object attribute value(s) from the object entry in LDAP search result"""
|
||||
"""Retrieve an on object attribute value(s) from the object entry in LDAP search result"""
|
||||
if attr not in obj:
|
||||
for k in obj:
|
||||
if k.lower() == attr.lower():
|
||||
|
@ -399,18 +399,16 @@ class LdapServer:
|
|||
return default
|
||||
|
||||
|
||||
class LdapServerException(BaseException):
|
||||
class LdapException(BaseException):
|
||||
"""Generic LDAP exception"""
|
||||
|
||||
|
||||
class LdapServerException(LdapException):
|
||||
"""Generic exception raised by LdapServer"""
|
||||
|
||||
def __init__(self, msg):
|
||||
BaseException.__init__(self, msg)
|
||||
|
||||
|
||||
class LdapClientException(LdapServerException):
|
||||
"""Generic exception raised by LdapServer"""
|
||||
|
||||
def __init__(self, msg):
|
||||
LdapServerException.__init__(self, msg)
|
||||
class LdapClientException(LdapException):
|
||||
"""Generic exception raised by LdapClient"""
|
||||
|
||||
|
||||
class LdapClient:
|
||||
|
@ -439,7 +437,7 @@ class LdapClient:
|
|||
self.initialize()
|
||||
|
||||
def _get_option(self, option, default=None, required=False):
|
||||
"""Retreive option value"""
|
||||
"""Retrieve option value"""
|
||||
if self._options and hasattr(self._options, self._options_prefix + option):
|
||||
return getattr(self._options, self._options_prefix + option)
|
||||
|
||||
|
@ -502,7 +500,7 @@ class LdapClient:
|
|||
self.config = loaded_config
|
||||
uri = self._get_option("uri", required=True)
|
||||
binddn = self._get_option("binddn")
|
||||
log.info("Connect to LDAP server %s as %s", uri, binddn if binddn else "annonymous")
|
||||
log.info("Connect to LDAP server %s as %s", uri, binddn if binddn else "anonymous")
|
||||
self._conn = LdapServer(
|
||||
uri,
|
||||
dn=binddn,
|
||||
|
@ -541,7 +539,7 @@ class LdapClient:
|
|||
:param dn: The object DN
|
||||
:param attrs: The object attributes as return by python-ldap search
|
||||
"""
|
||||
obj = dict(dn=dn)
|
||||
obj = {"dn": dn}
|
||||
for attr in attrs:
|
||||
obj[attr] = [self.decode(v) for v in self._conn.get_attr(attrs, attr, all_values=True)]
|
||||
return obj
|
||||
|
@ -555,7 +553,7 @@ class LdapClient:
|
|||
:param attr: The attribute name
|
||||
:param all_values: If True, all values of the attribute will be
|
||||
returned instead of the first value only
|
||||
(optinal, default: False)
|
||||
(optional, default: False)
|
||||
"""
|
||||
if attr not in obj:
|
||||
for k in obj:
|
||||
|
@ -584,7 +582,7 @@ class LdapClient:
|
|||
:param name: The object type name
|
||||
:param filterstr: The LDAP filter to use to search objects on LDAP directory
|
||||
:param basedn: The base DN of the search
|
||||
:param attrs: The list of attribute names to retreive
|
||||
:param attrs: The list of attribute names to retrieve
|
||||
:param key_attr: The attribute name or 'dn' to use as key in result
|
||||
(optional, if leave to None, the result will be a list)
|
||||
:param warn: If True, a warning message will be logged if no object is found
|
||||
|
@ -596,7 +594,7 @@ class LdapClient:
|
|||
(optional, default: see LdapServer.paged_search)
|
||||
"""
|
||||
if name in self._cached_objects:
|
||||
log.debug("Retreived %s objects from cache", name)
|
||||
log.debug("Retrieved %s objects from cache", name)
|
||||
else:
|
||||
assert self._conn or self.initialize()
|
||||
log.debug(
|
||||
|
@ -645,7 +643,7 @@ class LdapClient:
|
|||
:param object_name: The object name (only use in log messages)
|
||||
:param filterstr: The LDAP filter to use to search the object on LDAP directory
|
||||
:param basedn: The base DN of the search
|
||||
:param attrs: The list of attribute names to retreive
|
||||
:param attrs: The list of attribute names to retrieve
|
||||
:param warn: If True, a warning message will be logged if no object is found
|
||||
in LDAP directory (otherwise, it will be just a debug message)
|
||||
(optional, default: True)
|
||||
|
@ -782,21 +780,23 @@ class LdapClient:
|
|||
protected_attrs = [a.lower() for a in protected_attrs or []]
|
||||
protected_attrs.append("dn")
|
||||
# New/updated attributes
|
||||
for attr in attrs:
|
||||
for attr, values in attrs.items():
|
||||
if protected_attrs and attr.lower() in protected_attrs:
|
||||
continue
|
||||
if attr in ldap_obj and ldap_obj[attr]:
|
||||
if sorted(ldap_obj[attr]) == sorted(attrs[attr]):
|
||||
if sorted(ldap_obj[attr]) == sorted(values):
|
||||
continue
|
||||
old[attr] = self.encode(ldap_obj[attr])
|
||||
new[attr] = self.encode(attrs[attr])
|
||||
elif not values:
|
||||
continue
|
||||
new[attr] = self.encode(values)
|
||||
|
||||
# Deleted attributes
|
||||
for attr in ldap_obj:
|
||||
if (
|
||||
(not protected_attrs or attr.lower() not in protected_attrs)
|
||||
and ldap_obj[attr]
|
||||
and attr not in attrs
|
||||
and not attrs.get(attr)
|
||||
):
|
||||
old[attr] = self.encode(ldap_obj[attr])
|
||||
if old == new:
|
||||
|
@ -850,14 +850,15 @@ class LdapClient:
|
|||
)
|
||||
return False
|
||||
|
||||
def update_object(self, ldap_obj, changes, protected_attrs=None, rdn_attr=None):
|
||||
def update_object(self, ldap_obj, changes, protected_attrs=None, rdn_attr=None, relax=False):
|
||||
"""
|
||||
Update an object
|
||||
|
||||
:param ldap_obj: The original LDAP object
|
||||
:param changes: The changes to make on LDAP object (as formated by get_changes() method)
|
||||
:param changes: The changes to make on LDAP object (as formatted by get_changes() method)
|
||||
:param protected_attrs: An optional list of protected attributes
|
||||
:param rdn_attr: The LDAP object RDN attribute (to detect renaming, default: auto-detected)
|
||||
:param rdn_attr: Enable relax modification server control (optional, default: false)
|
||||
"""
|
||||
assert (
|
||||
isinstance(changes, (list, tuple))
|
||||
|
@ -914,7 +915,7 @@ class LdapClient:
|
|||
# Otherwise, update object DN
|
||||
ldap_obj["dn"] = new_dn
|
||||
else:
|
||||
log.debug("%s: No change detected on RDN attibute %s", ldap_obj["dn"], rdn_attr)
|
||||
log.debug("%s: No change detected on RDN attribute %s", ldap_obj["dn"], rdn_attr)
|
||||
|
||||
try:
|
||||
if self._just_try:
|
||||
|
@ -922,7 +923,7 @@ class LdapClient:
|
|||
return True
|
||||
assert self._conn or self.initialize()
|
||||
return self._conn.update_object(
|
||||
ldap_obj["dn"], _changes[0], _changes[1], ignore_attrs=protected_attrs
|
||||
ldap_obj["dn"], _changes[0], _changes[1], ignore_attrs=protected_attrs, relax=relax
|
||||
)
|
||||
except LdapServerException:
|
||||
log.error(
|
||||
|
@ -1015,7 +1016,7 @@ def parse_datetime(value, to_timezone=None, default_timezone=None, naive=None):
|
|||
elif isinstance(default_timezone, datetime.tzinfo):
|
||||
date = date.replace(tzinfo=default_timezone)
|
||||
else:
|
||||
raise Exception("It's not supposed to happen!")
|
||||
raise LdapException("It's not supposed to happen!")
|
||||
elif naive:
|
||||
return date.replace(tzinfo=None)
|
||||
if to_timezone:
|
||||
|
@ -1075,7 +1076,7 @@ def format_datetime(value, from_timezone=None, to_timezone=None, naive=None):
|
|||
elif isinstance(from_timezone, datetime.tzinfo):
|
||||
from_value = value.replace(tzinfo=from_timezone)
|
||||
else:
|
||||
raise Exception("It's not supposed to happen!")
|
||||
raise LdapException("It's not supposed to happen!")
|
||||
elif naive:
|
||||
from_value = value.replace(tzinfo=pytz.utc)
|
||||
else:
|
||||
|
@ -1102,7 +1103,7 @@ def format_date(value, from_timezone=None, to_timezone=None, naive=True):
|
|||
(optional, default : server local timezone)
|
||||
:param to_timezone: The timezone used in LDAP (optional, default : UTC)
|
||||
:param naive: Use naive datetime : do not handle timezone conversion before
|
||||
formating and return datetime as UTC (because LDAP required a
|
||||
formatting and return datetime as UTC (because LDAP required a
|
||||
timezone)
|
||||
"""
|
||||
assert isinstance(
|
||||
|
|
|
@ -29,7 +29,7 @@ Mapping configuration
|
|||
'join': '[glue]', # If present, sources values will be join using the "glue"
|
||||
|
||||
# Alternative mapping
|
||||
'or': { [map configuration] } # If this mapping case does not retreive any value, try to
|
||||
'or': { [map configuration] } # If this mapping case does not retrieve any value, try to
|
||||
# get value(s) with this other mapping configuration
|
||||
},
|
||||
'[dst key 2]': {
|
||||
|
|
|
@ -41,7 +41,7 @@ class MyDB(DB):
|
|||
)
|
||||
except Error as err:
|
||||
log.fatal(
|
||||
"An error occured during MySQL database connection (%s@%s:%s).",
|
||||
"An error occurred during MySQL database connection (%s@%s:%s).",
|
||||
self._user,
|
||||
self._host,
|
||||
self._db,
|
||||
|
|
|
@ -11,6 +11,7 @@ week_days = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "diman
|
|||
date_format = "%d/%m/%Y"
|
||||
date_pattern = re.compile("^([0-9]{2})/([0-9]{2})/([0-9]{4})$")
|
||||
time_pattern = re.compile("^([0-9]{1,2})h([0-9]{2})?$")
|
||||
_nonworking_french_public_days_of_the_year_cache = {}
|
||||
|
||||
|
||||
def easter_date(year):
|
||||
|
@ -37,23 +38,25 @@ def nonworking_french_public_days_of_the_year(year=None):
|
|||
"""Compute dict of nonworking french public days for the specified year"""
|
||||
if year is None:
|
||||
year = datetime.date.today().year
|
||||
dp = easter_date(year)
|
||||
return {
|
||||
"1janvier": datetime.date(year, 1, 1),
|
||||
"paques": dp,
|
||||
"lundi_paques": (dp + datetime.timedelta(1)),
|
||||
"1mai": datetime.date(year, 5, 1),
|
||||
"8mai": datetime.date(year, 5, 8),
|
||||
"jeudi_ascension": (dp + datetime.timedelta(39)),
|
||||
"pentecote": (dp + datetime.timedelta(49)),
|
||||
"lundi_pentecote": (dp + datetime.timedelta(50)),
|
||||
"14juillet": datetime.date(year, 7, 14),
|
||||
"15aout": datetime.date(year, 8, 15),
|
||||
"1novembre": datetime.date(year, 11, 1),
|
||||
"11novembre": datetime.date(year, 11, 11),
|
||||
"noel": datetime.date(year, 12, 25),
|
||||
"saint_etienne": datetime.date(year, 12, 26),
|
||||
}
|
||||
if year not in _nonworking_french_public_days_of_the_year_cache:
|
||||
dp = easter_date(year)
|
||||
_nonworking_french_public_days_of_the_year_cache[year] = {
|
||||
"1janvier": datetime.date(year, 1, 1),
|
||||
"paques": dp,
|
||||
"lundi_paques": (dp + datetime.timedelta(1)),
|
||||
"1mai": datetime.date(year, 5, 1),
|
||||
"8mai": datetime.date(year, 5, 8),
|
||||
"jeudi_ascension": (dp + datetime.timedelta(39)),
|
||||
"pentecote": (dp + datetime.timedelta(49)),
|
||||
"lundi_pentecote": (dp + datetime.timedelta(50)),
|
||||
"14juillet": datetime.date(year, 7, 14),
|
||||
"15aout": datetime.date(year, 8, 15),
|
||||
"1novembre": datetime.date(year, 11, 1),
|
||||
"11novembre": datetime.date(year, 11, 11),
|
||||
"noel": datetime.date(year, 12, 25),
|
||||
"saint_etienne": datetime.date(year, 12, 26),
|
||||
}
|
||||
return _nonworking_french_public_days_of_the_year_cache[year]
|
||||
|
||||
|
||||
def parse_exceptional_closures(values):
|
||||
|
@ -155,7 +158,153 @@ def parse_normal_opening_hours(values):
|
|||
if not days and not hours_periods:
|
||||
raise ValueError(f'No days or hours period found in this value: "{value}"')
|
||||
normal_opening_hours.append({"days": days, "hours_periods": hours_periods})
|
||||
return normal_opening_hours
|
||||
for idx, noh in enumerate(normal_opening_hours):
|
||||
normal_opening_hours[idx]["hours_periods"] = sorted_hours_periods(noh["hours_periods"])
|
||||
return sorted_opening_hours(normal_opening_hours)
|
||||
|
||||
|
||||
def sorted_hours_periods(hours_periods):
|
||||
"""Sort hours periods"""
|
||||
return sorted(hours_periods, key=lambda hp: (hp["start"], hp["stop"]))
|
||||
|
||||
|
||||
def sorted_opening_hours(opening_hours):
|
||||
"""Sort opening hours"""
|
||||
return sorted(
|
||||
opening_hours,
|
||||
key=lambda x: (
|
||||
week_days.index(x["days"][0]) if x["days"] else None,
|
||||
x["hours_periods"][0]["start"] if x["hours_periods"] else datetime.datetime.min.time(),
|
||||
x["hours_periods"][0]["stop"] if x["hours_periods"] else datetime.datetime.max.time(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def its_nonworking_day(nonworking_public_holidays_values, date=None):
|
||||
"""Check if is a non-working day"""
|
||||
if not nonworking_public_holidays_values:
|
||||
return False
|
||||
date = date if date else datetime.date.today()
|
||||
log.debug("its_nonworking_day(%s): values=%s", date, nonworking_public_holidays_values)
|
||||
nonworking_days = nonworking_french_public_days_of_the_year(year=date.year)
|
||||
for day in nonworking_public_holidays_values:
|
||||
if day in nonworking_days and nonworking_days[day] == date:
|
||||
log.debug("its_nonworking_day(%s): %s", date, day)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def its_exceptionally_closed(exceptional_closures_values, when=None, parse=True, all_day=False):
|
||||
"""Check if it's exceptionally closed"""
|
||||
if not exceptional_closures_values:
|
||||
return False
|
||||
when = when if when else datetime.datetime.now()
|
||||
assert isinstance(when, (datetime.date, datetime.datetime))
|
||||
when_date = when.date() if isinstance(when, datetime.datetime) else when
|
||||
exceptional_closures = (
|
||||
parse_exceptional_closures(exceptional_closures_values)
|
||||
if parse
|
||||
else exceptional_closures_values
|
||||
)
|
||||
log.debug("its_exceptionally_closed(%s): exceptional closures=%s", when, exceptional_closures)
|
||||
for cl in exceptional_closures:
|
||||
if when_date not in cl["days"]:
|
||||
log.debug(
|
||||
"its_exceptionally_closed(%s): %s not in days (%s)", when, when_date, cl["days"]
|
||||
)
|
||||
continue
|
||||
if not cl["hours_periods"]:
|
||||
# All day exceptional closure
|
||||
return True
|
||||
if all_day:
|
||||
# Wanted an all day closure, ignore it
|
||||
continue
|
||||
for hp in cl["hours_periods"]:
|
||||
if hp["start"] <= when.time() <= hp["stop"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_exceptional_closures_hours(exceptional_closures_values, date=None, parse=True):
|
||||
"""Get exceptional closures hours of the day"""
|
||||
if not exceptional_closures_values:
|
||||
return []
|
||||
date = date if date else datetime.date.today()
|
||||
exceptional_closures = (
|
||||
parse_exceptional_closures(exceptional_closures_values)
|
||||
if parse
|
||||
else exceptional_closures_values
|
||||
)
|
||||
log.debug(
|
||||
"get_exceptional_closures_hours(%s): exceptional closures=%s", date, exceptional_closures
|
||||
)
|
||||
exceptional_closures_hours = []
|
||||
for cl in exceptional_closures:
|
||||
if date not in cl["days"]:
|
||||
log.debug("get_exceptional_closures_hours(%s): not in days (%s)", date, cl["days"])
|
||||
continue
|
||||
if not cl["hours_periods"]:
|
||||
log.debug(
|
||||
"get_exceptional_closures_hours(%s): it's exceptionally closed all the day", date
|
||||
)
|
||||
return [
|
||||
{
|
||||
"start": datetime.datetime.min.time(),
|
||||
"stop": datetime.datetime.max.time(),
|
||||
}
|
||||
]
|
||||
exceptional_closures_hours.extend(cl["hours_periods"])
|
||||
log.debug(
|
||||
"get_exceptional_closures_hours(%s): exceptional closures hours=%s",
|
||||
date,
|
||||
exceptional_closures_hours,
|
||||
)
|
||||
return sorted_hours_periods(exceptional_closures_hours)
|
||||
|
||||
|
||||
def its_normally_open(normal_opening_hours_values, when=None, parse=True, ignore_time=False):
|
||||
"""Check if it's normally open"""
|
||||
when = when if when else datetime.datetime.now()
|
||||
if not normal_opening_hours_values:
|
||||
log.debug(
|
||||
"its_normally_open(%s): no normal opening hours defined, consider as opened", when
|
||||
)
|
||||
return True
|
||||
when_weekday = week_days[when.timetuple().tm_wday]
|
||||
log.debug("its_normally_open(%s): week day=%s", when, when_weekday)
|
||||
normal_opening_hours = (
|
||||
parse_normal_opening_hours(normal_opening_hours_values)
|
||||
if parse
|
||||
else normal_opening_hours_values
|
||||
)
|
||||
log.debug("its_normally_open(%s): normal opening hours=%s", when, normal_opening_hours)
|
||||
for oh in normal_opening_hours:
|
||||
if oh["days"] and when_weekday not in oh["days"]:
|
||||
log.debug("its_normally_open(%s): %s not in days (%s)", when, when_weekday, oh["days"])
|
||||
continue
|
||||
if not oh["hours_periods"] or ignore_time:
|
||||
return True
|
||||
for hp in oh["hours_periods"]:
|
||||
if hp["start"] <= when.time() <= hp["stop"]:
|
||||
return True
|
||||
log.debug("its_normally_open(%s): not in normal opening hours", when)
|
||||
return False
|
||||
|
||||
|
||||
def its_opening_day(
|
||||
normal_opening_hours_values=None,
|
||||
exceptional_closures_values=None,
|
||||
nonworking_public_holidays_values=None,
|
||||
date=None,
|
||||
parse=True,
|
||||
):
|
||||
"""Check if it's an opening day"""
|
||||
date = date if date else datetime.date.today()
|
||||
if its_nonworking_day(nonworking_public_holidays_values, date=date):
|
||||
return False
|
||||
if its_exceptionally_closed(exceptional_closures_values, when=date, all_day=True, parse=parse):
|
||||
return False
|
||||
return its_normally_open(normal_opening_hours_values, when=date, parse=parse, ignore_time=True)
|
||||
|
||||
|
||||
def is_closed(
|
||||
|
@ -193,76 +342,578 @@ def is_closed(
|
|||
when_time,
|
||||
when_weekday,
|
||||
)
|
||||
if nonworking_public_holidays_values:
|
||||
log.debug("Nonworking public holidays: %s", nonworking_public_holidays_values)
|
||||
nonworking_days = nonworking_french_public_days_of_the_year()
|
||||
for day in nonworking_public_holidays_values:
|
||||
if day in nonworking_days and when_date == nonworking_days[day]:
|
||||
log.debug("Non working day: %s", day)
|
||||
return {
|
||||
"closed": True,
|
||||
"exceptional_closure": exceptional_closure_on_nonworking_public_days,
|
||||
"exceptional_closure_all_day": exceptional_closure_on_nonworking_public_days,
|
||||
}
|
||||
# Handle non-working days
|
||||
if its_nonworking_day(nonworking_public_holidays_values, date=when_date):
|
||||
return {
|
||||
"closed": True,
|
||||
"exceptional_closure": exceptional_closure_on_nonworking_public_days,
|
||||
"exceptional_closure_all_day": exceptional_closure_on_nonworking_public_days,
|
||||
}
|
||||
|
||||
if exceptional_closures_values:
|
||||
# Handle exceptional closures
|
||||
try:
|
||||
if its_exceptionally_closed(exceptional_closures_values, when=when):
|
||||
return {
|
||||
"closed": True,
|
||||
"exceptional_closure": True,
|
||||
"exceptional_closure_all_day": its_exceptionally_closed(
|
||||
exceptional_closures_values, when=when, all_day=True
|
||||
),
|
||||
}
|
||||
except ValueError as e:
|
||||
if on_error_result is None:
|
||||
log.error("Fail to parse exceptional closures", exc_info=True)
|
||||
raise e from e
|
||||
log.error("Fail to parse exceptional closures, consider as %s", on_error, exc_info=True)
|
||||
return on_error_result
|
||||
|
||||
# Finally, handle normal opening hours
|
||||
try:
|
||||
return {
|
||||
"closed": not its_normally_open(normal_opening_hours_values, when=when),
|
||||
"exceptional_closure": False,
|
||||
"exceptional_closure_all_day": False,
|
||||
}
|
||||
except ValueError as e: # pylint: disable=broad-except
|
||||
if on_error_result is None:
|
||||
log.error("Fail to parse normal opening hours", exc_info=True)
|
||||
raise e from e
|
||||
log.error("Fail to parse normal opening hours, consider as %s", on_error, exc_info=True)
|
||||
return on_error_result
|
||||
|
||||
|
||||
def next_opening_date(
|
||||
normal_opening_hours_values=None,
|
||||
exceptional_closures_values=None,
|
||||
nonworking_public_holidays_values=None,
|
||||
date=None,
|
||||
max_anaylse_days=None,
|
||||
parse=True,
|
||||
):
|
||||
"""Search for the next opening day"""
|
||||
date = date if date else datetime.date.today()
|
||||
max_anaylse_days = max_anaylse_days if max_anaylse_days is not None else 30
|
||||
if parse:
|
||||
try:
|
||||
exceptional_closures = parse_exceptional_closures(exceptional_closures_values)
|
||||
log.debug("Exceptional closures: %s", exceptional_closures)
|
||||
except ValueError as e:
|
||||
log.error("Fail to parse exceptional closures, consider as closed", exc_info=True)
|
||||
if on_error_result is None:
|
||||
raise e from e
|
||||
return on_error_result
|
||||
for cl in exceptional_closures:
|
||||
if when_date not in cl["days"]:
|
||||
log.debug("when_date (%s) no in days (%s)", when_date, cl["days"])
|
||||
continue
|
||||
if not cl["hours_periods"]:
|
||||
# All day exceptional closure
|
||||
return {
|
||||
"closed": True,
|
||||
"exceptional_closure": True,
|
||||
"exceptional_closure_all_day": True,
|
||||
}
|
||||
for hp in cl["hours_periods"]:
|
||||
if hp["start"] <= when_time <= hp["stop"]:
|
||||
return {
|
||||
"closed": True,
|
||||
"exceptional_closure": True,
|
||||
"exceptional_closure_all_day": False,
|
||||
}
|
||||
normal_opening_hours_values = (
|
||||
parse_normal_opening_hours(normal_opening_hours_values)
|
||||
if normal_opening_hours_values
|
||||
else None
|
||||
)
|
||||
exceptional_closures_values = (
|
||||
parse_exceptional_closures(exceptional_closures_values)
|
||||
if exceptional_closures_values
|
||||
else None
|
||||
)
|
||||
except ValueError: # pylint: disable=broad-except
|
||||
log.error(
|
||||
"next_opening_date(%s): fail to parse normal opening hours or exceptional closures",
|
||||
date,
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
added_days = 0
|
||||
while added_days <= max_anaylse_days:
|
||||
test_date = date + datetime.timedelta(days=added_days)
|
||||
if its_opening_day(
|
||||
normal_opening_hours_values=normal_opening_hours_values,
|
||||
exceptional_closures_values=exceptional_closures_values,
|
||||
nonworking_public_holidays_values=nonworking_public_holidays_values,
|
||||
date=test_date,
|
||||
parse=False,
|
||||
):
|
||||
return test_date
|
||||
added_days += 1
|
||||
log.debug(
|
||||
"next_opening_date(%s): no opening day found in the next %d days", date, max_anaylse_days
|
||||
)
|
||||
return False
|
||||
|
||||
if normal_opening_hours_values:
|
||||
|
||||
def next_opening_hour(
|
||||
normal_opening_hours_values=None,
|
||||
exceptional_closures_values=None,
|
||||
nonworking_public_holidays_values=None,
|
||||
when=None,
|
||||
max_anaylse_days=None,
|
||||
parse=True,
|
||||
):
|
||||
"""Search for the next opening hour"""
|
||||
when = when if when else datetime.datetime.now()
|
||||
max_anaylse_days = max_anaylse_days if max_anaylse_days is not None else 30
|
||||
if parse:
|
||||
try:
|
||||
normal_opening_hours = parse_normal_opening_hours(normal_opening_hours_values)
|
||||
log.debug("Normal opening hours: %s", normal_opening_hours)
|
||||
except ValueError as e: # pylint: disable=broad-except
|
||||
log.error("Fail to parse normal opening hours, consider as closed", exc_info=True)
|
||||
if on_error_result is None:
|
||||
raise e from e
|
||||
return on_error_result
|
||||
for oh in normal_opening_hours:
|
||||
if oh["days"] and when_weekday not in oh["days"]:
|
||||
log.debug("when_weekday (%s) no in days (%s)", when_weekday, oh["days"])
|
||||
continue
|
||||
if not oh["hours_periods"]:
|
||||
# All day opened
|
||||
return {
|
||||
"closed": False,
|
||||
"exceptional_closure": False,
|
||||
"exceptional_closure_all_day": False,
|
||||
}
|
||||
for hp in oh["hours_periods"]:
|
||||
if hp["start"] <= when_time <= hp["stop"]:
|
||||
return {
|
||||
"closed": False,
|
||||
"exceptional_closure": False,
|
||||
"exceptional_closure_all_day": False,
|
||||
}
|
||||
log.debug("Not in normal opening hours => closed")
|
||||
return {"closed": True, "exceptional_closure": False, "exceptional_closure_all_day": False}
|
||||
normal_opening_hours_values = (
|
||||
parse_normal_opening_hours(normal_opening_hours_values)
|
||||
if normal_opening_hours_values
|
||||
else None
|
||||
)
|
||||
exceptional_closures_values = (
|
||||
parse_exceptional_closures(exceptional_closures_values)
|
||||
if exceptional_closures_values
|
||||
else None
|
||||
)
|
||||
except ValueError: # pylint: disable=broad-except
|
||||
log.error(
|
||||
"next_opening_hour(%s): fail to parse normal opening hours or exceptional closures",
|
||||
when,
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
date = next_opening_date(
|
||||
normal_opening_hours_values=normal_opening_hours_values,
|
||||
exceptional_closures_values=exceptional_closures_values,
|
||||
nonworking_public_holidays_values=nonworking_public_holidays_values,
|
||||
date=when.date(),
|
||||
max_anaylse_days=max_anaylse_days,
|
||||
parse=False,
|
||||
)
|
||||
if not date:
|
||||
log.debug(
|
||||
"next_opening_hour(%s): no opening day found in the next %d days",
|
||||
when,
|
||||
max_anaylse_days,
|
||||
)
|
||||
return False
|
||||
log.debug("next_opening_hour(%s): next opening date=%s", when, date)
|
||||
weekday = week_days[date.timetuple().tm_wday]
|
||||
log.debug("next_opening_hour(%s): next opening week day=%s", when, weekday)
|
||||
exceptional_closures_hours = get_exceptional_closures_hours(
|
||||
exceptional_closures_values, date=date, parse=False
|
||||
)
|
||||
log.debug(
|
||||
"next_opening_hour(%s): next opening day exceptional closures hours=%s",
|
||||
when,
|
||||
exceptional_closures_hours,
|
||||
)
|
||||
next_opening_datetime = None
|
||||
exceptionally_closed = False
|
||||
exceptionally_closed_all_day = False
|
||||
in_opening_hours = date != when.date()
|
||||
for oh in normal_opening_hours_values:
|
||||
if exceptionally_closed_all_day:
|
||||
break
|
||||
|
||||
# Not a nonworking day, not during exceptional closure and no normal opening
|
||||
# hours defined => Opened
|
||||
return {"closed": False, "exceptional_closure": False, "exceptional_closure_all_day": False}
|
||||
if oh["days"] and weekday not in oh["days"]:
|
||||
log.debug("next_opening_hour(%s): %s not in days (%s)", when, weekday, oh["days"])
|
||||
continue
|
||||
|
||||
log.debug(
|
||||
"next_opening_hour(%s): %s in days (%s), handle opening hours %s",
|
||||
when,
|
||||
weekday,
|
||||
oh["days"],
|
||||
oh["hours_periods"],
|
||||
)
|
||||
|
||||
if not oh["hours_periods"]:
|
||||
log.debug(
|
||||
"next_opening_hour(%s): %s is an all day opening day, handle exceptional closures "
|
||||
"hours %s to find the minimal opening time",
|
||||
when,
|
||||
weekday,
|
||||
exceptional_closures_hours,
|
||||
)
|
||||
if date == when.date():
|
||||
in_opening_hours = True
|
||||
test_time = when.time() if when.date() == date else datetime.datetime.min.time()
|
||||
for cl in exceptional_closures_hours:
|
||||
if cl["start"] <= test_time < cl["stop"]:
|
||||
if cl["stop"] >= datetime.datetime.max.time():
|
||||
exceptionally_closed = True
|
||||
exceptionally_closed_all_day = True
|
||||
next_opening_datetime = None
|
||||
break
|
||||
test_time = cl["stop"]
|
||||
else:
|
||||
break
|
||||
if not exceptionally_closed_all_day:
|
||||
candidate_next_opening_datetime = datetime.datetime.combine(date, test_time)
|
||||
next_opening_datetime = (
|
||||
candidate_next_opening_datetime
|
||||
if not next_opening_datetime
|
||||
or candidate_next_opening_datetime < next_opening_datetime
|
||||
else next_opening_datetime
|
||||
)
|
||||
continue
|
||||
|
||||
log.debug(
|
||||
"next_opening_hour(%s): only opened during some hours periods (%s) on %s, find the "
|
||||
"minimal starting time",
|
||||
when,
|
||||
oh["hours_periods"],
|
||||
weekday,
|
||||
)
|
||||
test_time = datetime.datetime.max.time()
|
||||
for hp in oh["hours_periods"]:
|
||||
if date == when.date() and hp["stop"] < when.time():
|
||||
log.debug(
|
||||
"next_opening_hour(%s): ignore opening hours %s before specified when time %s",
|
||||
when,
|
||||
hp,
|
||||
when.time(),
|
||||
)
|
||||
continue
|
||||
if date == when.date() and hp["start"] <= when.time() < hp["stop"]:
|
||||
in_opening_hours = True
|
||||
if exceptional_closures_hours:
|
||||
log.debug(
|
||||
"next_opening_hour(%s): check if opening hours %s match with exceptional "
|
||||
"closure hours %s",
|
||||
when,
|
||||
hp,
|
||||
exceptional_closures_hours,
|
||||
)
|
||||
for cl in exceptional_closures_hours:
|
||||
if cl["start"] <= hp["start"] and cl["stop"] >= hp["stop"]:
|
||||
log.debug(
|
||||
"next_opening_hour(%s): opening hour %s is included in exceptional "
|
||||
"closure hours %s",
|
||||
when,
|
||||
hp,
|
||||
cl,
|
||||
)
|
||||
exceptionally_closed = True
|
||||
break
|
||||
if hp["start"] < cl["start"]:
|
||||
log.debug(
|
||||
"next_opening_hour(%s): opening hour %s start before closure hours %s",
|
||||
when,
|
||||
hp,
|
||||
cl,
|
||||
)
|
||||
test_time = hp["start"] if hp["start"] < test_time else test_time
|
||||
elif cl["stop"] >= hp["start"] and cl["stop"] < hp["stop"]:
|
||||
log.debug(
|
||||
"next_opening_hour(%s): opening hour %s end after closure hours %s",
|
||||
when,
|
||||
hp,
|
||||
cl,
|
||||
)
|
||||
test_time = cl["stop"] if cl["stop"] < test_time else test_time
|
||||
elif hp["start"] < test_time:
|
||||
log.debug(
|
||||
"next_opening_hour(%s): no exceptional closure hours, use opening hours start "
|
||||
"time %s",
|
||||
when,
|
||||
hp["start"],
|
||||
)
|
||||
test_time = hp["start"]
|
||||
|
||||
if test_time < datetime.datetime.max.time():
|
||||
if date == when.date() and test_time < when.time():
|
||||
test_time = when.time()
|
||||
candidate_next_opening_datetime = datetime.datetime.combine(date, test_time)
|
||||
next_opening_datetime = (
|
||||
candidate_next_opening_datetime
|
||||
if not next_opening_datetime
|
||||
or candidate_next_opening_datetime < next_opening_datetime
|
||||
else next_opening_datetime
|
||||
)
|
||||
|
||||
if not next_opening_datetime and (
|
||||
exceptionally_closed or (date == when.date() and not in_opening_hours)
|
||||
):
|
||||
new_max_anaylse_days = max_anaylse_days - (date - when.date()).days
|
||||
if new_max_anaylse_days > 0:
|
||||
log.debug(
|
||||
"next_opening_hour(%s): exceptionally closed on %s, try on following %d days",
|
||||
when,
|
||||
date,
|
||||
new_max_anaylse_days,
|
||||
)
|
||||
next_opening_datetime = next_opening_hour(
|
||||
normal_opening_hours_values=normal_opening_hours_values,
|
||||
exceptional_closures_values=exceptional_closures_values,
|
||||
nonworking_public_holidays_values=nonworking_public_holidays_values,
|
||||
when=datetime.datetime.combine(
|
||||
date + datetime.timedelta(days=1), datetime.datetime.min.time()
|
||||
),
|
||||
max_anaylse_days=new_max_anaylse_days,
|
||||
parse=False,
|
||||
)
|
||||
if not next_opening_datetime:
|
||||
log.debug(
|
||||
"next_opening_hour(%s): no opening hours found in next %d days", when, max_anaylse_days
|
||||
)
|
||||
return False
|
||||
log.debug("next_opening_hour(%s): next opening hours=%s", when, next_opening_datetime)
|
||||
return next_opening_datetime
|
||||
|
||||
|
||||
def previous_opening_date(
|
||||
normal_opening_hours_values=None,
|
||||
exceptional_closures_values=None,
|
||||
nonworking_public_holidays_values=None,
|
||||
date=None,
|
||||
max_anaylse_days=None,
|
||||
parse=True,
|
||||
):
|
||||
"""Search for the previous opening day"""
|
||||
date = date if date else datetime.date.today()
|
||||
max_anaylse_days = max_anaylse_days if max_anaylse_days is not None else 30
|
||||
if parse:
|
||||
try:
|
||||
normal_opening_hours_values = (
|
||||
parse_normal_opening_hours(normal_opening_hours_values)
|
||||
if normal_opening_hours_values
|
||||
else None
|
||||
)
|
||||
exceptional_closures_values = (
|
||||
parse_exceptional_closures(exceptional_closures_values)
|
||||
if exceptional_closures_values
|
||||
else None
|
||||
)
|
||||
except ValueError: # pylint: disable=broad-except
|
||||
log.error(
|
||||
"previous_opening_date(%s): fail to parse normal opening hours or exceptional "
|
||||
"closures",
|
||||
date,
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
days = 0
|
||||
while days <= max_anaylse_days:
|
||||
test_date = date - datetime.timedelta(days=days)
|
||||
if its_opening_day(
|
||||
normal_opening_hours_values=normal_opening_hours_values,
|
||||
exceptional_closures_values=exceptional_closures_values,
|
||||
nonworking_public_holidays_values=nonworking_public_holidays_values,
|
||||
date=test_date,
|
||||
parse=False,
|
||||
):
|
||||
return test_date
|
||||
days += 1
|
||||
log.debug(
|
||||
"previous_opening_date(%s): no opening day found in the next %d days",
|
||||
date,
|
||||
max_anaylse_days,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def previous_opening_hour(
|
||||
normal_opening_hours_values=None,
|
||||
exceptional_closures_values=None,
|
||||
nonworking_public_holidays_values=None,
|
||||
when=None,
|
||||
max_anaylse_days=None,
|
||||
parse=True,
|
||||
):
|
||||
"""Search for the previous opening hour"""
|
||||
when = when if when else datetime.datetime.now()
|
||||
max_anaylse_days = max_anaylse_days if max_anaylse_days is not None else 30
|
||||
if parse:
|
||||
try:
|
||||
normal_opening_hours_values = (
|
||||
parse_normal_opening_hours(normal_opening_hours_values)
|
||||
if normal_opening_hours_values
|
||||
else None
|
||||
)
|
||||
exceptional_closures_values = (
|
||||
parse_exceptional_closures(exceptional_closures_values)
|
||||
if exceptional_closures_values
|
||||
else None
|
||||
)
|
||||
except ValueError: # pylint: disable=broad-except
|
||||
log.error(
|
||||
"previous_opening_hour(%s): fail to parse normal opening hours or exceptional "
|
||||
"closures",
|
||||
when,
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
date = previous_opening_date(
|
||||
normal_opening_hours_values=normal_opening_hours_values,
|
||||
exceptional_closures_values=exceptional_closures_values,
|
||||
nonworking_public_holidays_values=nonworking_public_holidays_values,
|
||||
date=when.date(),
|
||||
max_anaylse_days=max_anaylse_days,
|
||||
parse=False,
|
||||
)
|
||||
if not date:
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): no opening day found in the previous %d days",
|
||||
when,
|
||||
max_anaylse_days,
|
||||
)
|
||||
return False
|
||||
log.debug("previous_opening_hour(%s): previous opening date=%s", when, date)
|
||||
weekday = week_days[date.timetuple().tm_wday]
|
||||
log.debug("previous_opening_hour(%s): previous opening week day=%s", when, weekday)
|
||||
exceptional_closures_hours = get_exceptional_closures_hours(
|
||||
exceptional_closures_values, date=date, parse=False
|
||||
)
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): previous opening day exceptional closures hours=%s",
|
||||
when,
|
||||
exceptional_closures_hours,
|
||||
)
|
||||
previous_opening_datetime = None
|
||||
exceptionally_closed = False
|
||||
exceptionally_closed_all_day = False
|
||||
in_opening_hours = date != when.date()
|
||||
for oh in reversed(normal_opening_hours_values):
|
||||
if exceptionally_closed_all_day:
|
||||
break
|
||||
|
||||
if oh["days"] and weekday not in oh["days"]:
|
||||
log.debug("previous_opening_hour(%s): %s not in days (%s)", when, weekday, oh["days"])
|
||||
continue
|
||||
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): %s in days (%s), handle opening hours %s",
|
||||
when,
|
||||
weekday,
|
||||
oh["days"],
|
||||
oh["hours_periods"],
|
||||
)
|
||||
|
||||
if not oh["hours_periods"]:
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): %s is an all day opening day, handle exceptional "
|
||||
"closures hours %s to find the maximal opening time",
|
||||
when,
|
||||
weekday,
|
||||
exceptional_closures_hours,
|
||||
)
|
||||
if date == when.date():
|
||||
in_opening_hours = True
|
||||
test_time = when.time() if when.date() == date else datetime.datetime.max.time()
|
||||
for cl in exceptional_closures_hours:
|
||||
if cl["start"] <= test_time < cl["stop"]:
|
||||
if cl["start"] <= datetime.datetime.min.time():
|
||||
exceptionally_closed = True
|
||||
exceptionally_closed_all_day = True
|
||||
previous_opening_datetime = None
|
||||
break
|
||||
test_time = cl["start"]
|
||||
else:
|
||||
break
|
||||
if not exceptionally_closed_all_day:
|
||||
candidate_previous_opening_datetime = datetime.datetime.combine(date, test_time)
|
||||
previous_opening_datetime = (
|
||||
candidate_previous_opening_datetime
|
||||
if not previous_opening_datetime
|
||||
or candidate_previous_opening_datetime > previous_opening_datetime
|
||||
else previous_opening_datetime
|
||||
)
|
||||
continue
|
||||
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): only opened during some hours periods (%s) on %s, find the "
|
||||
"maximal opening time",
|
||||
when,
|
||||
oh["hours_periods"],
|
||||
weekday,
|
||||
)
|
||||
test_time = datetime.datetime.min.time()
|
||||
for hp in reversed(oh["hours_periods"]):
|
||||
if date == when.date() and hp["start"] > when.time():
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): ignore opening hours %s starting before specified "
|
||||
"when time %s",
|
||||
when,
|
||||
hp,
|
||||
when.time(),
|
||||
)
|
||||
continue
|
||||
if date == when.date() and hp["start"] <= when.time() < hp["stop"]:
|
||||
in_opening_hours = True
|
||||
if exceptional_closures_hours:
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): check if opening hours %s match with exceptional "
|
||||
"closure hours %s",
|
||||
when,
|
||||
hp,
|
||||
exceptional_closures_hours,
|
||||
)
|
||||
for cl in reversed(exceptional_closures_hours):
|
||||
if cl["start"] <= hp["start"] and cl["stop"] >= hp["stop"]:
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): opening hour %s is included in exceptional "
|
||||
"closure hours %s",
|
||||
when,
|
||||
hp,
|
||||
cl,
|
||||
)
|
||||
exceptionally_closed = True
|
||||
break
|
||||
if cl["stop"] < hp["stop"]:
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): opening hour %s end after closure hours %s",
|
||||
when,
|
||||
hp,
|
||||
cl,
|
||||
)
|
||||
test_time = hp["stop"] if hp["stop"] > test_time else test_time
|
||||
elif cl["start"] > hp["stop"]:
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): opening hour %s start before closure hours "
|
||||
"%s",
|
||||
when,
|
||||
hp,
|
||||
cl,
|
||||
)
|
||||
test_time = hp["stop"] if hp["stop"] > test_time else test_time
|
||||
elif cl["stop"] >= hp["stop"] and cl["start"] > hp["start"]:
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): opening hour %s start before closure hours "
|
||||
"%s",
|
||||
when,
|
||||
hp,
|
||||
cl,
|
||||
)
|
||||
test_time = cl["start"] if cl["start"] > test_time else test_time
|
||||
elif hp["stop"] > test_time:
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): no exceptional closure hours, use opening hours "
|
||||
"stop time %s",
|
||||
when,
|
||||
hp["stop"],
|
||||
)
|
||||
test_time = hp["stop"]
|
||||
|
||||
if test_time > datetime.datetime.min.time():
|
||||
if date == when.date() and test_time > when.time():
|
||||
test_time = when.time()
|
||||
candidate_previous_opening_datetime = datetime.datetime.combine(date, test_time)
|
||||
previous_opening_datetime = (
|
||||
candidate_previous_opening_datetime
|
||||
if not previous_opening_datetime
|
||||
or candidate_previous_opening_datetime > previous_opening_datetime
|
||||
else previous_opening_datetime
|
||||
)
|
||||
|
||||
if not previous_opening_datetime and (
|
||||
exceptionally_closed or (date == when.date() and not in_opening_hours)
|
||||
):
|
||||
new_max_anaylse_days = max_anaylse_days - (when.date() - date).days
|
||||
if new_max_anaylse_days > 0:
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): exceptionally closed on %s, try on previous %d days",
|
||||
when,
|
||||
date,
|
||||
new_max_anaylse_days,
|
||||
)
|
||||
previous_opening_datetime = previous_opening_hour(
|
||||
normal_opening_hours_values=normal_opening_hours_values,
|
||||
exceptional_closures_values=exceptional_closures_values,
|
||||
nonworking_public_holidays_values=nonworking_public_holidays_values,
|
||||
when=datetime.datetime.combine(
|
||||
date - datetime.timedelta(days=1), datetime.datetime.max.time()
|
||||
),
|
||||
max_anaylse_days=new_max_anaylse_days,
|
||||
parse=False,
|
||||
)
|
||||
if not previous_opening_datetime:
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): no opening hours found in previous %d days",
|
||||
when,
|
||||
max_anaylse_days,
|
||||
)
|
||||
return False
|
||||
log.debug(
|
||||
"previous_opening_hour(%s): previous opening hours=%s", when, previous_opening_datetime
|
||||
)
|
||||
return previous_opening_datetime
|
||||
|
|
|
@ -31,7 +31,7 @@ class OracleDB(DB):
|
|||
self._conn = cx_Oracle.connect(user=self._user, password=self._pwd, dsn=self._dsn)
|
||||
except cx_Oracle.Error as err:
|
||||
log.fatal(
|
||||
"An error occured during Oracle database connection (%s@%s).",
|
||||
"An error occurred during Oracle database connection (%s@%s).",
|
||||
self._user,
|
||||
self._dsn,
|
||||
exc_info=1,
|
||||
|
|
|
@ -5,6 +5,7 @@ import logging
|
|||
import sys
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
from mylib.db import DB, DBFailToConnect
|
||||
|
||||
|
@ -44,7 +45,7 @@ class PgDB(DB):
|
|||
)
|
||||
except psycopg2.Error as err:
|
||||
log.fatal(
|
||||
"An error occured during Postgresql database connection (%s@%s, database=%s).",
|
||||
"An error occurred during Postgresql database connection (%s@%s, database=%s).",
|
||||
self._user,
|
||||
self._host,
|
||||
self._db,
|
||||
|
@ -70,7 +71,7 @@ class PgDB(DB):
|
|||
return True
|
||||
except psycopg2.Error:
|
||||
log.error(
|
||||
'An error occured setting Postgresql database connection encoding to "%s"',
|
||||
'An error occurred setting Postgresql database connection encoding to "%s"',
|
||||
enc,
|
||||
exc_info=1,
|
||||
)
|
||||
|
@ -114,22 +115,18 @@ class PgDB(DB):
|
|||
:return: List of selected rows as dict on success, False otherwise
|
||||
:rtype: list, bool
|
||||
"""
|
||||
cursor = self._conn.cursor()
|
||||
cursor = self._conn.cursor(cursor_factory=RealDictCursor)
|
||||
try:
|
||||
self._log_query(sql, params)
|
||||
cursor.execute(sql, params)
|
||||
results = cursor.fetchall()
|
||||
return results
|
||||
return list(map(dict, results))
|
||||
except psycopg2.Error:
|
||||
self._log_query_exception(sql, params)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _map_row_fields_by_index(fields, row):
|
||||
return {field: row[idx] for idx, field in enumerate(fields)}
|
||||
|
||||
#
|
||||
# Depreated helpers
|
||||
# Deprecated helpers
|
||||
#
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -19,6 +19,7 @@ class Report(ConfigurableObject): # pylint: disable=useless-object-inheritance
|
|||
"subject": "Report",
|
||||
"loglevel": "WARNING",
|
||||
"logformat": "%(asctime)s - %(levelname)s - %(message)s",
|
||||
"just_try": False,
|
||||
}
|
||||
|
||||
content = []
|
||||
|
@ -26,9 +27,18 @@ class Report(ConfigurableObject): # pylint: disable=useless-object-inheritance
|
|||
formatter = None
|
||||
email_client = None
|
||||
|
||||
def __init__(self, email_client=None, initialize=True, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
email_client=None,
|
||||
add_logging_handler=False,
|
||||
send_at_exit=None,
|
||||
initialize=True,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.email_client = email_client
|
||||
self.add_logging_handler = add_logging_handler
|
||||
self._send_at_exit = send_at_exit
|
||||
self._attachment_files = []
|
||||
self._attachment_payloads = []
|
||||
|
||||
|
@ -37,7 +47,10 @@ class Report(ConfigurableObject): # pylint: disable=useless-object-inheritance
|
|||
|
||||
def configure(self, **kwargs): # pylint: disable=arguments-differ
|
||||
"""Configure options on registered mylib.Config object"""
|
||||
section = super().configure(**kwargs)
|
||||
section = super().configure(
|
||||
just_try_help=kwargs.pop("just_try_help", "Just-try mode: do not really send report"),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
section.add_option(StringOption, "recipient", comment="Report recipient email address")
|
||||
section.add_option(
|
||||
|
@ -77,8 +90,13 @@ class Report(ConfigurableObject): # pylint: disable=useless-object-inheritance
|
|||
self.formatter = logging.Formatter(self._get_option("logformat"))
|
||||
self.handler.setFormatter(self.formatter)
|
||||
|
||||
if self.add_logging_handler:
|
||||
logging.getLogger().addHandler(self.handler)
|
||||
if self._send_at_exit:
|
||||
self.send_at_exit()
|
||||
|
||||
def get_handler(self):
|
||||
"""Retreive logging handler"""
|
||||
"""Retrieve logging handler"""
|
||||
return self.handler
|
||||
|
||||
def write(self, msg):
|
||||
|
@ -97,7 +115,7 @@ class Report(ConfigurableObject): # pylint: disable=useless-object-inheritance
|
|||
"""Add attachment payload"""
|
||||
self._attachment_payloads.append(payload)
|
||||
|
||||
def send(self, subject=None, rcpt_to=None, email_client=None, just_try=False):
|
||||
def send(self, subject=None, rcpt_to=None, email_client=None, just_try=None):
|
||||
"""Send report using an EmailClient"""
|
||||
if rcpt_to is None:
|
||||
rcpt_to = self._get_option("recipient")
|
||||
|
@ -124,7 +142,9 @@ class Report(ConfigurableObject): # pylint: disable=useless-object-inheritance
|
|||
attachment_files=self._attachment_files,
|
||||
attachment_payloads=self._attachment_payloads,
|
||||
)
|
||||
if email_client.send(rcpt_to, msg=msg, just_try=just_try):
|
||||
if email_client.send(
|
||||
rcpt_to, msg=msg, just_try=just_try if just_try is not None else self._just_try
|
||||
):
|
||||
log.debug("Report sent to %s", rcpt_to)
|
||||
return True
|
||||
log.error("Fail to send report to %s", rcpt_to)
|
||||
|
|
1
mylib/scripts/email_templates/test.html
Normal file
1
mylib/scripts/email_templates/test.html
Normal file
|
@ -0,0 +1 @@
|
|||
<strong>Just a test email.</strong> <small>(sent at ${sent_date})</small>
|
1
mylib/scripts/email_templates/test.subject
Normal file
1
mylib/scripts/email_templates/test.subject
Normal file
|
@ -0,0 +1 @@
|
|||
Test email
|
1
mylib/scripts/email_templates/test.txt
Normal file
1
mylib/scripts/email_templates/test.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Just a test email sent at ${sent_date}.
|
|
@ -2,10 +2,9 @@
|
|||
import datetime
|
||||
import getpass
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from mako.template import Template as MakoTemplate
|
||||
|
||||
from mylib.scripts.helpers import add_email_opts, get_opts_parser, init_email_client, init_logging
|
||||
|
||||
log = logging.getLogger("mylib.scripts.email_test")
|
||||
|
@ -18,7 +17,10 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
|
|||
|
||||
# Options parser
|
||||
parser = get_opts_parser(just_try=True)
|
||||
add_email_opts(parser)
|
||||
add_email_opts(
|
||||
parser,
|
||||
templates_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "email_templates"),
|
||||
)
|
||||
|
||||
test_opts = parser.add_argument_group("Test email options")
|
||||
|
||||
|
@ -28,7 +30,17 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
|
|||
action="store",
|
||||
type=str,
|
||||
dest="test_to",
|
||||
help="Test email recipient",
|
||||
help="Test email recipient(s)",
|
||||
nargs="+",
|
||||
)
|
||||
|
||||
test_opts.add_argument(
|
||||
"-T",
|
||||
"--template",
|
||||
action="store_true",
|
||||
dest="template",
|
||||
help="Template name to send (default: test)",
|
||||
default="test",
|
||||
)
|
||||
|
||||
test_opts.add_argument(
|
||||
|
@ -39,10 +51,28 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
|
|||
help="Test mako templating",
|
||||
)
|
||||
|
||||
test_opts.add_argument(
|
||||
"--cc",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="test_cc",
|
||||
help="Test CC email recipient(s)",
|
||||
nargs="+",
|
||||
)
|
||||
|
||||
test_opts.add_argument(
|
||||
"--bcc",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="test_bcc",
|
||||
help="Test BCC email recipient(s)",
|
||||
nargs="+",
|
||||
)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
if not options.test_to:
|
||||
parser.error("You must specify test email recipient using -t/--to parameter")
|
||||
parser.error("You must specify at least one test email recipient using -t/--to parameter")
|
||||
sys.exit(1)
|
||||
|
||||
# Initialize logs
|
||||
|
@ -51,29 +81,21 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
|
|||
if options.email_smtp_user and not options.email_smtp_password:
|
||||
options.email_smtp_password = getpass.getpass("Please enter SMTP password: ")
|
||||
|
||||
email_client = init_email_client(
|
||||
options,
|
||||
templates=dict(
|
||||
test=dict(
|
||||
subject="Test email",
|
||||
text=(
|
||||
"Just a test email sent at {sent_date}."
|
||||
if not options.test_mako
|
||||
else MakoTemplate("Just a test email sent at ${sent_date}.")
|
||||
),
|
||||
html=(
|
||||
"<strong>Just a test email.</strong> <small>(sent at {sent_date})</small>"
|
||||
if not options.test_mako
|
||||
else MakoTemplate(
|
||||
"<strong>Just a test email.</strong> <small>(sent at ${sent_date})</small>"
|
||||
)
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
email_client = init_email_client(options)
|
||||
|
||||
log.info("Send a test email to %s", options.test_to)
|
||||
if email_client.send(options.test_to, template="test", sent_date=datetime.datetime.now()):
|
||||
log.info(
|
||||
"Send a test email to %s (CC: %s / BCC: %s)",
|
||||
", ".join(options.test_to),
|
||||
", ".join(options.test_cc) if options.test_cc else None,
|
||||
", ".join(options.test_bcc) if options.test_bcc else None,
|
||||
)
|
||||
if email_client.send(
|
||||
options.test_to,
|
||||
cc=options.test_cc,
|
||||
bcc=options.test_bcc,
|
||||
template="test",
|
||||
sent_date=datetime.datetime.now(),
|
||||
):
|
||||
log.info("Test email sent")
|
||||
sys.exit(0)
|
||||
log.error("Fail to send test email")
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
""" Test Email client using mylib.config.Config for configuration """
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from mako.template import Template as MakoTemplate
|
||||
|
||||
from mylib.config import Config
|
||||
from mylib.email import EmailClient
|
||||
|
||||
|
@ -19,7 +18,11 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
|
|||
config = Config(__doc__, __name__.replace(".", "_"))
|
||||
|
||||
email_client = EmailClient(config=config)
|
||||
email_client.configure()
|
||||
email_client.set_default(
|
||||
"templates_path",
|
||||
os.path.join(os.path.dirname(os.path.realpath(__file__)), "email_templates"),
|
||||
)
|
||||
email_client.configure(just_try=True)
|
||||
|
||||
# Options parser
|
||||
parser = config.get_arguments_parser(description=__doc__)
|
||||
|
@ -32,7 +35,17 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
|
|||
action="store",
|
||||
type=str,
|
||||
dest="test_to",
|
||||
help="Test email recipient",
|
||||
help="Test email recipient(s)",
|
||||
nargs="+",
|
||||
)
|
||||
|
||||
test_opts.add_argument(
|
||||
"-T",
|
||||
"--template",
|
||||
action="store_true",
|
||||
dest="template",
|
||||
help="Template name to send (default: test)",
|
||||
default="test",
|
||||
)
|
||||
|
||||
test_opts.add_argument(
|
||||
|
@ -43,32 +56,37 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
|
|||
help="Test mako templating",
|
||||
)
|
||||
|
||||
test_opts.add_argument(
|
||||
"--cc",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="test_cc",
|
||||
help="Test CC email recipient(s)",
|
||||
nargs="+",
|
||||
)
|
||||
|
||||
test_opts.add_argument(
|
||||
"--bcc",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="test_bcc",
|
||||
help="Test BCC email recipient(s)",
|
||||
nargs="+",
|
||||
)
|
||||
|
||||
options = config.parse_arguments_options()
|
||||
|
||||
if not options.test_to:
|
||||
parser.error("You must specify test email recipient using -t/--to parameter")
|
||||
parser.error("You must specify at least one test email recipient using -t/--to parameter")
|
||||
sys.exit(1)
|
||||
|
||||
email_client.templates = dict(
|
||||
test=dict(
|
||||
subject="Test email",
|
||||
text=(
|
||||
"Just a test email sent at {sent_date}."
|
||||
if not options.test_mako
|
||||
else MakoTemplate("Just a test email sent at ${sent_date}.")
|
||||
),
|
||||
html=(
|
||||
"<strong>Just a test email.</strong> <small>(sent at {sent_date})</small>"
|
||||
if not options.test_mako
|
||||
else MakoTemplate(
|
||||
"<strong>Just a test email.</strong> <small>(sent at ${sent_date})</small>"
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
logging.info("Send a test email to %s", options.test_to)
|
||||
if email_client.send(options.test_to, template="test", sent_date=datetime.datetime.now()):
|
||||
if email_client.send(
|
||||
options.test_to,
|
||||
cc=options.test_cc,
|
||||
bcc=options.test_bcc,
|
||||
template="test",
|
||||
sent_date=datetime.datetime.now(),
|
||||
):
|
||||
logging.info("Test email sent")
|
||||
sys.exit(0)
|
||||
logging.error("Fail to send test email")
|
||||
|
|
|
@ -31,17 +31,19 @@ def init_logging(options, name, report=None):
|
|||
|
||||
|
||||
def get_default_opt_value(config, default_config, key):
|
||||
"""Retreive default option value from config or default config dictionaries"""
|
||||
"""Retrieve default option value from config or default config dictionaries"""
|
||||
if config and key in config:
|
||||
return config[key]
|
||||
return default_config.get(key)
|
||||
|
||||
|
||||
def get_opts_parser(desc=None, just_try=False, just_one=False, progress=False, config=None):
|
||||
def get_opts_parser(
|
||||
desc=None, just_try=False, just_one=False, progress=False, config=None, **kwargs
|
||||
):
|
||||
"""Retrieve options parser"""
|
||||
default_config = dict(logfile=None)
|
||||
default_config = {"logfile": None}
|
||||
|
||||
parser = argparse.ArgumentParser(description=desc)
|
||||
parser = argparse.ArgumentParser(description=desc, **kwargs)
|
||||
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="store_true", dest="verbose", help="Enable verbose mode"
|
||||
|
@ -87,23 +89,25 @@ def get_opts_parser(desc=None, just_try=False, just_one=False, progress=False, c
|
|||
return parser
|
||||
|
||||
|
||||
def add_email_opts(parser, config=None):
|
||||
def add_email_opts(parser, config=None, **defaults):
|
||||
"""Add email options"""
|
||||
email_opts = parser.add_argument_group("Email options")
|
||||
|
||||
default_config = dict(
|
||||
smtp_host="127.0.0.1",
|
||||
smtp_port=25,
|
||||
smtp_ssl=False,
|
||||
smtp_tls=False,
|
||||
smtp_user=None,
|
||||
smtp_password=None,
|
||||
smtp_debug=False,
|
||||
email_encoding=sys.getdefaultencoding(),
|
||||
sender_name=getpass.getuser(),
|
||||
sender_email=f"{getpass.getuser()}@{socket.gethostname()}",
|
||||
catch_all=False,
|
||||
)
|
||||
default_config = {
|
||||
"smtp_host": "127.0.0.1",
|
||||
"smtp_port": 25,
|
||||
"smtp_ssl": False,
|
||||
"smtp_tls": False,
|
||||
"smtp_user": None,
|
||||
"smtp_password": None,
|
||||
"smtp_debug": False,
|
||||
"email_encoding": sys.getdefaultencoding(),
|
||||
"sender_name": getpass.getuser(),
|
||||
"sender_email": f"{getpass.getuser()}@{socket.gethostname()}",
|
||||
"catch_all": False,
|
||||
"templates_path": None,
|
||||
}
|
||||
default_config.update(defaults)
|
||||
|
||||
email_opts.add_argument(
|
||||
"--smtp-host",
|
||||
|
@ -220,27 +224,25 @@ def add_email_opts(parser, config=None):
|
|||
default=get_default_opt_value(config, default_config, "catch_all"),
|
||||
)
|
||||
|
||||
email_opts.add_argument(
|
||||
"--templates-path",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="email_templates_path",
|
||||
help=(
|
||||
"Load templates from specify directory "
|
||||
f'(default: {get_default_opt_value(config, default_config, "templates_path")})'
|
||||
),
|
||||
default=get_default_opt_value(config, default_config, "templates_path"),
|
||||
)
|
||||
|
||||
|
||||
def init_email_client(options, **kwargs):
|
||||
"""Initialize email client from calling script options"""
|
||||
from mylib.email import EmailClient # pylint: disable=import-outside-toplevel
|
||||
|
||||
log.info("Initialize Email client")
|
||||
return EmailClient(
|
||||
smtp_host=options.email_smtp_host,
|
||||
smtp_port=options.email_smtp_port,
|
||||
smtp_ssl=options.email_smtp_ssl,
|
||||
smtp_tls=options.email_smtp_tls,
|
||||
smtp_user=options.email_smtp_user,
|
||||
smtp_password=options.email_smtp_password,
|
||||
smtp_debug=options.email_smtp_debug,
|
||||
sender_name=options.email_sender_name,
|
||||
sender_email=options.email_sender_email,
|
||||
catch_all_addr=options.email_catch_all,
|
||||
just_try=options.just_try if hasattr(options, "just_try") else False,
|
||||
encoding=options.email_encoding,
|
||||
**kwargs,
|
||||
)
|
||||
return EmailClient(options=options, initialize=True, **kwargs)
|
||||
|
||||
|
||||
def add_sftp_opts(parser):
|
||||
|
|
|
@ -20,16 +20,21 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
|
|||
report_opts = parser.add_argument_group("Report options")
|
||||
|
||||
report_opts.add_argument(
|
||||
"-t", "--to", action="store", type=str, dest="report_rcpt", help="Send report to this email"
|
||||
"-t",
|
||||
"--to",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="report_recipient",
|
||||
help="Send report to this email",
|
||||
)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
if not options.report_rcpt:
|
||||
if not options.report_recipient:
|
||||
parser.error("You must specify a report recipient using -t/--to parameter")
|
||||
|
||||
# Initialize logs
|
||||
report = Report(rcpt_to=options.report_rcpt, subject="Test report")
|
||||
report = Report(options=options, subject="Test report")
|
||||
init_logging(options, "Test Report", report=report)
|
||||
|
||||
email_client = init_email_client(options)
|
||||
|
|
|
@ -43,15 +43,16 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
|
|||
options.sftp_password = getpass.getpass("Please enter SFTP password: ")
|
||||
|
||||
log.info("Initialize Email client")
|
||||
sftp = SFTPClient(options=options, just_try=options.just_try)
|
||||
sftp = SFTPClient(options=options)
|
||||
sftp.connect()
|
||||
atexit.register(sftp.close)
|
||||
|
||||
log.debug("Create tempory file")
|
||||
log.debug("Create temporary file")
|
||||
test_content = b"Juste un test."
|
||||
tmp_dir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
|
||||
tmp_file = os.path.join(
|
||||
tmp_dir.name, f'tmp{"".join(random.choice(string.ascii_lowercase) for i in range(8))}'
|
||||
tmp_dir.name,
|
||||
f'tmp{"".join(random.choice(string.ascii_lowercase) for i in range(8))}', # nosec
|
||||
)
|
||||
log.debug('Temporary file path: "%s"', tmp_file)
|
||||
with open(tmp_file, "wb") as file_desc:
|
||||
|
@ -73,30 +74,31 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
|
|||
else os.path.basename(tmp_file)
|
||||
)
|
||||
|
||||
with tempfile.NamedTemporaryFile() as tmp_file2:
|
||||
log.info("Retrieve test file to %s", tmp_file2.name)
|
||||
if not sftp.get_file(remote_filepath, tmp_file2.name):
|
||||
log.error("Fail to retrieve test file")
|
||||
else:
|
||||
with open(tmp_file2.name, "rb") as file_desc:
|
||||
content = file_desc.read()
|
||||
log.debug("Read content: %s", content)
|
||||
if test_content == content:
|
||||
log.info("Content file retrieved match with uploaded one")
|
||||
else:
|
||||
log.error("Content file retrieved doest not match with uploaded one")
|
||||
if not sftp._just_try: # pylint: disable=protected-access
|
||||
with tempfile.NamedTemporaryFile() as tmp_file2:
|
||||
log.info("Retrieve test file to %s", tmp_file2.name)
|
||||
if not sftp.get_file(remote_filepath, tmp_file2.name):
|
||||
log.error("Fail to retrieve test file")
|
||||
else:
|
||||
with open(tmp_file2.name, "rb") as file_desc:
|
||||
content = file_desc.read()
|
||||
log.debug("Read content: %s", content)
|
||||
if test_content == content:
|
||||
log.info("Content file retrieved match with uploaded one")
|
||||
else:
|
||||
log.error("Content file retrieved doest not match with uploaded one")
|
||||
|
||||
try:
|
||||
log.info("Remotly open test file %s", remote_filepath)
|
||||
file_desc = sftp.open_file(remote_filepath)
|
||||
content = file_desc.read()
|
||||
log.debug("Read content: %s", content)
|
||||
if test_content == content:
|
||||
log.info("Content of remote file match with uploaded one")
|
||||
else:
|
||||
log.error("Content of remote file doest not match with uploaded one")
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.exception("An exception occurred remotly opening test file %s", remote_filepath)
|
||||
try:
|
||||
log.info("Remotly open test file %s", remote_filepath)
|
||||
file_desc = sftp.open_file(remote_filepath)
|
||||
content = file_desc.read()
|
||||
log.debug("Read content: %s", content)
|
||||
if test_content == content:
|
||||
log.info("Content of remote file match with uploaded one")
|
||||
else:
|
||||
log.error("Content of remote file doest not match with uploaded one")
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.exception("An exception occurred remotly opening test file %s", remote_filepath)
|
||||
|
||||
if sftp.remove_file(remote_filepath):
|
||||
log.info("Test file removed on SFTP server")
|
||||
|
|
12
mylib/scripts/telltale_check_test.py
Normal file
12
mylib/scripts/telltale_check_test.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
""" Test telltale file """
|
||||
import logging
|
||||
|
||||
from mylib.scripts.telltale_test import default_filepath
|
||||
from mylib.telltale import TelltaleFile
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
"""Script main"""
|
||||
TelltaleFile.check_entrypoint(argv=argv, default_filepath=default_filepath)
|
40
mylib/scripts/telltale_test.py
Normal file
40
mylib/scripts/telltale_test.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
""" Test telltale file """
|
||||
import logging
|
||||
import os.path
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from mylib.scripts.helpers import get_opts_parser, init_logging
|
||||
from mylib.telltale import TelltaleFile
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
default_filepath = os.path.join(tempfile.gettempdir(), f"{__name__}.last")
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
"""Script main"""
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
# Options parser
|
||||
parser = get_opts_parser()
|
||||
options = parser.parse_args()
|
||||
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--telltale-file-path",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="telltale_file_path",
|
||||
help=f"Telltale file path (default: {default_filepath})",
|
||||
default=default_filepath,
|
||||
)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
# Initialize logs
|
||||
init_logging(options, __doc__)
|
||||
|
||||
telltale_file = TelltaleFile(filepath=options.telltale_file_path)
|
||||
telltale_file.update()
|
|
@ -40,9 +40,15 @@ class SFTPClient(ConfigurableObject):
|
|||
initial_directory = None
|
||||
|
||||
# pylint: disable=arguments-differ,arguments-renamed
|
||||
def configure(self, just_try=True, **kwargs):
|
||||
def configure(self, **kwargs):
|
||||
"""Configure options on registered mylib.Config object"""
|
||||
section = super().configure(**kwargs)
|
||||
section = super().configure(
|
||||
just_try=kwargs.pop("just_try", True),
|
||||
just_try_help=kwargs.pop(
|
||||
"just_try_help", "Just-try mode: do not really make change on remote SFTP host"
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
section.add_option(
|
||||
StringOption,
|
||||
|
@ -80,14 +86,6 @@ class SFTPClient(ConfigurableObject):
|
|||
comment="Auto add unknown host key",
|
||||
)
|
||||
|
||||
if just_try:
|
||||
section.add_option(
|
||||
BooleanOption,
|
||||
"just_try",
|
||||
default=self._defaults["just_try"],
|
||||
comment="Just-try mode: do not really make change on remote SFTP host",
|
||||
)
|
||||
|
||||
return section
|
||||
|
||||
def initialize(self, loaded_config=None):
|
||||
|
@ -118,13 +116,13 @@ class SFTPClient(ConfigurableObject):
|
|||
if self.initial_directory:
|
||||
log.debug("Initial remote directory: '%s'", self.initial_directory)
|
||||
else:
|
||||
log.debug("Fail to retreive remote directory, use empty string instead")
|
||||
log.debug("Fail to retrieve remote directory, use empty string instead")
|
||||
self.initial_directory = ""
|
||||
|
||||
def get_file(self, remote_filepath, local_filepath):
|
||||
"""Retrieve a file from SFTP server"""
|
||||
self.connect()
|
||||
log.debug("Retreive file '%s' to '%s'", remote_filepath, local_filepath)
|
||||
log.debug("Retrieve file '%s' to '%s'", remote_filepath, local_filepath)
|
||||
return self.sftp_client.get(remote_filepath, local_filepath) is None
|
||||
|
||||
def open_file(self, remote_filepath, mode="r"):
|
||||
|
@ -141,7 +139,7 @@ class SFTPClient(ConfigurableObject):
|
|||
os.path.basename(filepath),
|
||||
)
|
||||
log.debug("Upload file '%s' to '%s'", filepath, remote_filepath)
|
||||
if self._get_option("just_try"):
|
||||
if self._just_try:
|
||||
log.debug(
|
||||
"Just-try mode: do not really upload file '%s' to '%s'", filepath, remote_filepath
|
||||
)
|
||||
|
@ -153,7 +151,7 @@ class SFTPClient(ConfigurableObject):
|
|||
"""Remove a file on SFTP server"""
|
||||
self.connect()
|
||||
log.debug("Remove file '%s'", filepath)
|
||||
if self._get_option("just_try"):
|
||||
if self._just_try:
|
||||
log.debug("Just - try mode: do not really remove file '%s'", filepath)
|
||||
return True
|
||||
return self.sftp_client.remove(filepath) is None
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
""" Telltale files helpers """
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from mylib import pretty_format_timedelta
|
||||
from mylib.scripts.helpers import get_opts_parser, init_logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_WARNING_THRESHOLD = 90
|
||||
DEFAULT_CRITICAL_THRESHOLD = 240
|
||||
|
||||
|
||||
class TelltaleFile:
|
||||
"""Telltale file helper class"""
|
||||
|
@ -27,7 +35,7 @@ class TelltaleFile:
|
|||
|
||||
@property
|
||||
def last_update(self):
|
||||
"""Retreive last update datetime of the telltall file"""
|
||||
"""Retrieve last update datetime of the telltall file"""
|
||||
try:
|
||||
return datetime.datetime.fromtimestamp(os.stat(self.filepath).st_mtime)
|
||||
except FileNotFoundError:
|
||||
|
@ -50,3 +58,108 @@ class TelltaleFile:
|
|||
return True
|
||||
except FileNotFoundError:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def check_entrypoint(
|
||||
cls,
|
||||
argv=None,
|
||||
description=None,
|
||||
default_filepath=None,
|
||||
default_warning_threshold=None,
|
||||
default_critical_threshold=None,
|
||||
fail_message=None,
|
||||
success_message=None,
|
||||
):
|
||||
"""Entry point of the script to check a telltale file last update"""
|
||||
argv = argv if argv else sys.argv
|
||||
description = description if description else "Check last execution date"
|
||||
parser = get_opts_parser(desc=description, exit_on_error=False)
|
||||
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--telltale-file-path",
|
||||
action="store",
|
||||
type=str,
|
||||
dest="telltale_file_path",
|
||||
help=f"Telltale file path (default: {default_filepath})",
|
||||
default=default_filepath,
|
||||
required=not default_filepath,
|
||||
)
|
||||
|
||||
default_warning_threshold = (
|
||||
default_warning_threshold
|
||||
if default_warning_threshold is not None
|
||||
else DEFAULT_WARNING_THRESHOLD
|
||||
)
|
||||
default_critical_threshold = (
|
||||
default_critical_threshold
|
||||
if default_critical_threshold is not None
|
||||
else DEFAULT_CRITICAL_THRESHOLD
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
"--warning",
|
||||
type=int,
|
||||
dest="warning",
|
||||
help=(
|
||||
"Specify warning threshold (in minutes, default: "
|
||||
f"{default_warning_threshold} minutes)"
|
||||
),
|
||||
default=default_warning_threshold,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--critical",
|
||||
type=int,
|
||||
dest="critical",
|
||||
help=(
|
||||
"Specify critical threshold (in minutes, default: "
|
||||
f"{default_critical_threshold} minutes)"
|
||||
),
|
||||
default=default_critical_threshold,
|
||||
)
|
||||
|
||||
try:
|
||||
options = parser.parse_args(argv[1:])
|
||||
except argparse.ArgumentError as err:
|
||||
print(f"UNKNOWN - {err}")
|
||||
sys.exit(3)
|
||||
|
||||
# Initialize logs
|
||||
init_logging(options, argv[0])
|
||||
|
||||
telltale_file = cls(filepath=options.telltale_file_path)
|
||||
last = telltale_file.last_update
|
||||
if not last:
|
||||
status = "UNKNOWN"
|
||||
exit_code = 3
|
||||
msg = (
|
||||
fail_message
|
||||
if fail_message
|
||||
else "Fail to retrieve last successful date of execution"
|
||||
)
|
||||
else:
|
||||
delay = datetime.datetime.now() - last
|
||||
msg = (
|
||||
success_message
|
||||
if success_message
|
||||
else "Last successful execution was {last_delay} ago ({last_date})"
|
||||
).format(
|
||||
last_delay=pretty_format_timedelta(delay),
|
||||
last_date=last.strftime("%Y/%m/%d %H:%M:%S"),
|
||||
)
|
||||
|
||||
if delay >= datetime.timedelta(minutes=options.critical):
|
||||
status = "CRITICAL"
|
||||
exit_code = 2
|
||||
elif delay >= datetime.timedelta(minutes=options.warning):
|
||||
status = "WARNING"
|
||||
exit_code = 1
|
||||
else:
|
||||
status = "OK"
|
||||
exit_code = 0
|
||||
|
||||
print(f"{status} - {msg}")
|
||||
sys.exit(exit_code)
|
||||
|
|
13
setup.py
13
setup.py
|
@ -45,10 +45,14 @@ for extra, deps in extras_require.items():
|
|||
|
||||
version = "0.1"
|
||||
|
||||
with open("README.md", encoding="utf-8") as fd:
|
||||
long_description = fd.read()
|
||||
|
||||
setup(
|
||||
name="mylib",
|
||||
version=version,
|
||||
description="A set of helpers small libs to make common tasks easier in my script development",
|
||||
long_description=long_description,
|
||||
classifiers=[
|
||||
"Programming Language :: Python",
|
||||
],
|
||||
|
@ -59,6 +63,13 @@ setup(
|
|||
url="https://gogs.zionetrix.net/bn8/python-mylib",
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
package_data={
|
||||
"": [
|
||||
"scripts/email_templates/*.subject",
|
||||
"scripts/email_templates/*.txt",
|
||||
"scripts/email_templates/*.html",
|
||||
],
|
||||
},
|
||||
zip_safe=False,
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
|
@ -69,6 +80,8 @@ setup(
|
|||
"mylib-test-report = mylib.scripts.report_test:main",
|
||||
"mylib-test-ldap = mylib.scripts.ldap_test:main",
|
||||
"mylib-test-sftp = mylib.scripts.sftp_test:main",
|
||||
"mylib-test-telltale = mylib.scripts.telltale_test:main",
|
||||
"mylib-test-telltale-check = mylib.scripts.telltale_check_test:main",
|
||||
],
|
||||
},
|
||||
)
|
||||
|
|
2
tests.sh
2
tests.sh
|
@ -32,7 +32,7 @@ do
|
|||
set -x
|
||||
;;
|
||||
*)
|
||||
usage "Unkown parameter '$OPT'"
|
||||
usage "Unknown parameter '$OPT'"
|
||||
esac
|
||||
let idx=idx+1
|
||||
done
|
||||
|
|
|
@ -10,7 +10,7 @@ import pytest
|
|||
|
||||
from mylib.config import BooleanOption, Config, ConfigSection, StringOption
|
||||
|
||||
runned = {}
|
||||
tested = {}
|
||||
|
||||
|
||||
def test_config_init_default_args():
|
||||
|
@ -58,24 +58,24 @@ def test_add_section_with_callback():
|
|||
config = Config("Test app")
|
||||
name = "test_section"
|
||||
|
||||
global runned
|
||||
runned["test_add_section_with_callback"] = False
|
||||
global tested
|
||||
tested["test_add_section_with_callback"] = False
|
||||
|
||||
def test_callback(loaded_config):
|
||||
global runned
|
||||
global tested
|
||||
assert loaded_config == config
|
||||
assert runned["test_add_section_with_callback"] is False
|
||||
runned["test_add_section_with_callback"] = True
|
||||
assert tested["test_add_section_with_callback"] is False
|
||||
tested["test_add_section_with_callback"] = True
|
||||
|
||||
section = config.add_section(name, loaded_callback=test_callback)
|
||||
assert isinstance(section, ConfigSection)
|
||||
assert test_callback in config._loaded_callbacks
|
||||
assert runned["test_add_section_with_callback"] is False
|
||||
assert tested["test_add_section_with_callback"] is False
|
||||
|
||||
config.parse_arguments_options(argv=[], create=False)
|
||||
assert runned["test_add_section_with_callback"] is True
|
||||
assert tested["test_add_section_with_callback"] is True
|
||||
assert test_callback in config._loaded_callbacks_executed
|
||||
# Try to execute again to verify callback is not runned again
|
||||
# Try to execute again to verify callback is not tested again
|
||||
config._loaded()
|
||||
|
||||
|
||||
|
@ -84,21 +84,21 @@ def test_add_section_with_callback_already_loaded():
|
|||
name = "test_section"
|
||||
config.parse_arguments_options(argv=[], create=False)
|
||||
|
||||
global runned
|
||||
runned["test_add_section_with_callback_already_loaded"] = False
|
||||
global tested
|
||||
tested["test_add_section_with_callback_already_loaded"] = False
|
||||
|
||||
def test_callback(loaded_config):
|
||||
global runned
|
||||
global tested
|
||||
assert loaded_config == config
|
||||
assert runned["test_add_section_with_callback_already_loaded"] is False
|
||||
runned["test_add_section_with_callback_already_loaded"] = True
|
||||
assert tested["test_add_section_with_callback_already_loaded"] is False
|
||||
tested["test_add_section_with_callback_already_loaded"] = True
|
||||
|
||||
section = config.add_section(name, loaded_callback=test_callback)
|
||||
assert isinstance(section, ConfigSection)
|
||||
assert runned["test_add_section_with_callback_already_loaded"] is True
|
||||
assert tested["test_add_section_with_callback_already_loaded"] is True
|
||||
assert test_callback in config._loaded_callbacks
|
||||
assert test_callback in config._loaded_callbacks_executed
|
||||
# Try to execute again to verify callback is not runned again
|
||||
# Try to execute again to verify callback is not tested again
|
||||
config._loaded()
|
||||
|
||||
|
||||
|
@ -126,14 +126,14 @@ def test_add_option_custom_args():
|
|||
section = config.add_section("my_section")
|
||||
assert isinstance(section, ConfigSection)
|
||||
name = "my_option"
|
||||
kwargs = dict(
|
||||
default="default value",
|
||||
comment="my comment",
|
||||
no_arg=True,
|
||||
arg="--my-option",
|
||||
short_arg="-M",
|
||||
arg_help="My help",
|
||||
)
|
||||
kwargs = {
|
||||
"default": "default value",
|
||||
"comment": "my comment",
|
||||
"no_arg": True,
|
||||
"arg": "--my-option",
|
||||
"short_arg": "-M",
|
||||
"arg_help": "My help",
|
||||
}
|
||||
option = section.add_option(StringOption, name, **kwargs)
|
||||
assert isinstance(option, StringOption)
|
||||
assert name in section.options and section.options[name] == option
|
||||
|
|
|
@ -74,9 +74,14 @@ class FakeMySQLdb:
|
|||
just_try = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
allowed_kwargs = dict(
|
||||
db=str, user=str, passwd=(str, None), host=str, charset=str, use_unicode=bool
|
||||
)
|
||||
allowed_kwargs = {
|
||||
"db": str,
|
||||
"user": str,
|
||||
"passwd": (str, None),
|
||||
"host": str,
|
||||
"charset": str,
|
||||
"use_unicode": bool,
|
||||
}
|
||||
for arg, value in kwargs.items():
|
||||
assert arg in allowed_kwargs, f'Invalid arg {arg}="{value}"'
|
||||
assert isinstance(
|
||||
|
@ -200,21 +205,23 @@ mock_doSelect_just_try = mock_doSQL_just_try
|
|||
|
||||
|
||||
def test_combine_params_with_to_add_parameter():
|
||||
assert MyDB._combine_params(dict(test1=1), dict(test2=2)) == dict(test1=1, test2=2)
|
||||
assert MyDB._combine_params({"test1": 1}, {"test2": 2}) == {"test1": 1, "test2": 2}
|
||||
|
||||
|
||||
def test_combine_params_with_kargs():
|
||||
assert MyDB._combine_params(dict(test1=1), test2=2) == dict(test1=1, test2=2)
|
||||
assert MyDB._combine_params({"test1": 1}, test2=2) == {"test1": 1, "test2": 2}
|
||||
|
||||
|
||||
def test_combine_params_with_kargs_and_to_add_parameter():
|
||||
assert MyDB._combine_params(dict(test1=1), dict(test2=2), test3=3) == dict(
|
||||
test1=1, test2=2, test3=3
|
||||
)
|
||||
assert MyDB._combine_params({"test1": 1}, {"test2": 2}, test3=3) == {
|
||||
"test1": 1,
|
||||
"test2": 2,
|
||||
"test3": 3,
|
||||
}
|
||||
|
||||
|
||||
def test_format_where_clauses_params_are_preserved():
|
||||
args = ("test = test", dict(test1=1))
|
||||
args = ("test = test", {"test1": 1})
|
||||
assert MyDB._format_where_clauses(*args) == args
|
||||
|
||||
|
||||
|
@ -223,12 +230,12 @@ def test_format_where_clauses_raw():
|
|||
|
||||
|
||||
def test_format_where_clauses_tuple_clause_with_params():
|
||||
where_clauses = ("test1 = %(test1)s AND test2 = %(test2)s", dict(test1=1, test2=2))
|
||||
where_clauses = ("test1 = %(test1)s AND test2 = %(test2)s", {"test1": 1, "test2": 2})
|
||||
assert MyDB._format_where_clauses(where_clauses) == where_clauses
|
||||
|
||||
|
||||
def test_format_where_clauses_dict():
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
assert MyDB._format_where_clauses(where_clauses) == (
|
||||
"`test1` = %(test1)s AND `test2` = %(test2)s",
|
||||
where_clauses,
|
||||
|
@ -236,15 +243,15 @@ def test_format_where_clauses_dict():
|
|||
|
||||
|
||||
def test_format_where_clauses_combined_types():
|
||||
where_clauses = ("test1 = 1", ("test2 LIKE %(test2)s", dict(test2=2)), dict(test3=3, test4=4))
|
||||
where_clauses = ("test1 = 1", ("test2 LIKE %(test2)s", {"test2": 2}), {"test3": 3, "test4": 4})
|
||||
assert MyDB._format_where_clauses(where_clauses) == (
|
||||
"test1 = 1 AND test2 LIKE %(test2)s AND `test3` = %(test3)s AND `test4` = %(test4)s",
|
||||
dict(test2=2, test3=3, test4=4),
|
||||
{"test2": 2, "test3": 3, "test4": 4},
|
||||
)
|
||||
|
||||
|
||||
def test_format_where_clauses_with_where_op():
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
assert MyDB._format_where_clauses(where_clauses, where_op="OR") == (
|
||||
"`test1` = %(test1)s OR `test2` = %(test2)s",
|
||||
where_clauses,
|
||||
|
@ -253,7 +260,7 @@ def test_format_where_clauses_with_where_op():
|
|||
|
||||
def test_add_where_clauses():
|
||||
sql = "SELECT * FROM table"
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
assert MyDB._add_where_clauses(sql, None, where_clauses) == (
|
||||
sql + " WHERE `test1` = %(test1)s AND `test2` = %(test2)s",
|
||||
where_clauses,
|
||||
|
@ -262,11 +269,11 @@ def test_add_where_clauses():
|
|||
|
||||
def test_add_where_clauses_preserved_params():
|
||||
sql = "SELECT * FROM table"
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
params = dict(fake1=1)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
params = {"fake1": 1}
|
||||
assert MyDB._add_where_clauses(sql, params.copy(), where_clauses) == (
|
||||
sql + " WHERE `test1` = %(test1)s AND `test2` = %(test2)s",
|
||||
dict(**where_clauses, **params),
|
||||
{**where_clauses, **params},
|
||||
)
|
||||
|
||||
|
||||
|
@ -281,11 +288,11 @@ def test_add_where_clauses_with_op():
|
|||
|
||||
def test_add_where_clauses_with_duplicated_field():
|
||||
sql = "UPDATE table SET test1=%(test1)s"
|
||||
params = dict(test1="new_value")
|
||||
where_clauses = dict(test1="where_value")
|
||||
params = {"test1": "new_value"}
|
||||
where_clauses = {"test1": "where_value"}
|
||||
assert MyDB._add_where_clauses(sql, params, where_clauses) == (
|
||||
sql + " WHERE `test1` = %(test1_1)s",
|
||||
dict(test1="new_value", test1_1="where_value"),
|
||||
{"test1": "new_value", "test1_1": "where_value"},
|
||||
)
|
||||
|
||||
|
||||
|
@ -295,7 +302,7 @@ def test_quote_table_name():
|
|||
|
||||
|
||||
def test_insert(mocker, test_mydb):
|
||||
values = dict(test1=1, test2=2)
|
||||
values = {"test1": 1, "test2": 2}
|
||||
mocker.patch(
|
||||
"mylib.mysql.MyDB.doSQL",
|
||||
generate_mock_doSQL(
|
||||
|
@ -308,18 +315,18 @@ def test_insert(mocker, test_mydb):
|
|||
|
||||
def test_insert_just_try(mocker, test_mydb):
|
||||
mocker.patch("mylib.mysql.MyDB.doSQL", mock_doSQL_just_try)
|
||||
assert test_mydb.insert("mytable", dict(test1=1, test2=2), just_try=True)
|
||||
assert test_mydb.insert("mytable", {"test1": 1, "test2": 2}, just_try=True)
|
||||
|
||||
|
||||
def test_update(mocker, test_mydb):
|
||||
values = dict(test1=1, test2=2)
|
||||
where_clauses = dict(test3=3, test4=4)
|
||||
values = {"test1": 1, "test2": 2}
|
||||
where_clauses = {"test3": 3, "test4": 4}
|
||||
mocker.patch(
|
||||
"mylib.mysql.MyDB.doSQL",
|
||||
generate_mock_doSQL(
|
||||
"UPDATE `mytable` SET `test1` = %(test1)s, `test2` = %(test2)s WHERE `test3` ="
|
||||
" %(test3)s AND `test4` = %(test4)s",
|
||||
dict(**values, **where_clauses),
|
||||
{**values, **where_clauses},
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -328,11 +335,11 @@ def test_update(mocker, test_mydb):
|
|||
|
||||
def test_update_just_try(mocker, test_mydb):
|
||||
mocker.patch("mylib.mysql.MyDB.doSQL", mock_doSQL_just_try)
|
||||
assert test_mydb.update("mytable", dict(test1=1, test2=2), None, just_try=True)
|
||||
assert test_mydb.update("mytable", {"test1": 1, "test2": 2}, None, just_try=True)
|
||||
|
||||
|
||||
def test_delete(mocker, test_mydb):
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
mocker.patch(
|
||||
"mylib.mysql.MyDB.doSQL",
|
||||
generate_mock_doSQL(
|
||||
|
@ -361,23 +368,27 @@ def test_truncate_just_try(mocker, test_mydb):
|
|||
|
||||
def test_select(mocker, test_mydb):
|
||||
fields = ("field1", "field2")
|
||||
where_clauses = dict(test3=3, test4=4)
|
||||
where_clauses = {"test3": 3, "test4": 4}
|
||||
expected_return = [
|
||||
dict(field1=1, field2=2),
|
||||
dict(field1=2, field2=3),
|
||||
{"field1": 1, "field2": 2},
|
||||
{"field1": 2, "field2": 3},
|
||||
]
|
||||
order_by = "field1, DESC"
|
||||
limit = 10
|
||||
mocker.patch(
|
||||
"mylib.mysql.MyDB.doSelect",
|
||||
generate_mock_doSQL(
|
||||
"SELECT `field1`, `field2` FROM `mytable` WHERE `test3` = %(test3)s AND `test4` ="
|
||||
" %(test4)s ORDER BY " + order_by,
|
||||
" %(test4)s ORDER BY " + order_by + " LIMIT " + str(limit), # nosec: B608
|
||||
where_clauses,
|
||||
expected_return,
|
||||
),
|
||||
)
|
||||
|
||||
assert test_mydb.select("mytable", where_clauses, fields, order_by=order_by) == expected_return
|
||||
assert (
|
||||
test_mydb.select("mytable", where_clauses, fields, order_by=order_by, limit=limit)
|
||||
== expected_return
|
||||
)
|
||||
|
||||
|
||||
def test_select_without_field_and_order_by(mocker, test_mydb):
|
||||
|
@ -397,14 +408,14 @@ def test_select_just_try(mocker, test_mydb):
|
|||
|
||||
|
||||
def test_connect(mocker, test_mydb):
|
||||
expected_kwargs = dict(
|
||||
db=test_mydb._db,
|
||||
user=test_mydb._user,
|
||||
host=test_mydb._host,
|
||||
passwd=test_mydb._pwd,
|
||||
charset=test_mydb._charset,
|
||||
use_unicode=True,
|
||||
)
|
||||
expected_kwargs = {
|
||||
"db": test_mydb._db,
|
||||
"user": test_mydb._user,
|
||||
"host": test_mydb._host,
|
||||
"passwd": test_mydb._pwd,
|
||||
"charset": test_mydb._charset,
|
||||
"use_unicode": True,
|
||||
}
|
||||
|
||||
mocker.patch("MySQLdb.connect", generate_mock_args(expected_kwargs=expected_kwargs))
|
||||
|
||||
|
@ -421,7 +432,7 @@ def test_close_connected(fake_connected_mydb):
|
|||
|
||||
def test_doSQL(fake_connected_mydb):
|
||||
fake_connected_mydb._conn.expected_sql = "DELETE FROM table WHERE test1 = %(test1)s"
|
||||
fake_connected_mydb._conn.expected_params = dict(test1=1)
|
||||
fake_connected_mydb._conn.expected_params = {"test1": 1}
|
||||
fake_connected_mydb.doSQL(
|
||||
fake_connected_mydb._conn.expected_sql, fake_connected_mydb._conn.expected_params
|
||||
)
|
||||
|
@ -443,8 +454,8 @@ def test_doSQL_on_exception(fake_connected_mydb):
|
|||
|
||||
def test_doSelect(fake_connected_mydb):
|
||||
fake_connected_mydb._conn.expected_sql = "SELECT * FROM table WHERE test1 = %(test1)s"
|
||||
fake_connected_mydb._conn.expected_params = dict(test1=1)
|
||||
fake_connected_mydb._conn.expected_return = [dict(test1=1)]
|
||||
fake_connected_mydb._conn.expected_params = {"test1": 1}
|
||||
fake_connected_mydb._conn.expected_return = [{"test1": 1}]
|
||||
assert (
|
||||
fake_connected_mydb.doSelect(
|
||||
fake_connected_mydb._conn.expected_sql, fake_connected_mydb._conn.expected_params
|
||||
|
@ -455,7 +466,7 @@ def test_doSelect(fake_connected_mydb):
|
|||
|
||||
def test_doSelect_without_params(fake_connected_mydb):
|
||||
fake_connected_mydb._conn.expected_sql = "SELECT * FROM table"
|
||||
fake_connected_mydb._conn.expected_return = [dict(test1=1)]
|
||||
fake_connected_mydb._conn.expected_return = [{"test1": 1}]
|
||||
assert (
|
||||
fake_connected_mydb.doSelect(fake_connected_mydb._conn.expected_sql)
|
||||
== fake_connected_mydb._conn.expected_return
|
||||
|
@ -469,8 +480,8 @@ def test_doSelect_on_exception(fake_connected_mydb):
|
|||
|
||||
def test_doSelect_just_try(fake_connected_just_try_mydb):
|
||||
fake_connected_just_try_mydb._conn.expected_sql = "SELECT * FROM table WHERE test1 = %(test1)s"
|
||||
fake_connected_just_try_mydb._conn.expected_params = dict(test1=1)
|
||||
fake_connected_just_try_mydb._conn.expected_return = [dict(test1=1)]
|
||||
fake_connected_just_try_mydb._conn.expected_params = {"test1": 1}
|
||||
fake_connected_just_try_mydb._conn.expected_return = [{"test1": 1}]
|
||||
assert (
|
||||
fake_connected_just_try_mydb.doSelect(
|
||||
fake_connected_just_try_mydb._conn.expected_sql,
|
||||
|
|
|
@ -182,27 +182,96 @@ def test_parse_normal_opening_hours_multiple_periods():
|
|||
]
|
||||
|
||||
|
||||
#
|
||||
# Tests on is_closed
|
||||
#
|
||||
def test_parse_normal_opening_hours_is_sorted():
|
||||
assert opening_hours.parse_normal_opening_hours(
|
||||
[
|
||||
"samedi 9h30-18h",
|
||||
"lundi-vendredi 14h-18h 9h30-12h30",
|
||||
"samedi 9h30-12h",
|
||||
"dimanche 9h30-12h",
|
||||
]
|
||||
) == [
|
||||
{
|
||||
"days": ["lundi", "mardi", "mercredi", "jeudi", "vendredi"],
|
||||
"hours_periods": [
|
||||
{"start": datetime.time(9, 30), "stop": datetime.time(12, 30)},
|
||||
{"start": datetime.time(14, 0), "stop": datetime.time(18, 0)},
|
||||
],
|
||||
},
|
||||
{
|
||||
"days": ["samedi"],
|
||||
"hours_periods": [
|
||||
{"start": datetime.time(9, 30), "stop": datetime.time(12, 0)},
|
||||
],
|
||||
},
|
||||
{
|
||||
"days": ["samedi"],
|
||||
"hours_periods": [
|
||||
{"start": datetime.time(9, 30), "stop": datetime.time(18, 0)},
|
||||
],
|
||||
},
|
||||
{
|
||||
"days": ["dimanche"],
|
||||
"hours_periods": [
|
||||
{"start": datetime.time(9, 30), "stop": datetime.time(12, 0)},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
exceptional_closures = [
|
||||
"22/09/2017",
|
||||
"20/09/2017-22/09/2017",
|
||||
"20/09/2017-22/09/2017 18/09/2017",
|
||||
"25/11/2017",
|
||||
"26/11/2017 9h30-12h30",
|
||||
]
|
||||
#
|
||||
# Tests on normal opening hours
|
||||
#
|
||||
normal_opening_hours = [
|
||||
"lundi-mardi jeudi 9h30-12h30 14h-16h30",
|
||||
"mercredi vendredi 9h30-12h30 14h-17h",
|
||||
"samedi",
|
||||
]
|
||||
normally_opened_datetime = datetime.datetime(2024, 3, 1, 10, 15)
|
||||
normally_opened_all_day_datetime = datetime.datetime(2024, 4, 6, 10, 15)
|
||||
normally_closed_datetime = datetime.datetime(2017, 3, 1, 20, 15)
|
||||
normally_closed_all_day_datetime = datetime.datetime(2024, 4, 7, 20, 15)
|
||||
|
||||
|
||||
def test_its_normally_open():
|
||||
assert opening_hours.its_normally_open(normal_opening_hours, when=normally_opened_datetime)
|
||||
|
||||
|
||||
def test_its_normally_open_all_day():
|
||||
assert opening_hours.its_normally_open(
|
||||
normal_opening_hours, when=normally_opened_all_day_datetime
|
||||
)
|
||||
|
||||
|
||||
def test_its_normally_closed():
|
||||
assert not opening_hours.its_normally_open(normal_opening_hours, when=normally_closed_datetime)
|
||||
|
||||
|
||||
def test_its_normally_closed_all_day():
|
||||
assert not opening_hours.its_normally_open(
|
||||
normal_opening_hours, when=normally_closed_all_day_datetime
|
||||
)
|
||||
|
||||
|
||||
def test_its_normally_open_ignore_time():
|
||||
assert opening_hours.its_normally_open(
|
||||
normal_opening_hours, when=normally_closed_datetime.date(), ignore_time=True
|
||||
)
|
||||
|
||||
|
||||
def test_its_normally_closed_ignore_time():
|
||||
assert not opening_hours.its_normally_open(
|
||||
normal_opening_hours, when=normally_closed_all_day_datetime.date(), ignore_time=True
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Tests on non working days
|
||||
#
|
||||
nonworking_public_holidays = [
|
||||
"1janvier",
|
||||
"paques",
|
||||
"lundi_paques",
|
||||
"1mai",
|
||||
"8mai",
|
||||
"jeudi_ascension",
|
||||
"lundi_pentecote",
|
||||
|
@ -212,6 +281,120 @@ nonworking_public_holidays = [
|
|||
"11novembre",
|
||||
"noel",
|
||||
]
|
||||
nonworking_date = datetime.date(2017, 1, 1)
|
||||
not_included_nonworking_date = datetime.date(2017, 5, 1)
|
||||
not_nonworking_date = datetime.date(2017, 5, 2)
|
||||
|
||||
|
||||
def test_its_nonworking_day():
|
||||
assert (
|
||||
opening_hours.its_nonworking_day(nonworking_public_holidays, date=nonworking_date) is True
|
||||
)
|
||||
|
||||
|
||||
def test_its_not_nonworking_day():
|
||||
assert (
|
||||
opening_hours.its_nonworking_day(
|
||||
nonworking_public_holidays,
|
||||
date=not_nonworking_date,
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
def test_its_not_included_nonworking_day():
|
||||
assert (
|
||||
opening_hours.its_nonworking_day(
|
||||
nonworking_public_holidays,
|
||||
date=not_included_nonworking_date,
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Tests in exceptional closures
|
||||
#
|
||||
exceptional_closures = [
|
||||
"22/09/2017",
|
||||
"20/09/2017-22/09/2017",
|
||||
"20/09/2017-22/09/2017 18/09/2017",
|
||||
"25/11/2017",
|
||||
"26/11/2017 9h30-12h30",
|
||||
"27/11/2017 17h-18h 9h30-12h30",
|
||||
]
|
||||
exceptional_closure_all_day_date = datetime.date(2017, 9, 22)
|
||||
exceptional_closure_all_day_datetime = datetime.datetime.combine(
|
||||
exceptional_closure_all_day_date, datetime.time(20, 15)
|
||||
)
|
||||
exceptional_closure_datetime = datetime.datetime(2017, 11, 26, 10, 30)
|
||||
exceptional_closure_datetime_hours_period = {
|
||||
"start": datetime.time(9, 30),
|
||||
"stop": datetime.time(12, 30),
|
||||
}
|
||||
not_exceptional_closure_date = datetime.date(2019, 9, 22)
|
||||
|
||||
|
||||
def test_its_exceptionally_closed():
|
||||
assert (
|
||||
opening_hours.its_exceptionally_closed(
|
||||
exceptional_closures, when=exceptional_closure_all_day_datetime
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
def test_its_not_exceptionally_closed():
|
||||
assert (
|
||||
opening_hours.its_exceptionally_closed(
|
||||
exceptional_closures, when=not_exceptional_closure_date
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
def test_its_exceptionally_closed_all_day():
|
||||
assert (
|
||||
opening_hours.its_exceptionally_closed(
|
||||
exceptional_closures, when=exceptional_closure_all_day_datetime, all_day=True
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
def test_its_not_exceptionally_closed_all_day():
|
||||
assert (
|
||||
opening_hours.its_exceptionally_closed(
|
||||
exceptional_closures, when=exceptional_closure_datetime, all_day=True
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
def test_get_exceptional_closures_hours():
|
||||
assert opening_hours.get_exceptional_closures_hours(
|
||||
exceptional_closures, date=exceptional_closure_datetime.date()
|
||||
) == [exceptional_closure_datetime_hours_period]
|
||||
|
||||
|
||||
def test_get_exceptional_closures_hours_all_day():
|
||||
assert opening_hours.get_exceptional_closures_hours(
|
||||
exceptional_closures, date=exceptional_closure_all_day_date
|
||||
) == [{"start": datetime.datetime.min.time(), "stop": datetime.datetime.max.time()}]
|
||||
|
||||
|
||||
def test_get_exceptional_closures_hours_is_sorted():
|
||||
assert opening_hours.get_exceptional_closures_hours(
|
||||
["27/11/2017 17h-18h 9h30-12h30"], date=datetime.date(2017, 11, 27)
|
||||
) == [
|
||||
{"start": datetime.time(9, 30), "stop": datetime.time(12, 30)},
|
||||
{"start": datetime.time(17, 0), "stop": datetime.time(18, 0)},
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Tests on is_closed
|
||||
#
|
||||
|
||||
|
||||
def test_is_closed_when_normaly_closed_by_hour():
|
||||
|
@ -255,7 +438,7 @@ def test_is_closed_when_normaly_closed_by_day():
|
|||
normal_opening_hours_values=normal_opening_hours,
|
||||
exceptional_closures_values=exceptional_closures,
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2017, 5, 6, 14, 15),
|
||||
when=datetime.datetime(2017, 5, 7, 14, 15),
|
||||
) == {"closed": True, "exceptional_closure": False, "exceptional_closure_all_day": False}
|
||||
|
||||
|
||||
|
@ -300,3 +483,203 @@ def test_nonworking_french_public_days_of_the_year():
|
|||
"noel": datetime.date(2021, 12, 25),
|
||||
"saint_etienne": datetime.date(2021, 12, 26),
|
||||
}
|
||||
|
||||
|
||||
def test_next_opening_date():
|
||||
assert opening_hours.next_opening_date(
|
||||
normal_opening_hours_values=normal_opening_hours,
|
||||
exceptional_closures_values=exceptional_closures,
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
date=datetime.date(2021, 4, 4),
|
||||
) == datetime.date(2021, 4, 6)
|
||||
|
||||
|
||||
def test_next_opening_hour():
|
||||
assert opening_hours.next_opening_hour(
|
||||
normal_opening_hours_values=normal_opening_hours,
|
||||
exceptional_closures_values=exceptional_closures,
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2021, 4, 4, 10, 30),
|
||||
) == datetime.datetime(2021, 4, 6, 9, 30)
|
||||
|
||||
|
||||
def test_next_opening_hour_with_exceptionnal_closure_hours():
|
||||
assert opening_hours.next_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=["06/04/2021 9h-13h 14h-16h"],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2021, 4, 4, 10, 30),
|
||||
) == datetime.datetime(2021, 4, 6, 16, 0)
|
||||
|
||||
|
||||
def test_next_opening_hour_with_exceptionnal_closure_day():
|
||||
assert opening_hours.next_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=["06/04/2021"],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2021, 4, 4, 10, 30),
|
||||
) == datetime.datetime(2021, 4, 7, 9, 0)
|
||||
|
||||
|
||||
def test_next_opening_hour_with_overlapsed_opening_hours():
|
||||
assert opening_hours.next_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h", "mardi 8h-19h"],
|
||||
exceptional_closures_values=["06/04/2021 9h-13h 14h-16h"],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2021, 4, 4, 10, 30),
|
||||
) == datetime.datetime(2021, 4, 6, 8, 0)
|
||||
|
||||
|
||||
def test_next_opening_hour_with_too_large_exceptionnal_closure_days():
|
||||
assert (
|
||||
opening_hours.next_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=["06/04/2021-16-04/2021"],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2021, 4, 4, 10, 30),
|
||||
max_anaylse_days=10,
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
def test_next_opening_hour_on_opened_moment():
|
||||
assert opening_hours.next_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2021, 4, 6, 10, 30),
|
||||
) == datetime.datetime(2021, 4, 6, 10, 30)
|
||||
|
||||
|
||||
def test_next_opening_hour_on_same_day():
|
||||
assert opening_hours.next_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2021, 4, 6, 13, 0),
|
||||
) == datetime.datetime(2021, 4, 6, 14, 0)
|
||||
assert opening_hours.next_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2021, 4, 6, 16, 0),
|
||||
) == datetime.datetime(2021, 4, 6, 16, 0)
|
||||
assert opening_hours.next_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2021, 4, 6, 16, 0),
|
||||
) == datetime.datetime(2021, 4, 6, 16, 0)
|
||||
|
||||
|
||||
def test_next_opening_hour_on_opened_day_but_too_late():
|
||||
assert opening_hours.next_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2021, 4, 6, 23, 0),
|
||||
) == datetime.datetime(2021, 4, 7, 9, 0)
|
||||
|
||||
|
||||
def test_previous_opening_date():
|
||||
assert opening_hours.previous_opening_date(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-18h"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
date=datetime.date(2024, 4, 1),
|
||||
) == datetime.date(2024, 3, 29)
|
||||
|
||||
|
||||
def test_previous_opening_hour():
|
||||
assert opening_hours.previous_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-18h"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2024, 4, 1, 10, 30),
|
||||
) == datetime.datetime(2024, 3, 29, 18, 0)
|
||||
|
||||
|
||||
def test_previous_opening_hour_with_exceptionnal_closure_hours():
|
||||
assert opening_hours.previous_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=["29/03/2024 14h-18h"],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2024, 4, 1, 10, 30),
|
||||
) == datetime.datetime(2024, 3, 29, 12, 0)
|
||||
assert opening_hours.previous_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=["29/03/2024 16h-18h"],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2024, 4, 1, 10, 30),
|
||||
) == datetime.datetime(2024, 3, 29, 16, 0)
|
||||
|
||||
|
||||
def test_previous_opening_hour_with_exceptionnal_closure_day():
|
||||
assert opening_hours.previous_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=["29/03/2024"],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2024, 4, 1, 10, 30),
|
||||
) == datetime.datetime(2024, 3, 28, 18, 0)
|
||||
|
||||
|
||||
def test_previous_opening_hour_with_overlapsed_opening_hours():
|
||||
assert opening_hours.previous_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h", "mardi 8h-19h"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2024, 4, 3, 8, 30),
|
||||
) == datetime.datetime(2024, 4, 2, 19, 0)
|
||||
|
||||
|
||||
def test_previous_opening_hour_with_too_large_exceptionnal_closure_days():
|
||||
assert (
|
||||
opening_hours.previous_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=["06/03/2024-16-04/2024"],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2024, 4, 17, 8, 30),
|
||||
max_anaylse_days=10,
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
def test_previous_opening_hour_on_opened_moment():
|
||||
assert opening_hours.previous_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2024, 4, 5, 10, 30),
|
||||
) == datetime.datetime(2024, 4, 5, 10, 30)
|
||||
|
||||
|
||||
def test_previous_opening_hour_on_same_day():
|
||||
assert opening_hours.previous_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2024, 4, 5, 13, 0),
|
||||
) == datetime.datetime(2024, 4, 5, 12, 0)
|
||||
assert opening_hours.previous_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2024, 4, 5, 16, 0),
|
||||
) == datetime.datetime(2024, 4, 5, 16, 0)
|
||||
assert opening_hours.previous_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2024, 4, 5, 16, 0),
|
||||
) == datetime.datetime(2024, 4, 5, 16, 0)
|
||||
|
||||
|
||||
def test_previous_opening_hour_on_opened_day_but_too_early():
|
||||
assert opening_hours.previous_opening_hour(
|
||||
normal_opening_hours_values=["lundi-vendredi 9h-12h 14h-18h"],
|
||||
exceptional_closures_values=[],
|
||||
nonworking_public_holidays_values=nonworking_public_holidays,
|
||||
when=datetime.datetime(2024, 4, 5, 8, 0),
|
||||
) == datetime.datetime(2024, 4, 4, 18, 0)
|
||||
|
|
|
@ -73,7 +73,7 @@ class FakeCXOracle:
|
|||
just_try = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
allowed_kwargs = dict(dsn=str, user=str, password=(str, None))
|
||||
allowed_kwargs = {"dsn": str, "user": str, "password": (str, None)}
|
||||
for arg, value in kwargs.items():
|
||||
assert arg in allowed_kwargs, f"Invalid arg {arg}='{value}'"
|
||||
assert isinstance(
|
||||
|
@ -197,21 +197,23 @@ mock_doSelect_just_try = mock_doSQL_just_try
|
|||
|
||||
|
||||
def test_combine_params_with_to_add_parameter():
|
||||
assert OracleDB._combine_params(dict(test1=1), dict(test2=2)) == dict(test1=1, test2=2)
|
||||
assert OracleDB._combine_params({"test1": 1}, {"test2": 2}) == {"test1": 1, "test2": 2}
|
||||
|
||||
|
||||
def test_combine_params_with_kargs():
|
||||
assert OracleDB._combine_params(dict(test1=1), test2=2) == dict(test1=1, test2=2)
|
||||
assert OracleDB._combine_params({"test1": 1}, test2=2) == {"test1": 1, "test2": 2}
|
||||
|
||||
|
||||
def test_combine_params_with_kargs_and_to_add_parameter():
|
||||
assert OracleDB._combine_params(dict(test1=1), dict(test2=2), test3=3) == dict(
|
||||
test1=1, test2=2, test3=3
|
||||
)
|
||||
assert OracleDB._combine_params({"test1": 1}, {"test2": 2}, test3=3) == {
|
||||
"test1": 1,
|
||||
"test2": 2,
|
||||
"test3": 3,
|
||||
}
|
||||
|
||||
|
||||
def test_format_where_clauses_params_are_preserved():
|
||||
args = ("test = test", dict(test1=1))
|
||||
args = ("test = test", {"test1": 1})
|
||||
assert OracleDB._format_where_clauses(*args) == args
|
||||
|
||||
|
||||
|
@ -220,12 +222,12 @@ def test_format_where_clauses_raw():
|
|||
|
||||
|
||||
def test_format_where_clauses_tuple_clause_with_params():
|
||||
where_clauses = ("test1 = :test1 AND test2 = :test2", dict(test1=1, test2=2))
|
||||
where_clauses = ("test1 = :test1 AND test2 = :test2", {"test1": 1, "test2": 2})
|
||||
assert OracleDB._format_where_clauses(where_clauses) == where_clauses
|
||||
|
||||
|
||||
def test_format_where_clauses_dict():
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
assert OracleDB._format_where_clauses(where_clauses) == (
|
||||
'"test1" = :test1 AND "test2" = :test2',
|
||||
where_clauses,
|
||||
|
@ -233,15 +235,15 @@ def test_format_where_clauses_dict():
|
|||
|
||||
|
||||
def test_format_where_clauses_combined_types():
|
||||
where_clauses = ("test1 = 1", ("test2 LIKE :test2", dict(test2=2)), dict(test3=3, test4=4))
|
||||
where_clauses = ("test1 = 1", ("test2 LIKE :test2", {"test2": 2}), {"test3": 3, "test4": 4})
|
||||
assert OracleDB._format_where_clauses(where_clauses) == (
|
||||
'test1 = 1 AND test2 LIKE :test2 AND "test3" = :test3 AND "test4" = :test4',
|
||||
dict(test2=2, test3=3, test4=4),
|
||||
{"test2": 2, "test3": 3, "test4": 4},
|
||||
)
|
||||
|
||||
|
||||
def test_format_where_clauses_with_where_op():
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
assert OracleDB._format_where_clauses(where_clauses, where_op="OR") == (
|
||||
'"test1" = :test1 OR "test2" = :test2',
|
||||
where_clauses,
|
||||
|
@ -250,7 +252,7 @@ def test_format_where_clauses_with_where_op():
|
|||
|
||||
def test_add_where_clauses():
|
||||
sql = "SELECT * FROM table"
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
assert OracleDB._add_where_clauses(sql, None, where_clauses) == (
|
||||
sql + ' WHERE "test1" = :test1 AND "test2" = :test2',
|
||||
where_clauses,
|
||||
|
@ -259,11 +261,11 @@ def test_add_where_clauses():
|
|||
|
||||
def test_add_where_clauses_preserved_params():
|
||||
sql = "SELECT * FROM table"
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
params = dict(fake1=1)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
params = {"fake1": 1}
|
||||
assert OracleDB._add_where_clauses(sql, params.copy(), where_clauses) == (
|
||||
sql + ' WHERE "test1" = :test1 AND "test2" = :test2',
|
||||
dict(**where_clauses, **params),
|
||||
{**where_clauses, **params},
|
||||
)
|
||||
|
||||
|
||||
|
@ -278,11 +280,11 @@ def test_add_where_clauses_with_op():
|
|||
|
||||
def test_add_where_clauses_with_duplicated_field():
|
||||
sql = "UPDATE table SET test1=:test1"
|
||||
params = dict(test1="new_value")
|
||||
where_clauses = dict(test1="where_value")
|
||||
params = {"test1": "new_value"}
|
||||
where_clauses = {"test1": "where_value"}
|
||||
assert OracleDB._add_where_clauses(sql, params, where_clauses) == (
|
||||
sql + ' WHERE "test1" = :test1_1',
|
||||
dict(test1="new_value", test1_1="where_value"),
|
||||
{"test1": "new_value", "test1_1": "where_value"},
|
||||
)
|
||||
|
||||
|
||||
|
@ -292,7 +294,7 @@ def test_quote_table_name():
|
|||
|
||||
|
||||
def test_insert(mocker, test_oracledb):
|
||||
values = dict(test1=1, test2=2)
|
||||
values = {"test1": 1, "test2": 2}
|
||||
mocker.patch(
|
||||
"mylib.oracle.OracleDB.doSQL",
|
||||
generate_mock_doSQL(
|
||||
|
@ -305,18 +307,18 @@ def test_insert(mocker, test_oracledb):
|
|||
|
||||
def test_insert_just_try(mocker, test_oracledb):
|
||||
mocker.patch("mylib.oracle.OracleDB.doSQL", mock_doSQL_just_try)
|
||||
assert test_oracledb.insert("mytable", dict(test1=1, test2=2), just_try=True)
|
||||
assert test_oracledb.insert("mytable", {"test1": 1, "test2": 2}, just_try=True)
|
||||
|
||||
|
||||
def test_update(mocker, test_oracledb):
|
||||
values = dict(test1=1, test2=2)
|
||||
where_clauses = dict(test3=3, test4=4)
|
||||
values = {"test1": 1, "test2": 2}
|
||||
where_clauses = {"test3": 3, "test4": 4}
|
||||
mocker.patch(
|
||||
"mylib.oracle.OracleDB.doSQL",
|
||||
generate_mock_doSQL(
|
||||
'UPDATE "mytable" SET "test1" = :test1, "test2" = :test2 WHERE "test3" = :test3 AND'
|
||||
' "test4" = :test4',
|
||||
dict(**values, **where_clauses),
|
||||
{**values, **where_clauses},
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -325,11 +327,11 @@ def test_update(mocker, test_oracledb):
|
|||
|
||||
def test_update_just_try(mocker, test_oracledb):
|
||||
mocker.patch("mylib.oracle.OracleDB.doSQL", mock_doSQL_just_try)
|
||||
assert test_oracledb.update("mytable", dict(test1=1, test2=2), None, just_try=True)
|
||||
assert test_oracledb.update("mytable", {"test1": 1, "test2": 2}, None, just_try=True)
|
||||
|
||||
|
||||
def test_delete(mocker, test_oracledb):
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
mocker.patch(
|
||||
"mylib.oracle.OracleDB.doSQL",
|
||||
generate_mock_doSQL(
|
||||
|
@ -360,24 +362,26 @@ def test_truncate_just_try(mocker, test_oracledb):
|
|||
|
||||
def test_select(mocker, test_oracledb):
|
||||
fields = ("field1", "field2")
|
||||
where_clauses = dict(test3=3, test4=4)
|
||||
where_clauses = {"test3": 3, "test4": 4}
|
||||
expected_return = [
|
||||
dict(field1=1, field2=2),
|
||||
dict(field1=2, field2=3),
|
||||
{"field1": 1, "field2": 2},
|
||||
{"field1": 2, "field2": 3},
|
||||
]
|
||||
order_by = "field1, DESC"
|
||||
limit = 10
|
||||
mocker.patch(
|
||||
"mylib.oracle.OracleDB.doSelect",
|
||||
generate_mock_doSQL(
|
||||
'SELECT "field1", "field2" FROM "mytable" WHERE "test3" = :test3 AND "test4" = :test4'
|
||||
" ORDER BY " + order_by,
|
||||
" ORDER BY " + order_by + " LIMIT " + str(limit), # nosec: B608
|
||||
where_clauses,
|
||||
expected_return,
|
||||
),
|
||||
)
|
||||
|
||||
assert (
|
||||
test_oracledb.select("mytable", where_clauses, fields, order_by=order_by) == expected_return
|
||||
test_oracledb.select("mytable", where_clauses, fields, order_by=order_by, limit=limit)
|
||||
== expected_return
|
||||
)
|
||||
|
||||
|
||||
|
@ -398,9 +402,11 @@ def test_select_just_try(mocker, test_oracledb):
|
|||
|
||||
|
||||
def test_connect(mocker, test_oracledb):
|
||||
expected_kwargs = dict(
|
||||
dsn=test_oracledb._dsn, user=test_oracledb._user, password=test_oracledb._pwd
|
||||
)
|
||||
expected_kwargs = {
|
||||
"dsn": test_oracledb._dsn,
|
||||
"user": test_oracledb._user,
|
||||
"password": test_oracledb._pwd,
|
||||
}
|
||||
|
||||
mocker.patch("cx_Oracle.connect", generate_mock_args(expected_kwargs=expected_kwargs))
|
||||
|
||||
|
@ -417,7 +423,7 @@ def test_close_connected(fake_connected_oracledb):
|
|||
|
||||
def test_doSQL(fake_connected_oracledb):
|
||||
fake_connected_oracledb._conn.expected_sql = "DELETE FROM table WHERE test1 = :test1"
|
||||
fake_connected_oracledb._conn.expected_params = dict(test1=1)
|
||||
fake_connected_oracledb._conn.expected_params = {"test1": 1}
|
||||
fake_connected_oracledb.doSQL(
|
||||
fake_connected_oracledb._conn.expected_sql, fake_connected_oracledb._conn.expected_params
|
||||
)
|
||||
|
@ -439,8 +445,8 @@ def test_doSQL_on_exception(fake_connected_oracledb):
|
|||
|
||||
def test_doSelect(fake_connected_oracledb):
|
||||
fake_connected_oracledb._conn.expected_sql = "SELECT * FROM table WHERE test1 = :test1"
|
||||
fake_connected_oracledb._conn.expected_params = dict(test1=1)
|
||||
fake_connected_oracledb._conn.expected_return = [dict(test1=1)]
|
||||
fake_connected_oracledb._conn.expected_params = {"test1": 1}
|
||||
fake_connected_oracledb._conn.expected_return = [{"test1": 1}]
|
||||
assert (
|
||||
fake_connected_oracledb.doSelect(
|
||||
fake_connected_oracledb._conn.expected_sql,
|
||||
|
@ -452,7 +458,7 @@ def test_doSelect(fake_connected_oracledb):
|
|||
|
||||
def test_doSelect_without_params(fake_connected_oracledb):
|
||||
fake_connected_oracledb._conn.expected_sql = "SELECT * FROM table"
|
||||
fake_connected_oracledb._conn.expected_return = [dict(test1=1)]
|
||||
fake_connected_oracledb._conn.expected_return = [{"test1": 1}]
|
||||
assert (
|
||||
fake_connected_oracledb.doSelect(fake_connected_oracledb._conn.expected_sql)
|
||||
== fake_connected_oracledb._conn.expected_return
|
||||
|
@ -466,8 +472,8 @@ def test_doSelect_on_exception(fake_connected_oracledb):
|
|||
|
||||
def test_doSelect_just_try(fake_connected_just_try_oracledb):
|
||||
fake_connected_just_try_oracledb._conn.expected_sql = "SELECT * FROM table WHERE test1 = :test1"
|
||||
fake_connected_just_try_oracledb._conn.expected_params = dict(test1=1)
|
||||
fake_connected_just_try_oracledb._conn.expected_return = [dict(test1=1)]
|
||||
fake_connected_just_try_oracledb._conn.expected_params = {"test1": 1}
|
||||
fake_connected_just_try_oracledb._conn.expected_return = [{"test1": 1}]
|
||||
assert (
|
||||
fake_connected_just_try_oracledb.doSelect(
|
||||
fake_connected_just_try_oracledb._conn.expected_sql,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import psycopg2
|
||||
import pytest
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
from mylib.pgsql import PgDB
|
||||
|
||||
|
@ -57,13 +58,14 @@ class FakePsycopg2:
|
|||
|
||||
expected_sql = None
|
||||
expected_params = None
|
||||
expected_cursor_factory = None
|
||||
expected_return = True
|
||||
expected_just_try = False
|
||||
expected_exception = False
|
||||
just_try = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
allowed_kwargs = dict(dbname=str, user=str, password=(str, None), host=str)
|
||||
allowed_kwargs = {"dbname": str, "user": str, "password": (str, None), "host": str}
|
||||
for arg, value in kwargs.items():
|
||||
assert arg in allowed_kwargs, f'Invalid arg {arg}="{value}"'
|
||||
assert isinstance(
|
||||
|
@ -81,7 +83,8 @@ class FakePsycopg2:
|
|||
raise psycopg2.Error(f"set_client_encoding({arg[0]}): Expected exception")
|
||||
return self.expected_return
|
||||
|
||||
def cursor(self):
|
||||
def cursor(self, cursor_factory=None):
|
||||
assert cursor_factory is self.expected_cursor_factory
|
||||
return FakePsycopg2Cursor(
|
||||
self.expected_sql,
|
||||
self.expected_params,
|
||||
|
@ -194,21 +197,23 @@ mock_doSelect_just_try = mock_doSQL_just_try
|
|||
|
||||
|
||||
def test_combine_params_with_to_add_parameter():
|
||||
assert PgDB._combine_params(dict(test1=1), dict(test2=2)) == dict(test1=1, test2=2)
|
||||
assert PgDB._combine_params({"test1": 1}, {"test2": 2}) == {"test1": 1, "test2": 2}
|
||||
|
||||
|
||||
def test_combine_params_with_kargs():
|
||||
assert PgDB._combine_params(dict(test1=1), test2=2) == dict(test1=1, test2=2)
|
||||
assert PgDB._combine_params({"test1": 1}, test2=2) == {"test1": 1, "test2": 2}
|
||||
|
||||
|
||||
def test_combine_params_with_kargs_and_to_add_parameter():
|
||||
assert PgDB._combine_params(dict(test1=1), dict(test2=2), test3=3) == dict(
|
||||
test1=1, test2=2, test3=3
|
||||
)
|
||||
assert PgDB._combine_params({"test1": 1}, {"test2": 2}, test3=3) == {
|
||||
"test1": 1,
|
||||
"test2": 2,
|
||||
"test3": 3,
|
||||
}
|
||||
|
||||
|
||||
def test_format_where_clauses_params_are_preserved():
|
||||
args = ("test = test", dict(test1=1))
|
||||
args = ("test = test", {"test1": 1})
|
||||
assert PgDB._format_where_clauses(*args) == args
|
||||
|
||||
|
||||
|
@ -217,12 +222,12 @@ def test_format_where_clauses_raw():
|
|||
|
||||
|
||||
def test_format_where_clauses_tuple_clause_with_params():
|
||||
where_clauses = ("test1 = %(test1)s AND test2 = %(test2)s", dict(test1=1, test2=2))
|
||||
where_clauses = ("test1 = %(test1)s AND test2 = %(test2)s", {"test1": 1, "test2": 2})
|
||||
assert PgDB._format_where_clauses(where_clauses) == where_clauses
|
||||
|
||||
|
||||
def test_format_where_clauses_dict():
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
assert PgDB._format_where_clauses(where_clauses) == (
|
||||
'"test1" = %(test1)s AND "test2" = %(test2)s',
|
||||
where_clauses,
|
||||
|
@ -230,15 +235,15 @@ def test_format_where_clauses_dict():
|
|||
|
||||
|
||||
def test_format_where_clauses_combined_types():
|
||||
where_clauses = ("test1 = 1", ("test2 LIKE %(test2)s", dict(test2=2)), dict(test3=3, test4=4))
|
||||
where_clauses = ("test1 = 1", ("test2 LIKE %(test2)s", {"test2": 2}), {"test3": 3, "test4": 4})
|
||||
assert PgDB._format_where_clauses(where_clauses) == (
|
||||
'test1 = 1 AND test2 LIKE %(test2)s AND "test3" = %(test3)s AND "test4" = %(test4)s',
|
||||
dict(test2=2, test3=3, test4=4),
|
||||
{"test2": 2, "test3": 3, "test4": 4},
|
||||
)
|
||||
|
||||
|
||||
def test_format_where_clauses_with_where_op():
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
assert PgDB._format_where_clauses(where_clauses, where_op="OR") == (
|
||||
'"test1" = %(test1)s OR "test2" = %(test2)s',
|
||||
where_clauses,
|
||||
|
@ -247,7 +252,7 @@ def test_format_where_clauses_with_where_op():
|
|||
|
||||
def test_add_where_clauses():
|
||||
sql = "SELECT * FROM table"
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
assert PgDB._add_where_clauses(sql, None, where_clauses) == (
|
||||
sql + ' WHERE "test1" = %(test1)s AND "test2" = %(test2)s',
|
||||
where_clauses,
|
||||
|
@ -256,11 +261,11 @@ def test_add_where_clauses():
|
|||
|
||||
def test_add_where_clauses_preserved_params():
|
||||
sql = "SELECT * FROM table"
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
params = dict(fake1=1)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
params = {"fake1": 1}
|
||||
assert PgDB._add_where_clauses(sql, params.copy(), where_clauses) == (
|
||||
sql + ' WHERE "test1" = %(test1)s AND "test2" = %(test2)s',
|
||||
dict(**where_clauses, **params),
|
||||
{**where_clauses, **params},
|
||||
)
|
||||
|
||||
|
||||
|
@ -275,11 +280,11 @@ def test_add_where_clauses_with_op():
|
|||
|
||||
def test_add_where_clauses_with_duplicated_field():
|
||||
sql = "UPDATE table SET test1=%(test1)s"
|
||||
params = dict(test1="new_value")
|
||||
where_clauses = dict(test1="where_value")
|
||||
params = {"test1": "new_value"}
|
||||
where_clauses = {"test1": "where_value"}
|
||||
assert PgDB._add_where_clauses(sql, params, where_clauses) == (
|
||||
sql + ' WHERE "test1" = %(test1_1)s',
|
||||
dict(test1="new_value", test1_1="where_value"),
|
||||
{"test1": "new_value", "test1_1": "where_value"},
|
||||
)
|
||||
|
||||
|
||||
|
@ -289,7 +294,7 @@ def test_quote_table_name():
|
|||
|
||||
|
||||
def test_insert(mocker, test_pgdb):
|
||||
values = dict(test1=1, test2=2)
|
||||
values = {"test1": 1, "test2": 2}
|
||||
mocker.patch(
|
||||
"mylib.pgsql.PgDB.doSQL",
|
||||
generate_mock_doSQL(
|
||||
|
@ -302,18 +307,18 @@ def test_insert(mocker, test_pgdb):
|
|||
|
||||
def test_insert_just_try(mocker, test_pgdb):
|
||||
mocker.patch("mylib.pgsql.PgDB.doSQL", mock_doSQL_just_try)
|
||||
assert test_pgdb.insert("mytable", dict(test1=1, test2=2), just_try=True)
|
||||
assert test_pgdb.insert("mytable", {"test1": 1, "test2": 2}, just_try=True)
|
||||
|
||||
|
||||
def test_update(mocker, test_pgdb):
|
||||
values = dict(test1=1, test2=2)
|
||||
where_clauses = dict(test3=3, test4=4)
|
||||
values = {"test1": 1, "test2": 2}
|
||||
where_clauses = {"test3": 3, "test4": 4}
|
||||
mocker.patch(
|
||||
"mylib.pgsql.PgDB.doSQL",
|
||||
generate_mock_doSQL(
|
||||
'UPDATE "mytable" SET "test1" = %(test1)s, "test2" = %(test2)s WHERE "test3" ='
|
||||
' %(test3)s AND "test4" = %(test4)s',
|
||||
dict(**values, **where_clauses),
|
||||
{**values, **where_clauses},
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -322,11 +327,11 @@ def test_update(mocker, test_pgdb):
|
|||
|
||||
def test_update_just_try(mocker, test_pgdb):
|
||||
mocker.patch("mylib.pgsql.PgDB.doSQL", mock_doSQL_just_try)
|
||||
assert test_pgdb.update("mytable", dict(test1=1, test2=2), None, just_try=True)
|
||||
assert test_pgdb.update("mytable", {"test1": 1, "test2": 2}, None, just_try=True)
|
||||
|
||||
|
||||
def test_delete(mocker, test_pgdb):
|
||||
where_clauses = dict(test1=1, test2=2)
|
||||
where_clauses = {"test1": 1, "test2": 2}
|
||||
mocker.patch(
|
||||
"mylib.pgsql.PgDB.doSQL",
|
||||
generate_mock_doSQL(
|
||||
|
@ -355,23 +360,27 @@ def test_truncate_just_try(mocker, test_pgdb):
|
|||
|
||||
def test_select(mocker, test_pgdb):
|
||||
fields = ("field1", "field2")
|
||||
where_clauses = dict(test3=3, test4=4)
|
||||
where_clauses = {"test3": 3, "test4": 4}
|
||||
expected_return = [
|
||||
dict(field1=1, field2=2),
|
||||
dict(field1=2, field2=3),
|
||||
{"field1": 1, "field2": 2},
|
||||
{"field1": 2, "field2": 3},
|
||||
]
|
||||
order_by = "field1, DESC"
|
||||
limit = 10
|
||||
mocker.patch(
|
||||
"mylib.pgsql.PgDB.doSelect",
|
||||
generate_mock_doSQL(
|
||||
'SELECT "field1", "field2" FROM "mytable" WHERE "test3" = %(test3)s AND "test4" ='
|
||||
" %(test4)s ORDER BY " + order_by,
|
||||
" %(test4)s ORDER BY " + order_by + " LIMIT " + str(limit), # nosec: B608
|
||||
where_clauses,
|
||||
expected_return,
|
||||
),
|
||||
)
|
||||
|
||||
assert test_pgdb.select("mytable", where_clauses, fields, order_by=order_by) == expected_return
|
||||
assert (
|
||||
test_pgdb.select("mytable", where_clauses, fields, order_by=order_by, limit=limit)
|
||||
== expected_return
|
||||
)
|
||||
|
||||
|
||||
def test_select_without_field_and_order_by(mocker, test_pgdb):
|
||||
|
@ -391,9 +400,12 @@ def test_select_just_try(mocker, test_pgdb):
|
|||
|
||||
|
||||
def test_connect(mocker, test_pgdb):
|
||||
expected_kwargs = dict(
|
||||
dbname=test_pgdb._db, user=test_pgdb._user, host=test_pgdb._host, password=test_pgdb._pwd
|
||||
)
|
||||
expected_kwargs = {
|
||||
"dbname": test_pgdb._db,
|
||||
"user": test_pgdb._user,
|
||||
"host": test_pgdb._host,
|
||||
"password": test_pgdb._pwd,
|
||||
}
|
||||
|
||||
mocker.patch("psycopg2.connect", generate_mock_args(expected_kwargs=expected_kwargs))
|
||||
|
||||
|
@ -423,7 +435,7 @@ def test_setEncoding_on_exception(fake_connected_pgdb):
|
|||
|
||||
def test_doSQL(fake_connected_pgdb):
|
||||
fake_connected_pgdb._conn.expected_sql = "DELETE FROM table WHERE test1 = %(test1)s"
|
||||
fake_connected_pgdb._conn.expected_params = dict(test1=1)
|
||||
fake_connected_pgdb._conn.expected_params = {"test1": 1}
|
||||
fake_connected_pgdb.doSQL(
|
||||
fake_connected_pgdb._conn.expected_sql, fake_connected_pgdb._conn.expected_params
|
||||
)
|
||||
|
@ -445,8 +457,9 @@ def test_doSQL_on_exception(fake_connected_pgdb):
|
|||
|
||||
def test_doSelect(fake_connected_pgdb):
|
||||
fake_connected_pgdb._conn.expected_sql = "SELECT * FROM table WHERE test1 = %(test1)s"
|
||||
fake_connected_pgdb._conn.expected_params = dict(test1=1)
|
||||
fake_connected_pgdb._conn.expected_return = [dict(test1=1)]
|
||||
fake_connected_pgdb._conn.expected_params = {"test1": 1}
|
||||
fake_connected_pgdb._conn.expected_cursor_factory = RealDictCursor
|
||||
fake_connected_pgdb._conn.expected_return = [{"test1": 1}]
|
||||
assert (
|
||||
fake_connected_pgdb.doSelect(
|
||||
fake_connected_pgdb._conn.expected_sql, fake_connected_pgdb._conn.expected_params
|
||||
|
@ -457,7 +470,8 @@ def test_doSelect(fake_connected_pgdb):
|
|||
|
||||
def test_doSelect_without_params(fake_connected_pgdb):
|
||||
fake_connected_pgdb._conn.expected_sql = "SELECT * FROM table"
|
||||
fake_connected_pgdb._conn.expected_return = [dict(test1=1)]
|
||||
fake_connected_pgdb._conn.expected_cursor_factory = RealDictCursor
|
||||
fake_connected_pgdb._conn.expected_return = [{"test1": 1}]
|
||||
assert (
|
||||
fake_connected_pgdb.doSelect(fake_connected_pgdb._conn.expected_sql)
|
||||
== fake_connected_pgdb._conn.expected_return
|
||||
|
@ -465,14 +479,16 @@ def test_doSelect_without_params(fake_connected_pgdb):
|
|||
|
||||
|
||||
def test_doSelect_on_exception(fake_connected_pgdb):
|
||||
fake_connected_pgdb._conn.expected_cursor_factory = RealDictCursor
|
||||
fake_connected_pgdb._conn.expected_exception = True
|
||||
assert fake_connected_pgdb.doSelect("SELECT * FROM table") is False
|
||||
|
||||
|
||||
def test_doSelect_just_try(fake_connected_just_try_pgdb):
|
||||
fake_connected_just_try_pgdb._conn.expected_sql = "SELECT * FROM table WHERE test1 = %(test1)s"
|
||||
fake_connected_just_try_pgdb._conn.expected_params = dict(test1=1)
|
||||
fake_connected_just_try_pgdb._conn.expected_return = [dict(test1=1)]
|
||||
fake_connected_just_try_pgdb._conn.expected_params = {"test1": 1}
|
||||
fake_connected_just_try_pgdb._conn.expected_cursor_factory = RealDictCursor
|
||||
fake_connected_just_try_pgdb._conn.expected_return = [{"test1": 1}]
|
||||
assert (
|
||||
fake_connected_just_try_pgdb.doSelect(
|
||||
fake_connected_just_try_pgdb._conn.expected_sql,
|
||||
|
|
|
@ -25,12 +25,12 @@ def test_create_telltale_file(tmp_path):
|
|||
|
||||
def test_create_telltale_file_with_filepath_and_invalid_dirpath():
|
||||
with pytest.raises(AssertionError):
|
||||
TelltaleFile(filepath="/tmp/test", dirpath="/var/tmp")
|
||||
TelltaleFile(filepath="/tmp/test", dirpath="/var/tmp") # nosec: B108
|
||||
|
||||
|
||||
def test_create_telltale_file_with_filepath_and_invalid_filename():
|
||||
with pytest.raises(AssertionError):
|
||||
TelltaleFile(filepath="/tmp/test", filename="other")
|
||||
TelltaleFile(filepath="/tmp/test", filename="other") # nosec: B108
|
||||
|
||||
|
||||
def test_remove_telltale_file(tmp_path):
|
||||
|
|
Loading…
Reference in a new issue