Compare commits

...

33 Commits

Author SHA1 Message Date
Giacomo Leidi 584186e831
Update system dependencies. 2024-03-20 00:41:57 +01:00
Giacomo Leidi 9763ca5fbf
Update system dependencies. 2024-03-14 12:39:16 +01:00
Giacomo Leidi 904aa06629
Fix docker image. 2024-03-02 00:12:05 +01:00
Giacomo Leidi be715d201c
Update dependencies. 2024-03-01 23:38:11 +01:00
Giacomo Leidi 647925acd3
Update dependencies 2024-02-28 23:45:20 +01:00
Giacomo Leidi e5100d499e
Update channels-lock.scm 2024-02-18 14:00:38 +01:00
Giacomo Leidi 9ac1d55d02
Update channels-lock.scm 2024-02-01 20:28:14 +01:00
Giacomo Leidi bf1c18f347
update guix config 2024-01-28 21:50:00 +01:00
Giacomo Leidi e381c1b522
Migrate to importlib and update some dependencies (#189)
* Migrate to importlib.

* Update CI
2024-01-28 21:08:17 +01:00
Giacomo Leidi 77a881980b
Update README.md. 2024-01-07 22:44:11 +01:00
Giacomo Leidi 5710d46874
Update README.md. 2024-01-07 22:42:51 +01:00
Giacomo Leidi 9794b00cc0
add badges 2024-01-07 22:37:58 +01:00
Giacomo Leidi 5ebaa04f3d
Revert "Update .envrc"
This reverts commit 1e43a4e12d.
2023-10-30 20:58:53 +01:00
Giacomo Leidi 0f19cf4a9e
Revert "temporary lock"
This reverts commit 8d3026523a.
2023-10-30 20:57:07 +01:00
Giacomo Leidi 8d3026523a
temporary lock 2023-10-10 19:55:39 +02:00
Giacomo Leidi 1e43a4e12d
Update .envrc 2023-10-10 18:04:05 +02:00
Giacomo Leidi 45e1f551d8
Update release.yml 2023-07-17 17:48:32 +02:00
Giacomo Leidi 056e0217aa
Release v0.3.6. 2023-07-16 15:33:26 +02:00
Giacomo Leidi acce3a83fe
Enable publishing events by UUID. (#187) 2023-07-16 15:09:08 +02:00
Giacomo Leidi 775fb89cf6
telegram: Add support for topics. (#184) 2023-07-16 13:27:00 +02:00
Giacomo Leidi bf3170cb6f
Decouple DB instantiation from logger instantion. (#188) 2023-07-11 22:24:56 +02:00
Giacomo Leidi ff7567dc1b
Update docker image. 2023-07-11 19:20:08 +02:00
Giacomo Leidi f16cffa44e
Update Guix channel references. 2023-07-11 17:23:21 +02:00
Giacomo Leidi 7bcb374891
scripts/scheduler.py: Pass dry_run configuration. 2023-07-11 15:50:12 +02:00
Giacomo Leidi b17dc556d7
Fix scheduler. 2023-07-11 15:23:40 +02:00
Giacomo Leidi 201e259d37
Update CI 2023-07-11 02:19:01 +02:00
Giacomo Leidi 9744f436ae
Update build_docker_image.sh. 2023-07-11 01:50:19 +02:00
Giacomo Leidi 34ebd8f982
Update release.yml 2023-06-11 00:07:35 +02:00
Giacomo Leidi aaff82fe98
Update .envrc. 2023-06-10 23:34:12 +02:00
Giacomo Leidi 1c7e3c7ed5
Update docker-compose.yml 2023-06-10 23:31:04 +02:00
Giacomo Leidi c40a7aca35
Release v0.3.5. 2023-06-10 23:27:34 +02:00
Giacomo Leidi 4757cc6ec8
Hotfix release script 2023-06-10 23:24:59 +02:00
Giacomo Leidi 6bd2d606df
Store notifications. (#180) 2023-05-22 13:00:37 +02:00
58 changed files with 719 additions and 462 deletions

2
.envrc
View File

@ -13,7 +13,7 @@ if has guix; then
pre-commit uninstall pre-commit uninstall
fi fi
if [ ! -d "$venv_dir" ] ; then if [ ! -d "$venv_dir" ] ; then
virtualenv -p `which python3.9` "$venv_dir" virtualenv -p `which python3` "$venv_dir"
poetry install poetry install
pre-commit install pre-commit install
fi fi

View File

@ -5,10 +5,34 @@ name: CI
# Controls when the workflow will run # Controls when the workflow will run
on: on:
pull_request: pull_request:
paths-ignore:
- 'guix.scm'
- 'manifest.scm'
- 'channels-lock.scm'
- '.envrc'
- '.gitignore'
- 'pre-commit-*.yaml'
- Dockerfile
- README.*
- LICENSE
- 'sample_settings/**'
- 'etc/**'
push: push:
# Sequence of patterns matched against refs/tags # Sequence of patterns matched against refs/tags
branches: ["master"] branches: ["master"]
paths-ignore:
- 'guix.scm'
- 'manifest.scm'
- 'channels-lock.scm'
- '.envrc'
- '.gitignore'
- 'pre-commit-*.yaml'
- Dockerfile
- README.*
- LICENSE
- 'sample_settings/**'
- 'etc/**'
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
@ -16,20 +40,32 @@ on:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel # A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs: jobs:
run-tests-dev: run-tests-dev:
# The type of runner that the job will run on strategy:
runs-on: ubuntu-latest fail-fast: false
matrix:
# Steps represent a sequence of tasks that will be executed as part of the job python-version: ["3.10", "3.11"]
poetry-version: ["1.1.12", "1.7.0"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps: steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3
- uses: actions/checkout@v2 - uses: actions/setup-python@v4
# Runs a single command using the runners shell
- name: Set up Python 3.10
uses: actions/setup-python@v2
with: with:
python-version: "3.10" python-version: ${{ matrix.python-version }}
- name: Run image
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Setup a local virtual environment
run: |
poetry config virtualenvs.create true --local
poetry config virtualenvs.in-project true --local
- uses: actions/cache@v3
name: Define a cache for the virtual environment based on the dependencies lock file
with:
path: ./.venv
key: venv-${{ hashFiles('poetry.lock') }}
- name: Install dependencies - name: Install dependencies
run: scripts/install_github_actions_dev_dependencies.sh run: scripts/install_github_actions_dev_dependencies.sh
- name: Run tests in dev env - name: Run tests in dev env
run: scripts/run_pipeline_tests.sh run: scripts/run_pipeline_tests.sh

View File

@ -27,11 +27,11 @@ jobs:
# Runs a single command using the runners shell # Runs a single command using the runners shell
- name: Install GNU Guix - name: Install GNU Guix
uses: PromyLOPh/guix-install-action@v1 uses: PromyLOPh/guix-install-action@v1.4
# Runs a set of commands using the runners shell # Runs a set of commands using the runners shell
- name: Build image - name: Build image
run: scripts/build_docker_image.sh run: scripts/build_docker_image.sh -r
- name: Upload pack (Docker) - name: Upload pack (Docker)
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
@ -59,9 +59,9 @@ jobs:
uses: fishinthecalculator/publish-docker-image-action@v0.1.10 uses: fishinthecalculator/publish-docker-image-action@v0.1.10
env: env:
IMAGE_TAG: ${{ steps.vars.outputs.tag }} IMAGE_TAG: ${{ steps.vars.outputs.tag }}
IMAGE_NAME_TAG: mobilizon-reshare-scheduler:latest IMAGE_NAME_TAG: mobilizon-reshare-scheduler-python:latest
with: with:
name: fishinthecalculator/mobilizon-reshare name: twcita/mobilizon-reshare
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
image: docker-image.tar.gz image: docker-image.tar.gz

1
.img/license.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="118.9" height="20"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect width="118.9" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#round)"><rect width="56.2" height="20" fill="#555"/><rect x="56.2" width="62.7" height="20" fill="#007ec6"/><rect width="118.9" height="20" fill="url(#smooth)"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><text x="291.0" y="150" fill="#010101" fill-opacity=".3" transform="scale(0.1)" textLength="462.0" lengthAdjust="spacing">LICENSE</text><text x="291.0" y="140" transform="scale(0.1)" textLength="462.0" lengthAdjust="spacing">LICENSE</text><text x="865.5000000000001" y="150" fill="#010101" fill-opacity=".3" transform="scale(0.1)" textLength="527.0" lengthAdjust="spacing">Coopyleft</text><text x="865.5000000000001" y="140" transform="scale(0.1)" textLength="527.0" lengthAdjust="spacing">Coopyleft</text><a xlink:href="https://github.com/Tech-Workers-Coalition-Italia/mobilizon-reshare/blob/master/LICENSE"><rect width="56.2" height="20" fill="rgba(0,0,0,0)"/></a><a xlink:href="https://github.com/Tech-Workers-Coalition-Italia/mobilizon-reshare/blob/master/LICENSE"><rect x="56.2" width="62.7" height="20" fill="rgba(0,0,0,0)"/></a></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
.img/pypi.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="71.6" height="20"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect width="71.6" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#round)"><rect width="33.6" height="20" fill="#555"/><rect x="33.6" width="38.0" height="20" fill="#007ec6"/><rect width="71.6" height="20" fill="url(#smooth)"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><text x="178.0" y="150" fill="#010101" fill-opacity=".3" transform="scale(0.1)" textLength="236.0" lengthAdjust="spacing">pypi</text><text x="178.0" y="140" transform="scale(0.1)" textLength="236.0" lengthAdjust="spacing">pypi</text><text x="516.0" y="150" fill="#010101" fill-opacity=".3" transform="scale(0.1)" textLength="280.0" lengthAdjust="spacing">0.3.6</text><text x="516.0" y="140" transform="scale(0.1)" textLength="280.0" lengthAdjust="spacing">0.3.6</text><a xlink:href="https://pypi.org/project/mobilizon-reshare/"><rect width="33.6" height="20" fill="rgba(0,0,0,0)"/></a><a xlink:href="https://pypi.org/project/mobilizon-reshare/"><rect x="33.6" width="38.0" height="20" fill="rgba(0,0,0,0)"/></a></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
.img/python.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="131.5" height="20"><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="round"><rect width="131.5" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#round)"><rect width="65.5" height="20" fill="#555"/><rect x="65.5" width="66.0" height="20" fill="#007ec6"/><rect width="131.5" height="20" fill="url(#smooth)"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0icHlZZWxsb3ciIGdyYWRpZW50VHJhbnNmb3JtPSJyb3RhdGUoNDUpIj4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI2ZlNSIgb2Zmc2V0PSIwLjYiLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI2RhMSIgb2Zmc2V0PSIxIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJweUJsdWUiIGdyYWRpZW50VHJhbnNmb3JtPSJyb3RhdGUoNDUpIj4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzY5ZiIgb2Zmc2V0PSIwLjQiLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzQ2OCIgb2Zmc2V0PSIxIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KCiAgPHBhdGggZD0iTTI3LDE2YzAtNyw5LTEzLDI0LTEzYzE1LDAsMjMsNiwyMywxM2wwLDIyYzAsNy01LDEyLTExLDEybC0yNCwwYy04LDAtMTQsNi0xNCwxNWwwLDEwbC05LDBjLTgsMC0xMy05LTEzLTI0YzAtMTQsNS0yMywxMy0yM2wzNSwwbDAtM2wtMjQsMGwwLTlsMCwweiBNODgsNTB2MSIgZmlsbD0idXJsKCNweUJsdWUpIi8+CiAgPHBhdGggZD0iTTc0LDg3YzAsNy04LDEzLTIzLDEzYy0xNSwwLTI0LTYtMjQtMTNsMC0yMmMwLTcsNi0xMiwxMi0xMmwyNCwwYzgsMCwxNC03LDE0LTE1bDAtMTBsOSwwYzcsMCwxMyw5LDEzLDIzYzAsMTUtNiwyNC0xMywyNGwtMzUsMGwwLDNsMjMsMGwwLDlsMCwweiBNMTQwLDUwdjEiIGZpbGw9InVybCgjcHlZZWxsb3cpIi8+CgogIDxjaXJjbGUgcj0iNCIgY3g9IjY0IiBjeT0iODgiIGZpbGw9IiNGRkYiLz4KICA8Y2lyY2xlIHI9IjQiIGN4PSIzNyIgY3k9IjE1IiBmaWxsPSIjRkZGIi8+Cjwvc3ZnPgo="/><text x="422.5" y="150" fill="#010101" fill-opacity=".3" transform="scale(0.1)" textLength="385.0" lengthAdjust="spacing">python</text><text x="422.5" y="140" transform="scale(0.1)" textLength="385.0" lengthAdjust="spacing">python</text><text x="975.0" y="150" fill="#010101" fill-opacity=".3" transform="scale(0.1)" textLength="560.0" lengthAdjust="spacing">3.10, 3.11</text><text x="975.0" y="140" transform="scale(0.1)" textLength="560.0" lengthAdjust="spacing">3.10, 3.11</text><a xlink:href="https://www.python.org/"><rect width="65.5" height="20" fill="rgba(0,0,0,0)"/></a><a xlink:href="https://www.python.org/"><rect x="65.5" width="66.0" height="20" fill="rgba(0,0,0,0)"/></a></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -3,7 +3,7 @@ repos:
rev: stable rev: stable
hooks: hooks:
- id: black - id: black
language_version: python3.9 language_version: python3.10
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v1.2.3 rev: v1.2.3
hooks: hooks:

View File

@ -1,4 +1,7 @@
[![CI](https://github.com/Tech-Workers-Coalition-Italia/mobilizon-reshare/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/Tech-Workers-Coalition-Italia/mobilizon-reshare/actions/workflows/main.yml) [![CI](https://github.com/Tech-Workers-Coalition-Italia/mobilizon-reshare/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/Tech-Workers-Coalition-Italia/mobilizon-reshare/actions/workflows/main.yml)
[![Python versions](https://raw.githubusercontent.com/Tech-Workers-Coalition-Italia/mobilizon-reshare/master/.img/python.svg)](https://python.org)
[![PyPI version](https://raw.githubusercontent.com/Tech-Workers-Coalition-Italia/mobilizon-reshare/master/.img/pypi.svg)](https://pypi.org/project/mobilizon-reshare/)
[![License](https://raw.githubusercontent.com/Tech-Workers-Coalition-Italia/mobilizon-reshare/master/.img/license.svg)](https://github.com/Tech-Workers-Coalition-Italia/mobilizon-reshare/blob/master/LICENSE)
The goal of `mobilizon_reshare` is to provide a suite to reshare Mobilizon events on a broad selection of platforms. This The goal of `mobilizon_reshare` is to provide a suite to reshare Mobilizon events on a broad selection of platforms. This
tool enables an organization to automate their social media strategy in regards tool enables an organization to automate their social media strategy in regards
@ -37,7 +40,7 @@ commands and their description.
### Guix package ### Guix package
If you run Guix you can install `mobilizon-reshare` by adding our [Guix channel](https://github.com/fishinthecalculator/mobilizon-reshare-guix#configure) to your `.config/guix/channels.scm`. If you run Guix you can install `mobilizon-reshare` by adding our [Guix channel](https://git.sr.ht/~fishinthecalculator/mobilizon-reshare-guix#configure) to your `.config/guix/channels.scm`.

View File

@ -4,13 +4,13 @@
(list (list
(channel (channel
(name 'mobilizon-reshare) (name 'mobilizon-reshare)
(url "https://github.com/fishinthecalculator/mobilizon-reshare-guix") (url "https://git.sr.ht/~fishinthecalculator/mobilizon-reshare-guix")
(branch "main")) (branch "main"))
(channel (channel
(name 'guix) (name 'guix)
(url "https://git.savannah.gnu.org/git/guix.git") (url "https://git.savannah.gnu.org/git/guix.git")
(commit (commit
"79a3cd34c0318928186a04b6481c4d22c0051d04") "b7eb1a8116b2caee7acf26fb963ae998fbdb4253")
(introduction (introduction
(make-channel-introduction (make-channel-introduction
"afb9f2752315f131e4ddd44eba02eed403365085" "afb9f2752315f131e4ddd44eba02eed403365085"

View File

@ -1,14 +1,14 @@
version: "3.7" version: "3.7"
services: services:
mobilizon-reshare: mobilizon-reshare:
image: twcita/mobilizon-reshare:v0.3.2 image: twcita/mobilizon-reshare:v0.3.6
environment: environment:
SECRETS_FOR_DYNACONF: /etc/xdg/mobilizon-reshare/0.3.2/.secrets.toml SECRETS_FOR_DYNACONF: /etc/xdg/mobilizon-reshare/0.3.6/.secrets.toml
ENV_FOR_DYNACONF: production ENV_FOR_DYNACONF: production
MOBILIZON_RESHARE_INTERVAL: "*/15 10-18 * * 0-4" MOBILIZON_RESHARE_INTERVAL: "*/15 10-18 * * 0-4"
volumes: volumes:
- ./.secrets.toml:/etc/xdg/mobilizon-reshare/0.3.2/.secrets.toml:ro - ./.secrets.toml:/etc/xdg/mobilizon-reshare/0.3.6/.secrets.toml:ro
- ./mobilizon_reshare.toml:/etc/xdg/mobilizon-reshare/0.3.2/mobilizon_reshare.toml:ro - ./mobilizon_reshare.toml:/etc/xdg/mobilizon-reshare/0.3.6/mobilizon_reshare.toml:ro
- ./var:/var/lib/mobilizon-reshare - ./var:/var/lib/mobilizon-reshare
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro

View File

@ -4,11 +4,9 @@
#:use-module (guix gexp) #:use-module (guix gexp)
#:use-module (guix packages) #:use-module (guix packages)
#:use-module (guix utils) #:use-module (guix utils)
#:use-module (gnu packages databases) ;; for python-tortoise-orm #:use-module (gnu packages markup) ;; for python-markdownify
#:use-module (gnu packages markup) ;; for python-markdownify #:use-module (gnu packages python-web) ;; for python-fastapi-pagination-minimal and uvicorn
#:use-module (gnu packages python) #:use-module (gnu packages python-xyz) ;; for python-apscheduler
#:use-module (gnu packages python-web) ;; for python-uvicorn
#:use-module (gnu packages python-xyz) ;; for dynaconf
#:use-module (mobilizon-reshare package) #:use-module (mobilizon-reshare package)
#:use-module (mobilizon-reshare dependencies) #:use-module (mobilizon-reshare dependencies)
#:use-module (ice-9 rdelim) #:use-module (ice-9 rdelim)
@ -21,33 +19,7 @@
#:recursive? #t #:recursive? #t
#:select? (git-predicate %source-dir))) #:select? (git-predicate %source-dir)))
(use-modules (guix download) (define mobilizon-reshare.git
(guix transformations))
(define-public python-tweepy-4.13
(package
(inherit python-tweepy)
(version "4.13.0")
(source (origin
(method url-fetch)
(uri (pypi-uri "tweepy" version))
(sha256
(base32
"123cikpmp2m360pxh2qarb4kkjmv8wi2prx7df178rlzbwrjax09"))))
(arguments
`(#:tests? #f))))
(define-public python-oauthlib-3.2
(package
(inherit python-oauthlib)
(version "3.2.2")
(source (origin
(method url-fetch)
(uri (pypi-uri "oauthlib" version))
(sha256
(base32
"066r7mimlpb5q1fr2f1z59l4jc89kv4h2kgkcifyqav6544w8ncq"))))))
(define _mobilizon-reshare.git
(let ((source-version (with-input-from-file (let ((source-version (with-input-from-file
(string-append %source-dir (string-append %source-dir
"/mobilizon_reshare/VERSION") "/mobilizon_reshare/VERSION")
@ -55,43 +27,24 @@
(revision "0") (revision "0")
(commit (read-line (commit (read-line
(open-input-pipe "git show HEAD | head -1 | cut -d ' ' -f 2")))) (open-input-pipe "git show HEAD | head -1 | cut -d ' ' -f 2"))))
(package (inherit mobilizon-reshare) ((package-input-rewriting/spec `(("python-fastapi" . ,(const python-fastapi))
(name "mobilizon-reshare.git") ("python-dotenv" . ,(const python-dotenv-0.13.0))
(version (git-version source-version revision commit)) ("python-uvicorn" . ,(const python-uvicorn))))
(source mobilizon-reshare-git-origin) (package (inherit mobilizon-reshare)
(arguments (name "mobilizon-reshare.git")
(substitute-keyword-arguments (package-arguments mobilizon-reshare) (version (git-version source-version revision commit))
((#:phases phases) (source mobilizon-reshare-git-origin)
#~(modify-phases #$phases (propagated-inputs
(add-after 'unpack 'patch-version (modify-inputs (package-propagated-inputs mobilizon-reshare)
(lambda _ (replace "python-uvicorn" python-uvicorn)
(with-output-to-file "mobilizon_reshare/VERSION" (replace "python-fastapi" python-fastapi)
(lambda _ (replace "python-fastapi-pagination-minimal"
(display #$version))))) (package
(delete 'patch-pyproject.toml))))) (inherit python-fastapi-pagination-minimal)
(native-inputs (propagated-inputs
(modify-inputs (package-native-inputs mobilizon-reshare) (modify-inputs (package-propagated-inputs python-fastapi-pagination-minimal)
(prepend python-httpx))) (replace "python-fastapi" python-fastapi)))))
(propagated-inputs (replace "python-markdownify" python-markdownify)))))))
(modify-inputs (package-propagated-inputs mobilizon-reshare)
(prepend python-asyncpg
python-uvicorn
python-fastapi
python-fastapi-pagination)
(replace "python-tweepy"
python-tweepy-4.13)
(replace "dynaconf"
dynaconf-3.1.11)
(replace "python-markdownify"
python-markdownify))))))
(define-public patch-for-mobilizon-reshare-0.3.3
(package-input-rewriting/spec `(("python-oauthlib" . ,(const python-oauthlib-3.2))
("python-beautifulsoup4" . ,(const python-beautifulsoup4))
("python-tortoise-orm" . ,(const python-tortoise-orm)))))
(define-public mobilizon-reshare.git
(patch-for-mobilizon-reshare-0.3.3 _mobilizon-reshare.git))
(define-public mobilizon-reshare-scheduler (define-public mobilizon-reshare-scheduler
(package (inherit mobilizon-reshare.git) (package (inherit mobilizon-reshare.git)

View File

@ -12,6 +12,6 @@
(map cadr (package-direct-inputs mobilizon-reshare)) (map cadr (package-direct-inputs mobilizon-reshare))
(map specification->package+output (map specification->package+output
'("git-cal" "man-db" "texinfo" '("git-cal" "man-db" "texinfo"
"python-pre-commit" "cloc" "pre-commit" "cloc"
"ripgrep" "python-semver" "ripgrep" "python-semver"
"fd" "docker-compose" "poetry")))) "fd" "docker-compose" "poetry"))))

View File

@ -1,6 +1,7 @@
[default.publisher.telegram] [default.publisher.telegram]
active=true active=true
chat_id="xxx" chat_id="xxx"
message_thread_id="xxx"
token="xxx" token="xxx"
username="xxx" username="xxx"
[default.publisher.zulip] [default.publisher.zulip]
@ -31,6 +32,7 @@ page_access_token="xxx"
[default.notifier.telegram] [default.notifier.telegram]
active=true active=true
chat_id="xxx" chat_id="xxx"
message_thread_id="xxx"
token="xxx" token="xxx"
username="xxx" username="xxx"
[default.notifier.zulip] [default.notifier.zulip]
@ -51,4 +53,4 @@ active=false
[default.notifier.facebook] [default.notifier.facebook]
active=false active=false
page_access_token="xxx" page_access_token="xxx"

View File

@ -1 +1 @@
0.3.2 0.3.6

View File

@ -5,6 +5,7 @@ import sys
import traceback import traceback
from mobilizon_reshare.config.command import CommandConfig from mobilizon_reshare.config.command import CommandConfig
from mobilizon_reshare.config.config import init_logging
from mobilizon_reshare.storage.db import tear_down, init from mobilizon_reshare.storage.db import tear_down, init
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -15,6 +16,7 @@ async def graceful_exit():
async def _safe_execution(function): async def _safe_execution(function):
init_logging()
await init() await init()
return_code = 1 return_code = 1

View File

@ -17,7 +17,7 @@ from mobilizon_reshare.cli.commands.retry.main import (
) )
from mobilizon_reshare.cli.commands.start.main import start_command as start_main from mobilizon_reshare.cli.commands.start.main import start_command as start_main
from mobilizon_reshare.config.command import CommandConfig from mobilizon_reshare.config.command import CommandConfig
from mobilizon_reshare.config.config import current_version, get_settings from mobilizon_reshare.config.config import current_version, get_settings, init_logging
from mobilizon_reshare.config.publishers import publisher_names from mobilizon_reshare.config.publishers import publisher_names
from mobilizon_reshare.dataclasses.event import _EventPublicationStatus from mobilizon_reshare.dataclasses.event import _EventPublicationStatus
from mobilizon_reshare.models.publication import PublicationStatus from mobilizon_reshare.models.publication import PublicationStatus
@ -27,7 +27,8 @@ from mobilizon_reshare.publishers import get_active_publishers
def test_settings(ctx, param, value): def test_settings(ctx, param, value):
if not value or ctx.resilient_parsing: if not value or ctx.resilient_parsing:
return return
get_settings() settings = get_settings()
init_logging(settings)
click.echo("OK!") click.echo("OK!")
ctx.exit() ctx.exit()
@ -87,26 +88,21 @@ publication_status_argument = click.argument(
default="all", default="all",
expose_value=True, expose_value=True,
) )
event_uuid_option = click.option( force_publish_option = click.option(
"-E", "-F",
"--event", "--force",
type=click.UUID, type=click.UUID,
expose_value=True, expose_value=True,
help="Publish the given event.", help="Publish the given event, bypassing all selection logic. This command WILL publish"
) "regardless of the configured strategy, so use it with care.",
publication_uuid_option = click.option(
"-P",
"--publication",
type=click.UUID,
expose_value=True,
help="Publish the given publication.",
) )
platform_name_option = click.option( platform_name_option = click.option(
"-p", "-p",
"--platform", "--platform",
type=str, type=str,
expose_value=True, expose_value=True,
help="Publish to the given platform. This makes sense only for events.", help="Restrict the platforms where the event will be published. This makes sense only in"
" case of force-publishing.",
) )
list_supported_option = click.option( list_supported_option = click.option(
"--list-platforms", "--list-platforms",
@ -181,11 +177,19 @@ def pull():
help="Select an event with the current configured strategy" help="Select an event with the current configured strategy"
" and publish it to all active platforms." " and publish it to all active platforms."
) )
@event_uuid_option @force_publish_option
@publication_uuid_option
@platform_name_option @platform_name_option
def publish(): @click.option(
safe_execution(publish_main,) "--dry-run",
"dry_run",
is_flag=True,
help="Prevents data to be published to platforms.",
default=False,
)
def publish(event, platform, dry_run):
safe_execution(functools.partial(
publish_main, event, platform
), CommandConfig(dry_run=dry_run))
@mobilizon_reshare.group(help="Operations that pertain to events") @mobilizon_reshare.group(help="Operations that pertain to events")

View File

@ -24,7 +24,7 @@ def pretty(publication: Publication):
return ( return (
f"{str(publication.id) : <40}{publication.timestamp.isoformat() : <36}" f"{str(publication.id) : <40}{publication.timestamp.isoformat() : <36}"
f"{click.style(publication.status.name, fg=status_to_color[publication.status]) : <22}" f"{click.style(publication.status.name, fg=status_to_color[publication.status]) : <22}"
f"{publication.publisher.name : <12}{str(publication.event.id)}" f"{publication.publisher.name : <12}{str(publication.event.mobilizon_id)}"
) )
@ -33,7 +33,6 @@ async def list_publications(
frm: Optional[datetime] = None, frm: Optional[datetime] = None,
to: Optional[datetime] = None, to: Optional[datetime] = None,
): ):
frm = Arrow.fromdatetime(frm) if frm else None frm = Arrow.fromdatetime(frm) if frm else None
to = Arrow.fromdatetime(to) if to else None to = Arrow.fromdatetime(to) if to else None
if status is None: if status is None:

View File

@ -1,14 +1,23 @@
import logging import logging
import click
from mobilizon_reshare.main.publish import select_and_publish from mobilizon_reshare.config.command import CommandConfig
from mobilizon_reshare.main.publish import select_and_publish, publish_by_mobilizon_id
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def publish_command(): async def publish_command(event_mobilizon_id: click.UUID, platform: str, command_config: CommandConfig):
""" """
Select an event with the current configured strategy Select an event with the current configured strategy
and publish it to all active platforms. and publish it to all active platforms.
""" """
report = await select_and_publish() if event_mobilizon_id is not None:
report = await publish_by_mobilizon_id(
event_mobilizon_id,
command_config,
[platform] if platform is not None else None,
)
else:
report = await select_and_publish(command_config)
return 0 if report and report.successful else 1 return 0 if report and report.successful else 1

View File

@ -1,9 +1,9 @@
import importlib.resources import importlib
import logging import logging
from logging.config import dictConfig
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import pkg_resources
from appdirs import AppDirs from appdirs import AppDirs
from dynaconf import Dynaconf, Validator from dynaconf import Dynaconf, Validator
@ -38,23 +38,30 @@ def current_version() -> str:
return fp.read() return fp.read()
def init_logging(settings: Optional[Dynaconf] = None):
if settings is None:
settings = get_settings()
dictConfig(settings["logging"])
def get_settings_files_paths() -> Optional[str]: def get_settings_files_paths() -> Optional[str]:
dirs = AppDirs(appname="mobilizon-reshare", version=current_version()) dirs = AppDirs(appname="mobilizon-reshare", version=current_version())
bundled_settings_path = pkg_resources.resource_filename( bundled_settings_ref = importlib.resources.files(
"mobilizon_reshare", "settings.toml" "mobilizon_reshare"
) ) / "settings.toml"
for config_path in [ with importlib.resources.as_file(bundled_settings_ref) as bundled_settings_path:
Path(dirs.user_config_dir, "mobilizon_reshare.toml").absolute(), for config_path in [
Path(dirs.site_config_dir, "mobilizon_reshare.toml").absolute(), Path(dirs.user_config_dir, "mobilizon_reshare.toml").absolute(),
bundled_settings_path, Path(dirs.site_config_dir, "mobilizon_reshare.toml").absolute(),
]: bundled_settings_path.absolute(),
if config_path and Path(config_path).exists(): ]:
logger.debug(f"Loading configuration from {config_path}") if config_path and Path(config_path).exists():
return config_path logger.debug(f"Loading configuration from {config_path}")
return config_path
def build_settings(validators: Optional[list[Validator]] = None): def build_settings(validators: Optional[list[Validator]] = None) -> Dynaconf:
""" """
Creates a Dynaconf base object. Configuration files are checked in this order: Creates a Dynaconf base object. Configuration files are checked in this order:
@ -78,7 +85,7 @@ def build_settings(validators: Optional[list[Validator]] = None):
return config return config
def build_and_validate_settings(): def build_and_validate_settings() -> Dynaconf:
""" """
Creates a settings object to be used in the application. It collects and apply generic validators and validators Creates a settings object to be used in the application. It collects and apply generic validators and validators
specific for each publisher, notifier and publication strategy. specific for each publisher, notifier and publication strategy.
@ -128,9 +135,9 @@ class CustomConfig:
cls._instance = None cls._instance = None
def get_settings(): def get_settings() -> Dynaconf:
return CustomConfig.get_instance().settings return CustomConfig.get_instance().settings
def get_settings_without_validation(): def get_settings_without_validation() -> Dynaconf:
return build_settings() return build_settings()

View File

@ -4,6 +4,7 @@ from dynaconf import Validator
telegram_validators = [ telegram_validators = [
Validator("notifier.telegram.chat_id", must_exist=True), Validator("notifier.telegram.chat_id", must_exist=True),
Validator("notifier.telegram.message_thread_id", default=None),
Validator("notifier.telegram.token", must_exist=True), Validator("notifier.telegram.token", must_exist=True),
Validator("notifier.telegram.username", must_exist=True), Validator("notifier.telegram.username", must_exist=True),
] ]

View File

@ -3,6 +3,7 @@ from dynaconf import Validator
telegram_validators = [ telegram_validators = [
Validator("publisher.telegram.chat_id", must_exist=True), Validator("publisher.telegram.chat_id", must_exist=True),
Validator("publisher.telegram.message_thread_id", default=None),
Validator("publisher.telegram.msg_template_path", must_exist=True, default=None), Validator("publisher.telegram.msg_template_path", must_exist=True, default=None),
Validator("publisher.telegram.recap_template_path", must_exist=True, default=None), Validator("publisher.telegram.recap_template_path", must_exist=True, default=None),
Validator( Validator(

View File

@ -2,8 +2,12 @@ from mobilizon_reshare.dataclasses.event import _MobilizonEvent
from mobilizon_reshare.dataclasses.event_publication_status import ( from mobilizon_reshare.dataclasses.event_publication_status import (
_EventPublicationStatus, _EventPublicationStatus,
) )
from mobilizon_reshare.dataclasses.publication import _EventPublication from mobilizon_reshare.dataclasses.publication import (
_EventPublication,
_PublicationNotification,
)
EventPublication = _EventPublication EventPublication = _EventPublication
MobilizonEvent = _MobilizonEvent MobilizonEvent = _MobilizonEvent
EventPublicationStatus = _EventPublicationStatus EventPublicationStatus = _EventPublicationStatus
PublicationNotification = _PublicationNotification

View File

@ -108,7 +108,7 @@ class _MobilizonEvent:
async def get_all_mobilizon_events( async def get_all_mobilizon_events(
from_date: Optional[Arrow] = None, to_date: Optional[Arrow] = None, from_date: Optional[Arrow] = None, to_date: Optional[Arrow] = None,
) -> list[_MobilizonEvent]: ) -> list[_MobilizonEvent]:
return [_MobilizonEvent.from_model(event) for event in await get_all_events()] return [_MobilizonEvent.from_model(event) for event in await get_all_events(from_date, to_date)]
async def get_published_events( async def get_published_events(
@ -155,3 +155,10 @@ async def get_mobilizon_events_without_publications(
from_date=from_date, to_date=to_date from_date=from_date, to_date=to_date
) )
] ]
async def get_mobilizon_event_by_id(
event_id: UUID,
) -> _MobilizonEvent:
event = await get_event(event_id)
return _MobilizonEvent.from_model(event)

View File

@ -54,6 +54,11 @@ class RecapPublication(BasePublication):
events: List[_MobilizonEvent] events: List[_MobilizonEvent]
@dataclass
class _PublicationNotification(BasePublication):
publication: _EventPublication
@atomic() @atomic()
async def build_publications_for_event( async def build_publications_for_event(
event: _MobilizonEvent, publishers: Iterator[str] event: _MobilizonEvent, publishers: Iterator[str]

View File

@ -6,6 +6,7 @@ from mobilizon_reshare.dataclasses import MobilizonEvent
from mobilizon_reshare.dataclasses.event import ( from mobilizon_reshare.dataclasses.event import (
get_published_events, get_published_events,
get_mobilizon_events_without_publications, get_mobilizon_events_without_publications,
get_mobilizon_event_by_id,
) )
from mobilizon_reshare.dataclasses.publication import ( from mobilizon_reshare.dataclasses.publication import (
_EventPublication, _EventPublication,
@ -23,7 +24,10 @@ from mobilizon_reshare.publishers.coordinators.event_publishing.publish import (
PublisherCoordinatorReport, PublisherCoordinatorReport,
PublisherCoordinator, PublisherCoordinator,
) )
from mobilizon_reshare.storage.query.write import save_publication_report from mobilizon_reshare.storage.query.write import (
save_publication_report,
save_notification_report,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,14 +35,16 @@ logger = logging.getLogger(__name__)
async def publish_publications( async def publish_publications(
publications: list[_EventPublication], publications: list[_EventPublication],
) -> PublisherCoordinatorReport: ) -> PublisherCoordinatorReport:
report = PublisherCoordinator(publications).run() publishers_report = PublisherCoordinator(publications).run()
await save_publication_report(publishers_report)
await save_publication_report(report) for publication_report in publishers_report.reports:
for publication_report in report.reports:
if not publication_report.successful: if not publication_report.successful:
PublicationFailureNotifiersCoordinator(publication_report,).notify_failure() notifiers_report = PublicationFailureNotifiersCoordinator(publication_report,).notify_failure()
if notifiers_report:
await save_notification_report(notifiers_report)
return report return publishers_report
def perform_dry_run(publications: list[_EventPublication]): def perform_dry_run(publications: list[_EventPublication]):
@ -63,6 +69,15 @@ async def publish_event(
return await publish_publications(publications) return await publish_publications(publications)
async def publish_by_mobilizon_id(
event_mobilizon_id,
command_config: CommandConfig,
publishers: Optional[Iterator[str]] = None,
):
event = await get_mobilizon_event_by_id(event_mobilizon_id)
return await publish_event(event, command_config, publishers)
async def select_and_publish( async def select_and_publish(
command_config: CommandConfig, command_config: CommandConfig,
unpublished_events: Optional[list[MobilizonEvent]] = None, unpublished_events: Optional[list[MobilizonEvent]] = None,

View File

@ -5,10 +5,8 @@ from tortoise.models import Model
class NotificationStatus(IntEnum): class NotificationStatus(IntEnum):
WAITING = 1 FAILED = 0
FAILED = 2 COMPLETED = 1
PARTIAL = 3
COMPLETED = 4
class Notification(Model): class Notification(Model):

View File

@ -1,3 +1,4 @@
import importlib
import inspect import inspect
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@ -124,6 +125,33 @@ class AbstractEventFormatter(LoggerMixin, ConfLoaderMixin):
""" """
raise NotImplementedError # pragma: no cover raise NotImplementedError # pragma: no cover
def _get_name(self) -> str:
return self._conf[1]
def _get_template(self, configured_template, default_generator) -> Template:
if configured_template:
return JINJA_ENV.get_template(configured_template)
else:
template_ref = default_generator()
with importlib.resources.as_file(template_ref) as template_path:
return JINJA_ENV.get_template(template_path.as_posix())
def get_default_template_path(self, type=""):
return importlib.resources.files(
"mobilizon_reshare.publishers.templates"
) / f"{self._get_name()}{type}.tmpl.j2"
def get_default_recap_template_path(self):
return self.get_default_template_path(type="_recap")
def get_default_recap_header_template_path(self):
return self.get_default_template_path(type="_recap_header")
def validate_event(self, event: _MobilizonEvent) -> None: def validate_event(self, event: _MobilizonEvent) -> None:
self._validate_event(event) self._validate_event(event)
self._validate_message(self.get_message_from_event(event)) self._validate_message(self.get_message_from_event(event))
@ -148,21 +176,20 @@ class AbstractEventFormatter(LoggerMixin, ConfLoaderMixin):
""" """
Retrieves publisher's message template. Retrieves publisher's message template.
""" """
template_path = self.conf.msg_template_path or self.default_template_path return self._get_template(self.conf.msg_template_path, self.get_default_template_path)
return JINJA_ENV.get_template(template_path)
def get_recap_header(self): def get_recap_header(self) -> Template:
template_path = ( return self._get_template(
self.conf.recap_header_template_path self.conf.recap_header_template_path,
or self.default_recap_header_template_path self.get_default_recap_header_template_path
) )
return JINJA_ENV.get_template(template_path).render()
def get_recap_fragment_template(self) -> Template: def get_recap_fragment_template(self) -> Template:
template_path = ( return self._get_template(
self.conf.recap_template_path or self.default_recap_template_path self.conf.recap_template_path,
self.get_default_recap_template_path
) )
return JINJA_ENV.get_template(template_path)
def get_recap_fragment(self, event: _MobilizonEvent) -> str: def get_recap_fragment(self, event: _MobilizonEvent) -> str:
""" """

View File

@ -16,7 +16,7 @@ class BasePublicationReport:
def get_failure_message(self): def get_failure_message(self):
return ( return (
f"Publication failed with status: {self.status}.\n" f"Reason: {self.reason}" f"Publication failed with status: {self.status.name}.\n" f"Reason: {self.reason}"
) )
@ -26,7 +26,7 @@ class BaseCoordinatorReport:
@property @property
def successful(self): def successful(self):
return all(r.status == PublicationStatus.COMPLETED for r in self.reports) return all(r.successful for r in self.reports)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -20,7 +20,7 @@ class EventPublicationReport(BasePublicationReport):
logger.error("Report of failure without reason.", exc_info=True) logger.error("Report of failure without reason.", exc_info=True)
return ( return (
f"Publication {self.publication.id} failed with status: {self.status}.\n" f"Publication {self.publication.id} failed with status: {self.status.name}.\n"
f"Reason: {self.reason}\n" f"Reason: {self.reason}\n"
f"Publisher: {self.publication.publisher.name}\n" f"Publisher: {self.publication.publisher.name}\n"
f"Event: {self.publication.event.name}" f"Event: {self.publication.event.name}"

View File

@ -1,3 +1,4 @@
import logging
from typing import List, Sequence from typing import List, Sequence
from mobilizon_reshare.dataclasses import _EventPublication from mobilizon_reshare.dataclasses import _EventPublication
@ -7,6 +8,8 @@ from mobilizon_reshare.publishers.coordinators.event_publishing.publish import (
EventPublicationReport, EventPublicationReport,
) )
logger = logging.getLogger(__name__)
class DryRunPublisherCoordinator(PublisherCoordinator): class DryRunPublisherCoordinator(PublisherCoordinator):
""" """
@ -14,7 +17,7 @@ class DryRunPublisherCoordinator(PublisherCoordinator):
""" """
def _publish(self, publications: Sequence[_EventPublication]) -> List[EventPublicationReport]: def _publish(self, publications: Sequence[_EventPublication]) -> List[EventPublicationReport]:
return [ reports = [
EventPublicationReport( EventPublicationReport(
status=PublicationStatus.COMPLETED, status=PublicationStatus.COMPLETED,
publication=publication, publication=publication,
@ -25,3 +28,9 @@ class DryRunPublisherCoordinator(PublisherCoordinator):
) )
for publication in publications for publication in publications
] ]
logger.info("The following events would be published:")
for r in reports:
event_name = r.publication.event.name
publisher_name = r.publication.publisher.name
logger.info(f"{event_name}{publisher_name}")
return reports

View File

@ -1,33 +1,92 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List from dataclasses import dataclass, field
from typing import List, Optional, Sequence
from mobilizon_reshare.dataclasses import PublicationNotification, EventPublication
from mobilizon_reshare.models.notification import NotificationStatus
from mobilizon_reshare.models.publication import PublicationStatus from mobilizon_reshare.models.publication import PublicationStatus
from mobilizon_reshare.publishers import get_active_notifiers from mobilizon_reshare.publishers import get_active_notifiers
from mobilizon_reshare.publishers.abstract import AbstractPlatform from mobilizon_reshare.publishers.abstract import (
from mobilizon_reshare.publishers.coordinators import logger AbstractPlatform,
from mobilizon_reshare.publishers.coordinators.event_publishing.publish import ( )
from mobilizon_reshare.publishers.coordinators import (
logger,
BasePublicationReport,
BaseCoordinatorReport,
)
from mobilizon_reshare.publishers.coordinators.event_publishing import (
EventPublicationReport, EventPublicationReport,
) )
from mobilizon_reshare.publishers.platforms.platform_mapping import get_notifier_class from mobilizon_reshare.publishers.platforms.platform_mapping import (
get_notifier_class,
get_formatter_class,
)
@dataclass
class PublicationNotificationReport(BasePublicationReport):
status: NotificationStatus
notification: PublicationNotification
@property
def successful(self):
return self.status == NotificationStatus.COMPLETED
def get_failure_message(self):
if not self.reason:
logger.error("Report of failure without reason.", exc_info=True)
return (
f"Failed with status: {self.status.name}.\n"
f"Reason: {self.reason}\n"
f"Publisher: {self.notification.publisher.name}\n"
f"Publication: {self.notification.publication.id}"
)
@dataclass
class NotifierCoordinatorReport(BaseCoordinatorReport):
reports: Sequence[PublicationNotificationReport]
notifications: Sequence[PublicationNotification] = field(default_factory=list)
class Sender: class Sender:
def __init__(self, message: str, platforms: List[AbstractPlatform] = None): def __init__(
self,
message: str,
publication: EventPublication,
platforms: List[AbstractPlatform] = None,
):
self.message = message self.message = message
self.platforms = platforms self.platforms = platforms
self.publication = publication
def send_to_all(self): def send_to_all(self) -> NotifierCoordinatorReport:
reports = []
notifications = []
for platform in self.platforms: for platform in self.platforms:
notification = PublicationNotification(
platform, get_formatter_class(platform.name)(), self.publication
)
try: try:
platform.send(self.message) platform.send(self.message)
report = PublicationNotificationReport(
NotificationStatus.COMPLETED, self.message, notification
)
except Exception as e: except Exception as e:
logger.critical(f"Failed to send message:\n{self.message}") msg = f"[{platform.name}] Failed to notify failure of message:\n{self.message}"
logger.critical(msg)
logger.exception(e) logger.exception(e)
report = PublicationNotificationReport(
NotificationStatus.FAILED, msg, notification
)
notifications.append(notification)
reports.append(report)
return NotifierCoordinatorReport(reports=reports, notifications=notifications)
class AbstractNotifiersCoordinator(ABC): class AbstractNotifiersCoordinator(ABC):
def __init__( def __init__(
self, report: EventPublicationReport, notifiers: List[AbstractPlatform] = None self, report: BasePublicationReport, notifiers: List[AbstractPlatform] = None
): ):
self.platforms = notifiers or [ self.platforms = notifiers or [
get_notifier_class(notifier)() for notifier in get_active_notifiers() get_notifier_class(notifier)() for notifier in get_active_notifiers()
@ -44,10 +103,17 @@ class PublicationFailureNotifiersCoordinator(AbstractNotifiersCoordinator):
Sends a notification of a failure report to the active platforms Sends a notification of a failure report to the active platforms
""" """
def notify_failure(self): report: EventPublicationReport
platforms: List[AbstractPlatform]
def notify_failure(self) -> Optional[NotifierCoordinatorReport]:
logger.info("Sending failure notifications") logger.info("Sending failure notifications")
if self.report.status == PublicationStatus.FAILED: if self.report.status == PublicationStatus.FAILED:
Sender(self.report.get_failure_message(), self.platforms).send_to_all() return Sender(
self.report.get_failure_message(),
self.report.publication,
self.platforms,
).send_to_all()
class PublicationFailureLoggerCoordinator(PublicationFailureNotifiersCoordinator): class PublicationFailureLoggerCoordinator(PublicationFailureNotifiersCoordinator):

View File

@ -1,7 +1,6 @@
from typing import Optional from typing import Optional
import facebook import facebook
import pkg_resources
from facebook import GraphAPIError from facebook import GraphAPIError
from mobilizon_reshare.dataclasses import MobilizonEvent from mobilizon_reshare.dataclasses import MobilizonEvent
@ -19,19 +18,7 @@ from mobilizon_reshare.publishers.exceptions import (
class FacebookFormatter(AbstractEventFormatter): class FacebookFormatter(AbstractEventFormatter):
_conf = ("publisher", "facebook") _conf = ("publisher", "facebook")
default_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "facebook.tmpl.j2"
)
default_recap_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "facebook_recap.tmpl.j2"
)
default_recap_header_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "facebook_recap_header.tmpl.j2"
)
def _validate_event(self, event: MobilizonEvent) -> None: def _validate_event(self, event: MobilizonEvent) -> None:
text = event.description text = event.description

View File

@ -1,7 +1,6 @@
from typing import Optional from typing import Optional
from urllib.parse import urljoin from urllib.parse import urljoin
import pkg_resources
import requests import requests
from requests import Response from requests import Response
@ -20,19 +19,7 @@ from mobilizon_reshare.publishers.exceptions import (
class MastodonFormatter(AbstractEventFormatter): class MastodonFormatter(AbstractEventFormatter):
_conf = ("publisher", "mastodon") _conf = ("publisher", "mastodon")
default_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "mastodon.tmpl.j2"
)
default_recap_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "mastodon_recap.tmpl.j2"
)
default_recap_header_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "mastodon_recap_header.tmpl.j2"
)
def _validate_event(self, event: MobilizonEvent) -> None: def _validate_event(self, event: MobilizonEvent) -> None:
text = event.description text = event.description

View File

@ -1,7 +1,6 @@
import re import re
from typing import Optional from typing import Optional
import pkg_resources
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from requests import Response from requests import Response
@ -20,18 +19,6 @@ from mobilizon_reshare.publishers.exceptions import (
class TelegramFormatter(AbstractEventFormatter): class TelegramFormatter(AbstractEventFormatter):
default_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "telegram.tmpl.j2"
)
default_recap_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "telegram_recap.tmpl.j2"
)
default_recap_header_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "telegram_recap_header.tmpl.j2"
)
_conf = ("publisher", "telegram") _conf = ("publisher", "telegram")
def _validate_event(self, event: MobilizonEvent) -> None: def _validate_event(self, event: MobilizonEvent) -> None:
@ -99,9 +86,14 @@ class TelegramPlatform(AbstractPlatform):
) )
def _send(self, message: str, event: Optional[MobilizonEvent] = None) -> Response: def _send(self, message: str, event: Optional[MobilizonEvent] = None) -> Response:
json_message = {"chat_id": self.conf.chat_id, "text": message, "parse_mode": "html"}
if self.conf.message_thread_id:
json_message["message_thread_id"] = self.conf.message_thread_id
return requests.post( return requests.post(
url=f"https://api.telegram.org/bot{self.conf.token}/sendMessage", url=f"https://api.telegram.org/bot{self.conf.token}/sendMessage",
json={"chat_id": self.conf.chat_id, "text": message, "parse_mode": "html"}, json=json_message,
) )
def _validate_response(self, res): def _validate_response(self, res):

View File

@ -1,6 +1,5 @@
from typing import Optional from typing import Optional
import pkg_resources
from tweepy import OAuthHandler, API, TweepyException from tweepy import OAuthHandler, API, TweepyException
from tweepy.models import Status from tweepy.models import Status
@ -17,19 +16,7 @@ from mobilizon_reshare.publishers.exceptions import (
class TwitterFormatter(AbstractEventFormatter): class TwitterFormatter(AbstractEventFormatter):
_conf = ("publisher", "twitter") _conf = ("publisher", "twitter")
default_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "twitter.tmpl.j2"
)
default_recap_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "twitter_recap.tmpl.j2"
)
default_recap_header_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "twitter_recap_header.tmpl.j2"
)
def _validate_event(self, event: MobilizonEvent) -> None: def _validate_event(self, event: MobilizonEvent) -> None:
pass # pragma: no cover pass # pragma: no cover

View File

@ -1,7 +1,6 @@
from typing import Optional from typing import Optional
from urllib.parse import urljoin from urllib.parse import urljoin
import pkg_resources
import requests import requests
from requests import Response from requests import Response
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
@ -23,19 +22,7 @@ from mobilizon_reshare.publishers.exceptions import (
class ZulipFormatter(AbstractEventFormatter): class ZulipFormatter(AbstractEventFormatter):
_conf = ("publisher", "zulip") _conf = ("publisher", "zulip")
default_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "zulip.tmpl.j2"
)
default_recap_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "zulip_recap.tmpl.j2"
)
default_recap_header_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "zulip_recap_header.tmpl.j2"
)
def _validate_event(self, event: MobilizonEvent) -> None: def _validate_event(self, event: MobilizonEvent) -> None:
text = event.description text = event.description

View File

@ -1,6 +1,7 @@
[default] [default]
local_state_dir = "/var/mobilizon_reshare" local_state_dir = "/var/mobilizon_reshare"
db_url = "sqlite:///var/mobilizon_reshare/events.db" log_dir = "@format {this.local_state_dir}"
db_url = "@format sqlite://{this.local_state_dir}/events.db"
locale= "en-us" locale= "en-us"
[default.source.mobilizon] [default.source.mobilizon]
@ -31,7 +32,7 @@ stream = "ext://sys.stderr"
level = "DEBUG" level = "DEBUG"
class = "logging.handlers.RotatingFileHandler" class = "logging.handlers.RotatingFileHandler"
formatter = "standard" formatter = "standard"
filename = "/var/log/mobilizon_reshare/mobilizon_reshare.log" filename = "@format {this.log_dir}/mobilizon_reshare.log"
maxBytes = 52428800 maxBytes = 52428800
backupCount = 500 backupCount = 500
encoding = "utf8" encoding = "utf8"

View File

@ -1,8 +1,7 @@
import logging import logging
from logging.config import dictConfig
from pathlib import Path from pathlib import Path
import pkg_resources import importlib
import urllib3.util import urllib3.util
from aerich import Command from aerich import Command
from tortoise import Tortoise from tortoise import Tortoise
@ -48,9 +47,9 @@ TORTOISE_ORM = get_tortoise_orm()
class MoReDB: class MoReDB:
def get_migration_location(self): def get_migration_location(self):
scheme = get_db_url().scheme scheme = get_db_url().scheme
return pkg_resources.resource_filename( scheme_ref = importlib.resources.files("mobilizon_reshare") / "migrations" / f"{scheme}"
"mobilizon_reshare", f"migrations/{scheme}" with importlib.resources.as_file(scheme_ref) as scheme_path:
) return scheme_path
async def _implement_db_changes(self): async def _implement_db_changes(self):
logging.info("Performing aerich migrations.") logging.info("Performing aerich migrations.")
@ -92,11 +91,7 @@ async def tear_down():
return await Tortoise.close_connections() return await Tortoise.close_connections()
async def init(init_logging=True): async def init():
if init_logging:
dictConfig(get_settings()["logging"])
# init storage # init storage
url = get_db_url() url = get_db_url()
if url.scheme == "sqlite": if url.scheme == "sqlite":

View File

@ -33,7 +33,7 @@ async def get_all_publishers() -> list[Publisher]:
async def prefetch_event_relations(queryset: QuerySet[Event]) -> list[Event]: async def prefetch_event_relations(queryset: QuerySet[Event]) -> list[Event]:
return ( return (
await queryset.prefetch_related("publications__publisher") await queryset.prefetch_related("publications__publisher", "publications__notifications")
.order_by("begin_datetime") .order_by("begin_datetime")
.distinct() .distinct()
) )
@ -46,6 +46,7 @@ async def prefetch_publication_relations(
await queryset.prefetch_related( await queryset.prefetch_related(
"publisher", "publisher",
"event", "event",
"notifications",
"event__publications", "event__publications",
"event__publications__publisher", "event__publications__publisher",
) )

View File

@ -9,11 +9,15 @@ from mobilizon_reshare.dataclasses.event import (
get_mobilizon_events_without_publications, get_mobilizon_events_without_publications,
) )
from mobilizon_reshare.models.event import Event from mobilizon_reshare.models.event import Event
from mobilizon_reshare.models.notification import Notification
from mobilizon_reshare.models.publication import Publication from mobilizon_reshare.models.publication import Publication
from mobilizon_reshare.models.publisher import Publisher from mobilizon_reshare.models.publisher import Publisher
from mobilizon_reshare.publishers.coordinators.event_publishing import ( from mobilizon_reshare.publishers.coordinators.event_publishing import (
EventPublicationReport, EventPublicationReport,
) )
from mobilizon_reshare.publishers.coordinators.event_publishing.notify import (
NotifierCoordinatorReport,
)
from mobilizon_reshare.publishers.coordinators.event_publishing.publish import ( from mobilizon_reshare.publishers.coordinators.event_publishing.publish import (
PublisherCoordinatorReport, PublisherCoordinatorReport,
) )
@ -64,6 +68,24 @@ async def save_publication_report(
await upsert_publication(publication_report, event) await upsert_publication(publication_report, event)
@atomic()
async def save_notification_report(
coordinator_report: NotifierCoordinatorReport,
) -> None:
"""
Store a notification process outcome
"""
for report in coordinator_report.reports:
publisher = await Publisher.filter(name=report.notification.publisher.name).first()
await Notification.create(
publication_id=report.notification.publication.id,
target_id=publisher.id,
status=report.status,
message=report.reason,
)
@atomic() @atomic()
async def create_unpublished_events( async def create_unpublished_events(
events_from_mobilizon: Iterable[MobilizonEvent], events_from_mobilizon: Iterable[MobilizonEvent],

View File

@ -3,6 +3,7 @@ import logging
from fastapi import FastAPI from fastapi import FastAPI
from fastapi_pagination import add_pagination from fastapi_pagination import add_pagination
from mobilizon_reshare.config.config import init_logging as init_log
from mobilizon_reshare.storage.db import init as init_db, get_db_url from mobilizon_reshare.storage.db import init as init_db, get_db_url
from mobilizon_reshare.web.backend.events.endpoints import ( from mobilizon_reshare.web.backend.events.endpoints import (
register_endpoints as register_event_endpoints, register_endpoints as register_event_endpoints,
@ -38,7 +39,9 @@ def init_endpoints(app):
@app.on_event("startup") @app.on_event("startup")
async def init_app(init_logging=True): async def init_app(init_logging=True):
if init_logging:
init_log()
check_database() check_database()
await init_db(init_logging=init_logging) await init_db()
init_endpoints(app) init_endpoints(app)
return app return app

279
poetry.lock generated
View File

@ -30,28 +30,30 @@ typing_extensions = ">=3.7.2"
[[package]] [[package]]
name = "alabaster" name = "alabaster"
version = "0.7.13" version = "0.7.16"
description = "A configurable sidebar-enabled Sphinx theme" description = "A light, configurable Sphinx theme"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.9"
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "3.6.2" version = "4.3.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations" description = "High level compatibility layer for multiple asynchronous event loop implementations"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6.2" python-versions = ">=3.8"
[package.dependencies] [package.dependencies]
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
idna = ">=2.8" idna = ">=2.8"
sniffio = ">=1.1" sniffio = ">=1.1"
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
[package.extras] [package.extras]
doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] doc = ["packaging", "Sphinx (>=7)", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] test = ["anyio", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (>=0.16,<0.22)"] trio = ["trio (>=0.23)"]
[[package]] [[package]]
name = "appdirs" name = "appdirs"
@ -73,28 +75,27 @@ python-versions = ">=3.6"
python-dateutil = ">=2.7.0" python-dateutil = ">=2.7.0"
[[package]] [[package]]
name = "asgiref" name = "async-timeout"
version = "3.6.0" version = "4.0.3"
description = "ASGI specs, helper code, and adapters" description = "Timeout context manager for asyncio programs"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[package.extras]
tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
[[package]] [[package]]
name = "asyncpg" name = "asyncpg"
version = "0.27.0" version = "0.29.0"
description = "An asyncio PostgreSQL driver" description = "An asyncio PostgreSQL driver"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7.0" python-versions = ">=3.8.0"
[package.dependencies]
async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""}
[package.extras] [package.extras]
dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "flake8 (>=5.0.4,<5.1.0)", "uvloop (>=0.15.3)"] docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=1.2.2)"]
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"]
test = ["flake8 (>=5.0.4,<5.1.0)", "uvloop (>=0.15.3)"]
[[package]] [[package]]
name = "asynctest" name = "asynctest"
@ -114,7 +115,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "23.1.0" version = "23.2.0"
description = "Classes Without Boilerplate" description = "Classes Without Boilerplate"
category = "dev" category = "dev"
optional = false optional = false
@ -125,16 +126,20 @@ cov = ["attrs", "coverage[toml] (>=5.3)"]
dev = ["attrs", "pre-commit"] dev = ["attrs", "pre-commit"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
tests = ["attrs", "zope-interface"] tests = ["attrs", "zope-interface"]
tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest-mypy-plugins", "pytest-xdist", "pytest (>=4.3.0)"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
tests-no-zope = ["attrs", "cloudpickle", "hypothesis", "pympler", "pytest-xdist", "pytest (>=4.3.0)"]
[[package]] [[package]]
name = "babel" name = "babel"
version = "2.12.1" version = "2.14.0"
description = "Internationalization utilities" description = "Internationalization utilities"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[package.extras]
dev = ["pytest (>=6.0)", "pytest-cov", "freezegun (>=1.0,<2.0)"]
[[package]] [[package]]
name = "beautifulsoup4" name = "beautifulsoup4"
version = "4.11.2" version = "4.11.2"
@ -152,7 +157,7 @@ lxml = ["lxml"]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2023.5.7" version = "2024.2.2"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
category = "main" category = "main"
optional = false optional = false
@ -160,7 +165,7 @@ python-versions = ">=3.6"
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.1.0" version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main" category = "main"
optional = false optional = false
@ -168,7 +173,7 @@ python-versions = ">=3.7.0"
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.3" version = "8.1.7"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
category = "main" category = "main"
optional = false optional = false
@ -187,11 +192,11 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7
[[package]] [[package]]
name = "coverage" name = "coverage"
version = "7.2.5" version = "7.4.3"
description = "Code coverage measurement for Python" description = "Code coverage measurement for Python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
[package.dependencies] [package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
@ -247,6 +252,17 @@ toml = ["toml"]
vault = ["hvac"] vault = ["hvac"]
yaml = ["ruamel.yaml"] yaml = ["ruamel.yaml"]
[[package]]
name = "exceptiongroup"
version = "1.2.0"
description = "Backport of PEP 654 (exception groups)"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
test = ["pytest (>=6)"]
[[package]] [[package]]
name = "facebook-sdk" name = "facebook-sdk"
version = "3.1.0" version = "3.1.0"
@ -260,7 +276,7 @@ requests = "*"
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.85.2" version = "0.92.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
category = "main" category = "main"
optional = false optional = false
@ -268,17 +284,17 @@ python-versions = ">=3.7"
[package.dependencies] [package.dependencies]
pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0"
starlette = "0.20.4" starlette = ">=0.25.0,<0.26.0"
[package.extras] [package.extras]
all = ["email-validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"]
doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.7.0)"] doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.8.0)"]
test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest-cov (>=2.12.0,<5.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<=1.4.41)", "types-orjson (==3.6.2)", "types-ujson (==5.5.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.10.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.6.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
[[package]] [[package]]
name = "fastapi-pagination" name = "fastapi-pagination"
version = "0.11.4" version = "0.12.3"
description = "FastAPI pagination" description = "FastAPI pagination"
category = "main" category = "main"
optional = false optional = false
@ -289,20 +305,21 @@ fastapi = ">=0.80.0"
pydantic = ">=1.9.1" pydantic = ">=1.9.1"
[package.extras] [package.extras]
sqlalchemy = ["SQLAlchemy (>=1.3.20)", "sqlakeyset (>=1.0.1659142803,<2.0.0)"] sqlalchemy = ["SQLAlchemy (>=1.3.20)", "sqlakeyset (>=2.0.1680321678,<3.0.0)"]
asyncpg = ["SQLAlchemy (>=1.3.20)", "asyncpg (>=0.24.0)"] asyncpg = ["SQLAlchemy (>=1.3.20)", "asyncpg (>=0.24.0)"]
all = ["SQLAlchemy (>=1.3.20)", "databases (>=0.6.0)", "orm (>=0.3.1)", "tortoise-orm (>=0.16.18,<0.20.0)", "asyncpg (>=0.24.0)", "ormar (>=0.11.2)", "django (<5.0.0)", "piccolo (>=0.89,<0.106)", "motor (>=2.5.1,<4.0.0)", "mongoengine (>=0.23.1,<0.27.0)", "sqlmodel (>=0.0.8,<0.0.9)", "pony (>=0.7.16,<0.8.0)", "beanie (>=1.11.9,<2.0.0)", "sqlakeyset (>=1.0.1659142803,<2.0.0)", "scylla-driver (>=3.25.6,<4.0.0)"] all = ["SQLAlchemy (>=1.3.20)", "databases (>=0.6.0)", "orm (>=0.3.1)", "tortoise-orm (>=0.16.18,<0.20.0)", "asyncpg (>=0.24.0)", "ormar (>=0.11.2)", "django (<5.0.0)", "piccolo (>=0.89,<0.112)", "motor (>=2.5.1,<4.0.0)", "mongoengine (>=0.23.1,<0.28.0)", "sqlmodel (>=0.0.8,<0.0.9)", "pony (>=0.7.16,<0.8.0)", "beanie (>=1.11.9,<2.0.0)", "sqlakeyset (>=2.0.1680321678,<3.0.0)", "scylla-driver (>=3.25.6,<4.0.0)", "bunnet (>=1.1.0,<2.0.0)"]
databases = ["databases (>=0.6.0)"] databases = ["databases (>=0.6.0)"]
orm = ["databases (>=0.6.0)", "orm (>=0.3.1)"] orm = ["databases (>=0.6.0)", "orm (>=0.3.1)"]
django = ["databases (>=0.6.0)", "django (<5.0.0)"] django = ["databases (>=0.6.0)", "django (<5.0.0)"]
tortoise = ["tortoise-orm (>=0.16.18,<0.20.0)"] tortoise = ["tortoise-orm (>=0.16.18,<0.20.0)"]
ormar = ["ormar (>=0.11.2)"] ormar = ["ormar (>=0.11.2)"]
piccolo = ["piccolo (>=0.89,<0.106)"] piccolo = ["piccolo (>=0.89,<0.112)"]
motor = ["motor (>=2.5.1,<4.0.0)"] motor = ["motor (>=2.5.1,<4.0.0)"]
mongoengine = ["mongoengine (>=0.23.1,<0.27.0)"] mongoengine = ["mongoengine (>=0.23.1,<0.28.0)"]
sqlmodel = ["sqlmodel (>=0.0.8,<0.0.9)", "sqlakeyset (>=1.0.1659142803,<2.0.0)"] sqlmodel = ["sqlmodel (>=0.0.8,<0.0.9)", "sqlakeyset (>=2.0.1680321678,<3.0.0)"]
beanie = ["beanie (>=1.11.9,<2.0.0)"] beanie = ["beanie (>=1.11.9,<2.0.0)"]
scylla-driver = ["scylla-driver (>=3.25.6,<4.0.0)"] scylla-driver = ["scylla-driver (>=3.25.6,<4.0.0)"]
bunnet = ["bunnet (>=1.1.0,<2.0.0)"]
[[package]] [[package]]
name = "h11" name = "h11"
@ -314,7 +331,7 @@ python-versions = ">=3.7"
[[package]] [[package]]
name = "httpcore" name = "httpcore"
version = "0.16.3" version = "0.17.3"
description = "A minimal low-level HTTP client." description = "A minimal low-level HTTP client."
category = "dev" category = "dev"
optional = false optional = false
@ -332,7 +349,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]] [[package]]
name = "httpx" name = "httpx"
version = "0.23.3" version = "0.24.1"
description = "The next generation HTTP client." description = "The next generation HTTP client."
category = "dev" category = "dev"
optional = false optional = false
@ -340,19 +357,19 @@ python-versions = ">=3.7"
[package.dependencies] [package.dependencies]
certifi = "*" certifi = "*"
httpcore = ">=0.15.0,<0.17.0" httpcore = ">=0.15.0,<0.18.0"
rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} idna = "*"
sniffio = "*" sniffio = "*"
[package.extras] [package.extras]
brotli = ["brotli", "brotlicffi"] brotli = ["brotli", "brotlicffi"]
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"] http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"] socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.4" version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
category = "main" category = "main"
optional = false optional = false
@ -366,22 +383,6 @@ category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "importlib-metadata"
version = "6.6.0"
description = "Read metadata from Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"]
perf = ["ipython"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "pytest-flake8", "importlib-resources (>=1.3)"]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.0.0" version = "2.0.0"
@ -400,7 +401,7 @@ python-versions = ">=3.6.2,<4.0"
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.2" version = "3.1.3"
description = "A very fast and expressive template engine." description = "A very fast and expressive template engine."
category = "main" category = "main"
optional = false optional = false
@ -414,21 +415,21 @@ i18n = ["Babel (>=2.7)"]
[[package]] [[package]]
name = "lxml" name = "lxml"
version = "4.9.2" version = "5.1.0"
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" python-versions = ">=3.6"
[package.extras] [package.extras]
cssselect = ["cssselect (>=0.7)"] cssselect = ["cssselect (>=0.7)"]
html5 = ["html5lib"] html5 = ["html5lib"]
htmlsoup = ["beautifulsoup4"] htmlsoup = ["beautifulsoup4"]
source = ["Cython (>=0.29.7)"] source = ["Cython (>=3.0.7)"]
[[package]] [[package]]
name = "markdownify" name = "markdownify"
version = "0.10.3" version = "0.11.6"
description = "Convert HTML to markdown." description = "Convert HTML to markdown."
category = "main" category = "main"
optional = false optional = false
@ -440,7 +441,7 @@ six = ">=1.15,<2"
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "2.1.2" version = "2.1.5"
description = "Safely add untrusted strings to HTML/XML markup." description = "Safely add untrusted strings to HTML/XML markup."
category = "main" category = "main"
optional = false optional = false
@ -461,7 +462,7 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "23.1" version = "23.2"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "dev" category = "dev"
optional = false optional = false
@ -469,11 +470,11 @@ python-versions = ">=3.7"
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.0.0" version = "1.4.0"
description = "plugin and hook calling mechanisms for python" description = "plugin and hook calling mechanisms for python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.8"
[package.extras] [package.extras]
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
@ -500,7 +501,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "1.10.7" version = "1.10.14"
description = "Data validation and settings management using python type hints" description = "Data validation and settings management using python type hints"
category = "main" category = "main"
optional = false optional = false
@ -515,7 +516,7 @@ email = ["email-validator (>=1.0.3)"]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.15.1" version = "2.17.2"
description = "Pygments is a syntax highlighting package written in Python." description = "Pygments is a syntax highlighting package written in Python."
category = "dev" category = "dev"
optional = false optional = false
@ -523,6 +524,7 @@ python-versions = ">=3.7"
[package.extras] [package.extras]
plugins = ["importlib-metadata"] plugins = ["importlib-metadata"]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]] [[package]]
name = "pypika-tortoise" name = "pypika-tortoise"
@ -595,7 +597,7 @@ pytest = ">=3.2.5"
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.8.2" version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module" description = "Extensions to the standard Python datetime module"
category = "main" category = "main"
optional = false optional = false
@ -606,7 +608,7 @@ six = ">=1.5"
[[package]] [[package]]
name = "python-slugify" name = "python-slugify"
version = "8.0.1" version = "8.0.4"
description = "A Python slugify application that also handles Unicode" description = "A Python slugify application that also handles Unicode"
category = "dev" category = "dev"
optional = false optional = false
@ -621,7 +623,7 @@ unidecode = ["Unidecode (>=1.1.1)"]
[[package]] [[package]]
name = "pytz" name = "pytz"
version = "2023.3" version = "2024.1"
description = "World timezone definitions, modern and historical" description = "World timezone definitions, modern and historical"
category = "main" category = "main"
optional = false optional = false
@ -662,33 +664,20 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
[[package]] [[package]]
name = "responses" name = "responses"
version = "0.13.4" version = "0.22.0"
description = "A utility library for mocking out the `requests` Python library." description = "A utility library for mocking out the `requests` Python library."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=3.7"
[package.dependencies] [package.dependencies]
requests = ">=2.0" requests = ">=2.22.0,<3.0"
six = "*" toml = "*"
types-toml = "*"
urllib3 = ">=1.25.10" urllib3 = ">=1.25.10"
[package.extras] [package.extras]
tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "types-mock", "types-requests", "types-six", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"] tests = ["pytest (>=7.0.0)", "coverage (>=6.0.0)", "pytest-cov", "pytest-asyncio", "pytest-httpserver", "flake8", "types-requests", "mypy"]
[[package]]
name = "rfc3986"
version = "1.5.0"
description = "Validating URI References per RFC 3986"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
[package.extras]
idna2008 = ["idna"]
[[package]] [[package]]
name = "six" name = "six"
@ -700,7 +689,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.0" version = "1.3.1"
description = "Sniff out which async library your code is running under" description = "Sniff out which async library your code is running under"
category = "main" category = "main"
optional = false optional = false
@ -716,11 +705,11 @@ python-versions = "*"
[[package]] [[package]]
name = "soupsieve" name = "soupsieve"
version = "2.4.1" version = "2.5"
description = "A modern CSS selector implementation for Beautiful Soup." description = "A modern CSS selector implementation for Beautiful Soup."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
[[package]] [[package]]
name = "sphinx" name = "sphinx"
@ -736,7 +725,6 @@ babel = ">=1.3"
colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""}
docutils = ">=0.14,<0.18" docutils = ">=0.14,<0.18"
imagesize = "*" imagesize = "*"
importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
Jinja2 = ">=2.3" Jinja2 = ">=2.3"
packaging = "*" packaging = "*"
Pygments = ">=2.0" Pygments = ">=2.0"
@ -771,7 +759,7 @@ type_comments = ["typed-ast (>=1.4.0)"]
[[package]] [[package]]
name = "sphinx-material" name = "sphinx-material"
version = "0.0.35" version = "0.0.36"
description = "Material sphinx theme" description = "Material sphinx theme"
category = "dev" category = "dev"
optional = false optional = false
@ -785,42 +773,45 @@ python-slugify = {version = "*", extras = ["unidecode"]}
sphinx = ">=2.0" sphinx = ">=2.0"
[package.extras] [package.extras]
dev = ["black (==19.10b0)"] dev = ["black (==22.12.0)"]
[[package]] [[package]]
name = "sphinxcontrib-applehelp" name = "sphinxcontrib-applehelp"
version = "1.0.4" version = "1.0.8"
description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.9"
[package.extras] [package.extras]
lint = ["flake8", "mypy", "docutils-stubs"] lint = ["flake8", "mypy", "docutils-stubs"]
standalone = ["Sphinx (>=5)"]
test = ["pytest"] test = ["pytest"]
[[package]] [[package]]
name = "sphinxcontrib-devhelp" name = "sphinxcontrib-devhelp"
version = "1.0.2" version = "1.0.6"
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.9"
[package.extras] [package.extras]
lint = ["flake8", "mypy", "docutils-stubs"] lint = ["flake8", "mypy", "docutils-stubs"]
standalone = ["Sphinx (>=5)"]
test = ["pytest"] test = ["pytest"]
[[package]] [[package]]
name = "sphinxcontrib-htmlhelp" name = "sphinxcontrib-htmlhelp"
version = "2.0.1" version = "2.0.5"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.9"
[package.extras] [package.extras]
lint = ["flake8", "mypy", "docutils-stubs"] lint = ["flake8", "mypy", "docutils-stubs"]
standalone = ["Sphinx (>=5)"]
test = ["pytest", "html5lib"] test = ["pytest", "html5lib"]
[[package]] [[package]]
@ -848,31 +839,33 @@ six = ">=1.5.2"
[[package]] [[package]]
name = "sphinxcontrib-qthelp" name = "sphinxcontrib-qthelp"
version = "1.0.3" version = "1.0.7"
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.9"
[package.extras] [package.extras]
lint = ["flake8", "mypy", "docutils-stubs"] lint = ["flake8", "mypy", "docutils-stubs"]
standalone = ["Sphinx (>=5)"]
test = ["pytest"] test = ["pytest"]
[[package]] [[package]]
name = "sphinxcontrib-serializinghtml" name = "sphinxcontrib-serializinghtml"
version = "1.1.5" version = "1.1.10"
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.9"
[package.extras] [package.extras]
lint = ["flake8", "mypy", "docutils-stubs"] lint = ["flake8", "mypy", "docutils-stubs"]
standalone = ["Sphinx (>=5)"]
test = ["pytest"] test = ["pytest"]
[[package]] [[package]]
name = "starlette" name = "starlette"
version = "0.20.4" version = "0.25.0"
description = "The little ASGI library that shines." description = "The little ASGI library that shines."
category = "main" category = "main"
optional = false optional = false
@ -880,10 +873,9 @@ python-versions = ">=3.7"
[package.dependencies] [package.dependencies]
anyio = ">=3.4.0,<5" anyio = ">=3.4.0,<5"
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[package.extras] [package.extras]
full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"]
[[package]] [[package]]
name = "text-unidecode" name = "text-unidecode"
@ -911,7 +903,7 @@ python-versions = ">=3.7"
[[package]] [[package]]
name = "tomlkit" name = "tomlkit"
version = "0.11.8" version = "0.12.4"
description = "Style preserving TOML library" description = "Style preserving TOML library"
category = "main" category = "main"
optional = false optional = false
@ -938,7 +930,7 @@ asyncmy = ["asyncmy (>=0.2.5,<0.3.0)"]
asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"] asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"]
asyncpg = ["asyncpg"] asyncpg = ["asyncpg"]
accel = ["ciso8601", "orjson", "uvloop"] accel = ["ciso8601", "orjson", "uvloop"]
psycopg = ["psycopg[pool,binary] (==3.0.12)"] psycopg = ["psycopg[binary,pool] (==3.0.12)"]
[[package]] [[package]]
name = "tweepy" name = "tweepy"
@ -960,17 +952,25 @@ docs = ["myst-parser (==0.15.2)", "readthedocs-sphinx-search (==0.1.1)", "sphinx
socks = ["requests[socks] (>=2.27.0,<3)"] socks = ["requests[socks] (>=2.27.0,<3)"]
test = ["vcrpy (>=1.10.3)"] test = ["vcrpy (>=1.10.3)"]
[[package]]
name = "types-toml"
version = "0.10.8.7"
description = "Typing stubs for toml"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.5.0" version = "4.10.0"
description = "Backported and Experimental Type Hints for Python 3.7+" description = "Backported and Experimental Type Hints for Python 3.8+"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
[[package]] [[package]]
name = "unidecode" name = "unidecode"
version = "1.3.6" version = "1.3.8"
description = "ASCII transliterations of Unicode text" description = "ASCII transliterations of Unicode text"
category = "dev" category = "dev"
optional = false optional = false
@ -978,49 +978,37 @@ python-versions = ">=3.5"
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "1.26.15" version = "1.26.18"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.extras] [package.extras]
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] brotli = ["brotlicffi (>=0.8.0)", "brotli (==1.0.9)", "brotlipy (>=0.6.0)", "brotli (>=1.0.9)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.17.6" version = "0.23.2"
description = "The lightning-fast ASGI server." description = "The lightning-fast ASGI server."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
[package.dependencies] [package.dependencies]
asgiref = ">=3.4.0"
click = ">=7.0" click = ">=7.0"
h11 = ">=0.8" h11 = ">=0.8"
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
[package.extras] [package.extras]
standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
[[package]]
name = "zipp"
version = "3.15.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "jaraco.functools", "more-itertools", "big-o", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "pytest-flake8"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.10"
content-hash = "bfc1512cd6f94fdc013dbebcf70c0077093b2bc3126c8573c35a3569445f948d" content-hash = "a81b518a3185eb0b2c42e6c927dab3f46dd91eb749cb52a1fbd0a462d51b685c"
[metadata.files] [metadata.files]
aerich = [] aerich = []
@ -1029,7 +1017,7 @@ alabaster = []
anyio = [] anyio = []
appdirs = [] appdirs = []
arrow = [] arrow = []
asgiref = [] async-timeout = []
asyncpg = [] asyncpg = []
asynctest = [] asynctest = []
atomicwrites = [] atomicwrites = []
@ -1045,6 +1033,7 @@ css-html-js-minify = []
dictdiffer = [] dictdiffer = []
docutils = [] docutils = []
dynaconf = [] dynaconf = []
exceptiongroup = []
facebook-sdk = [] facebook-sdk = []
fastapi = [] fastapi = []
fastapi-pagination = [] fastapi-pagination = []
@ -1053,7 +1042,6 @@ httpcore = []
httpx = [] httpx = []
idna = [] idna = []
imagesize = [] imagesize = []
importlib-metadata = []
iniconfig = [] iniconfig = []
iso8601 = [] iso8601 = []
jinja2 = [] jinja2 = []
@ -1078,7 +1066,6 @@ pytz = []
requests = [] requests = []
requests-oauthlib = [] requests-oauthlib = []
responses = [] responses = []
rfc3986 = []
six = [] six = []
sniffio = [] sniffio = []
snowballstemmer = [] snowballstemmer = []
@ -1100,8 +1087,8 @@ tomli = []
tomlkit = [] tomlkit = []
tortoise-orm = [] tortoise-orm = []
tweepy = [] tweepy = []
types-toml = []
typing-extensions = [] typing-extensions = []
unidecode = [] unidecode = []
urllib3 = [] urllib3 = []
uvicorn = [] uvicorn = []
zipp = []

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "mobilizon-reshare" name = "mobilizon-reshare"
version = "0.3.2" version = "0.3.6"
description = "A suite to reshare Mobilizon events on a broad selection of platforms" description = "A suite to reshare Mobilizon events on a broad selection of platforms"
readme = "README.md" readme = "README.md"
homepage = "https://github.com/Tech-Workers-Coalition-Italia/mobilizon-reshare" homepage = "https://github.com/Tech-Workers-Coalition-Italia/mobilizon-reshare"
@ -9,7 +9,7 @@ authors = ["Simone Robutti <simone.robutti@protonmail.com>"]
license = "Coopyleft" license = "Coopyleft"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = "^3.10"
dynaconf = "~3.1" dynaconf = "~3.1"
tortoise-orm = {extras = ["asyncpg"], version = "~0.19"} tortoise-orm = {extras = ["asyncpg"], version = "~0.19"}
aiosqlite = "~0.17" aiosqlite = "~0.17"
@ -18,17 +18,17 @@ requests = "~2.28"
arrow = "~1.1" arrow = "~1.1"
click = "~8.1" click = "~8.1"
beautifulsoup4 = "~4.11" beautifulsoup4 = "~4.11"
markdownify = "~0.10" markdownify = "~0.11"
appdirs = "~1.4" appdirs = "~1.4"
tweepy = "~4.13" tweepy = "~4.13"
facebook-sdk = "~3.1" facebook-sdk = "~3.1"
aerich = "~0.6" aerich = "~0.6"
fastapi = "~0.85" fastapi = "~0.92"
uvicorn = "~0.17" uvicorn = "~0.23"
fastapi-pagination = "^0.11.0" fastapi-pagination = "~0.12"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
responses = "~0.13" responses = "~0.22"
pytest-asyncio = "~0.15" pytest-asyncio = "~0.15"
asynctest = "~0.13" asynctest = "~0.13"
pytest = "~6.2" pytest = "~6.2"
@ -38,7 +38,7 @@ Sphinx = "~4.4"
sphinxcontrib-napoleon = "~0.7" sphinxcontrib-napoleon = "~0.7"
sphinx-material = "~0.0" sphinx-material = "~0.0"
sphinx-autodoc-typehints = "~1.17" sphinx-autodoc-typehints = "~1.17"
httpx = "~0.23" httpx = "~0.24"

View File

@ -2,8 +2,9 @@
debug = false debug = false
default = true default = true
local_state_dir = "/var/lib/mobilizon-reshare" local_state_dir = "/var/lib/mobilizon-reshare"
#db_path = "@format {this.local_state_dir}/events.db" #db_url = "@format sqlite://{this.local_state_dir}/events.db"
db_url = "@format postgres://mobilizon_reshare:mobilizon_reshare@db:5432/mobilizon_reshare" db_url = "@format postgres://mobilizon_reshare:mobilizon_reshare@db:5432/mobilizon_reshare"
locale = "en-uk"
[default.source.mobilizon] [default.source.mobilizon]
url="https://some-mobilizon.com/api" url="https://some-mobilizon.com/api"
@ -28,6 +29,15 @@ class = "logging.StreamHandler"
formatter = "standard" formatter = "standard"
stream = "ext://sys.stderr" stream = "ext://sys.stderr"
[default.logging.handlers.file]
level = "INFO"
class = "logging.handlers.RotatingFileHandler"
formatter = "standard"
filename = "@format {this.local_state_dir}/mobilizon_reshare.log"
maxBytes = 52428800
backupCount = 500
encoding = "utf8"
[default.logging.root] [default.logging.root]
level = "DEBUG" level = "INFO"
handlers = ['console'] handlers = ['console', 'file']

View File

@ -1,6 +1,10 @@
#!/bin/sh #!/bin/sh
set -eu set -e
guix time-machine -C channels-lock.scm -- build -f guix.scm if [ "$1" = "--release" ] || [ "$1" = "-r" ]; then
with_input="--with-input=mobilizon-reshare.git=mobilizon-reshare"
fi
guix time-machine -C channels-lock.scm -- pack -L . -f docker -S /opt/bin=bin --save-provenance --root=docker-image.tar.gz --entry-point=bin/scheduler.py mobilizon-reshare-scheduler guix time-machine -C channels-lock.scm -- build -L . ${with_input} mobilizon-reshare-scheduler
guix time-machine -C channels-lock.scm -- pack -L . ${with_input} -f docker -S /opt/bin=bin --save-provenance --root=docker-image.tar.gz --entry-point=bin/scheduler.py mobilizon-reshare-scheduler python

29
scripts/generate_badges.sh Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -eu
project_root="$(cd "$(dirname $(dirname "$0"))" && pwd)"
get_version () {
cat "${project_root}/mobilizon_reshare/VERSION"
}
python -m pybadges \
--left-text="python" \
--right-text="3.10, 3.11" \
--whole-link="https://www.python.org/" \
--browser \
--logo='https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/python.svg' \
--embed-logo=yes
python -m pybadges \
--left-text="pypi" \
--right-text="$(get_version)" \
--whole-link="https://pypi.org/project/mobilizon-reshare/" \
--browser
python -m pybadges \
--left-text="LICENSE" \
--right-text="Coopyleft" \
--whole-link="https://github.com/Tech-Workers-Coalition-Italia/mobilizon-reshare/blob/master/LICENSE" \
--browser

View File

@ -5,7 +5,7 @@ set -e
export MOBILIZON_RESHARE_LOG_DIR="/tmp" export MOBILIZON_RESHARE_LOG_DIR="/tmp"
export MOBILIZON_RESHARE_LOCAL_STATE_DIR="/tmp" export MOBILIZON_RESHARE_LOCAL_STATE_DIR="/tmp"
export SECRETS_FOR_DYNACONF="$(pwd)/.secrets.toml" export SECRETS_FOR_DYNACONF="$(pwd)/.secrets.toml"
export SETTINGS_FILE_FOR_DYNACONF="$(pwd)/settings.toml" export SETTINGS_FILE_FOR_DYNACONF="$(pwd)/mobilizon_reshare/settings.toml"
export ENV_FOR_DYNACONF="production" export ENV_FOR_DYNACONF="production"
poetry run mobilizon-reshare "$@" poetry run mobilizon-reshare "$@"

View File

@ -14,16 +14,27 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from mobilizon_reshare.cli import _safe_execution from mobilizon_reshare.cli import _safe_execution
from mobilizon_reshare.cli.commands.recap.main import recap from mobilizon_reshare.cli.commands.recap.main import recap as recap_main
from mobilizon_reshare.cli.commands.start.main import start from mobilizon_reshare.cli.commands.start.main import start as start_main
from mobilizon_reshare.config.command import CommandConfig
sched = AsyncIOScheduler() sched = AsyncIOScheduler()
config = CommandConfig(dry_run=False)
async def start():
await start_main(config)
async def recap():
await recap_main(config)
# Runs "start" from Monday to Friday every 15 mins # Runs "start" from Monday to Friday every 15 mins
sched.add_job( sched.add_job(
partial(_safe_execution, start), partial(_safe_execution, start),
CronTrigger.from_crontab( CronTrigger.from_crontab(
os.environ.get("MOBILIZON_RESHARE_INTERVAL", "*/15 10-18 * * 0-4") os.environ.get("MOBILIZON_RESHARE_INTERVAL", "*/15 10-18 * * 1-4")
), ),
) )
# Runs "recap" once a week # Runs "recap" once a week

View File

@ -8,7 +8,6 @@ import mobilizon_reshare.publishers
import mobilizon_reshare.storage.query.read import mobilizon_reshare.storage.query.read
from mobilizon_reshare.models.publisher import Publisher from mobilizon_reshare.models.publisher import Publisher
import mobilizon_reshare.main.recap import mobilizon_reshare.main.recap
from mobilizon_reshare.publishers.coordinators.event_publishing import notify
from tests import today from tests import today
from tests.conftest import event_1, event_0 from tests.conftest import event_1, event_0
@ -138,15 +137,41 @@ async def mock_notifier_config(monkeypatch, publisher_class, mock_formatter_clas
return mock_formatter_class return mock_formatter_class
monkeypatch.setattr( monkeypatch.setattr(
notify, "get_notifier_class", _mock_notifier_class, mobilizon_reshare.publishers.coordinators.event_publishing.notify,
"get_notifier_class",
_mock_notifier_class,
)
monkeypatch.setattr(
mobilizon_reshare.publishers.coordinators.event_publishing.notify,
"get_formatter_class",
_mock_format_class,
)
monkeypatch.setattr(
mobilizon_reshare.publishers.coordinators.event_publishing.notify,
"get_notifier_class",
_mock_notifier_class,
) )
monkeypatch.setattr( monkeypatch.setattr(
mobilizon_reshare.publishers.platforms.platform_mapping, mobilizon_reshare.publishers.platforms.platform_mapping,
"get_formatter_class", "get_formatter_class",
_mock_format_class, _mock_format_class,
) )
monkeypatch.setattr(
mobilizon_reshare.publishers.coordinators.event_publishing.notify,
"get_formatter_class",
_mock_format_class,
)
monkeypatch.setattr(notify, "get_active_notifiers", _mock_active_notifier) monkeypatch.setattr(
mobilizon_reshare.publishers.coordinators.event_publishing.notify,
"get_active_notifiers",
_mock_active_notifier,
)
monkeypatch.setattr(
mobilizon_reshare.config.notifiers,
"get_active_notifiers",
lambda s: [],
)
@pytest.fixture @pytest.fixture

View File

@ -1,13 +1,15 @@
from logging import DEBUG from logging import DEBUG
from uuid import UUID
import pytest import pytest
from mobilizon_reshare.dataclasses import EventPublicationStatus from mobilizon_reshare.dataclasses import EventPublicationStatus
from mobilizon_reshare.dataclasses import MobilizonEvent from mobilizon_reshare.dataclasses import MobilizonEvent
from mobilizon_reshare.main.publish import select_and_publish, publish_event from mobilizon_reshare.main.publish import select_and_publish, publish_by_mobilizon_id
from mobilizon_reshare.models.notification import NotificationStatus, Notification
from mobilizon_reshare.models.event import Event from mobilizon_reshare.models.event import Event
from mobilizon_reshare.models.publication import PublicationStatus from mobilizon_reshare.models.publication import PublicationStatus
from mobilizon_reshare.storage.query.read import get_all_publications from mobilizon_reshare.storage.query.read import get_all_publications, get_event
from tests.conftest import event_0, event_1 from tests.conftest import event_0, event_1
one_unpublished_event_specification = { one_unpublished_event_specification = {
@ -102,7 +104,9 @@ async def test_publish_event(
await generate_models(one_unpublished_event_specification) await generate_models(one_unpublished_event_specification)
with caplog.at_level(DEBUG): with caplog.at_level(DEBUG):
# calling mobilizon-reshare publish -E <UUID> -p <platform> # calling mobilizon-reshare publish -E <UUID> -p <platform>
report = await publish_event(event_0, command_config, publishers) report = await publish_by_mobilizon_id(
event_0.mobilizon_id, command_config, publishers
)
assert report is not None assert report is not None
assert report.successful assert report.successful
@ -112,3 +116,50 @@ async def test_publish_event(
assert len(publications) == len(expected) assert len(publications) == len(expected)
assert all(p.status == PublicationStatus.COMPLETED for p in publications) assert all(p.status == PublicationStatus.COMPLETED for p in publications)
assert {p.publisher.name for p in publications} == expected assert {p.publisher.name for p in publications} == expected
@pytest.mark.asyncio
@pytest.mark.parametrize(
"publisher_class", [pytest.lazy_fixture("mock_publisher_invalid_class")]
)
async def test_notify_publisher_failure(
caplog,
mock_publisher_config,
message_collector,
generate_models,
mock_notifier_config,
command_config,
):
await generate_models(one_unpublished_event_specification)
with caplog.at_level(DEBUG):
# calling the publish command
result = await select_and_publish(command_config)
assert not result.successful
assert len(result.reports) == 1
assert result.reports[0].published_content is None
# since the db contains at least one event, this has to be picked and published
event_model = await get_event(UUID(int=0))
# it should create a publication for each publisher and a notification for each notifier
publications = event_model.publications
assert len(publications) == 1, publications
publication = publications[0]
notifications: list[Notification] = list(publications[0].notifications)
assert len(notifications) == 2, notifications
# all the publications for event should be saved as FAILED
for n in notifications:
assert n.status == NotificationStatus.COMPLETED
assert (
n.message
== f"Publication {publication.id} failed with status: FAILED.\nReason: credentials error"
"\nPublisher: mock\nEvent: event_0"
)
# the derived status for the event should be FAILED
assert (
MobilizonEvent.from_model(event_model).status
== EventPublicationStatus.FAILED
)

View File

@ -122,16 +122,16 @@ async def test_retry_publication_missing(
async def test_event_retry_failure( async def test_event_retry_failure(
event_with_failed_publication, event_with_failed_publication,
mock_publisher_config, mock_publisher_config,
mock_notifier_config,
failed_publication: Publication, failed_publication: Publication,
caplog,
): ):
with caplog.at_level(ERROR): report = await retry_event(event_with_failed_publication.mobilizon_id)
await retry_event(event_with_failed_publication.mobilizon_id) assert len(report.reports) == 1
assert ( assert (
f"Publication {failed_publication.id} failed with status: 0.\nReason: credentials error" f"Publication {failed_publication.id} failed with status: FAILED.\nReason: credentials error"
in caplog.text in report.reports[0].get_failure_message()
) )
p = await Publication.filter(id=failed_publication.id).first() p = await Publication.filter(id=failed_publication.id).first()
assert p.status == PublicationStatus.FAILED, p.id assert p.status == PublicationStatus.FAILED, p.id
@ -144,15 +144,17 @@ async def test_event_retry_failure(
async def test_publication_retry_failure( async def test_publication_retry_failure(
event_with_failed_publication, event_with_failed_publication,
mock_publisher_config, mock_publisher_config,
mock_notifier_config,
failed_publication: Publication, failed_publication: Publication,
caplog, caplog,
): ):
with caplog.at_level(ERROR): with caplog.at_level(ERROR):
await retry_publication(failed_publication.id) report = await retry_publication(failed_publication.id)
assert len(report.reports) == 1
assert ( assert (
f"Publication {failed_publication.id} failed with status: 0.\nReason: credentials error" f"Publication {failed_publication.id} failed with status: FAILED.\nReason: credentials error"
in caplog.text in report.reports[0].get_failure_message()
) )
p = await Publication.filter(id=failed_publication.id).first() p = await Publication.filter(id=failed_publication.id).first()
assert p.status == PublicationStatus.FAILED, p.id assert p.status == PublicationStatus.FAILED, p.id

View File

@ -186,7 +186,7 @@ async def test_start_publisher_failure(
assert "Event to publish found" in caplog.text assert "Event to publish found" in caplog.text
assert message_collector == [ assert message_collector == [
f"Publication {p.id} failed with status: 0." f"Publication {p.id} failed with status: FAILED."
f"\nReason: credentials error\nPublisher: mock\nEvent: test event" f"\nReason: credentials error\nPublisher: mock\nEvent: test event"
for p in publications for p in publications
for _ in range(2) for _ in range(2)

View File

@ -51,7 +51,7 @@ def generate_event_status(published):
def generate_notification_status(published): def generate_notification_status(published):
return NotificationStatus.COMPLETED if published else NotificationStatus.WAITING return NotificationStatus.COMPLETED if published else NotificationStatus.FAILED
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
@ -421,6 +421,12 @@ def mock_publisher_valid(message_collector, mock_publisher_class):
return mock_publisher_class() return mock_publisher_class()
@pytest.fixture
def mock_zulip_publisher(message_collector, mock_zulip_publisher_class):
return mock_zulip_publisher_class()
@pytest.fixture @pytest.fixture
def mobilizon_url(): def mobilizon_url():
return get_settings()["source"]["mobilizon"]["url"] return get_settings()["source"]["mobilizon"]["url"]

View File

@ -8,5 +8,5 @@ async def test_notification_create(notification_model_generator):
notification_model = notification_model_generator() notification_model = notification_model_generator()
await notification_model.save() await notification_model.save()
notification_db = await Notification.all().first() notification_db = await Notification.all().first()
assert notification_db.status == NotificationStatus.WAITING assert notification_db.status == NotificationStatus.FAILED
assert notification_db.message == "message_1" assert notification_db.message == "message_1"

View File

@ -78,3 +78,21 @@ def mock_publisher_invalid_response(message_collector):
pass pass
return MockPublisher() return MockPublisher()
@pytest.fixture
def mock_zulip_publisher_invalid_response(message_collector):
class MockPublisher(AbstractPlatform):
name = "zulip"
def _send(self, message, event):
message_collector.append(message)
def _validate_response(self, response):
raise InvalidResponse("Invalid response")
def validate_credentials(self) -> None:
pass
return MockPublisher()

View File

@ -115,8 +115,12 @@ async def mock_publications(
@pytest.mark.parametrize("num_publications", [2]) @pytest.mark.parametrize("num_publications", [2])
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_publication_coordinator_run_success(mock_publications,): async def test_publication_coordinator_run_success(
coordinator = PublisherCoordinator(publications=mock_publications,) mock_publications,
):
coordinator = PublisherCoordinator(
publications=mock_publications,
)
report = coordinator.run() report = coordinator.run()
assert len(report.reports) == 2 assert len(report.reports) == 2
assert report.successful, "\n".join(map(lambda rep: rep.reason, report.reports)) assert report.successful, "\n".join(map(lambda rep: rep.reason, report.reports))
@ -173,12 +177,12 @@ async def test_publication_coordinator_run_failure_response(
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_notifier_coordinator_publication_failed( async def test_notifier_coordinator_publication_failed(
mock_publisher_valid, failure_report mock_zulip_publisher, failure_report
): ):
mock_send = MagicMock() mock_send = MagicMock()
mock_publisher_valid._send = mock_send mock_zulip_publisher._send = mock_send
coordinator = PublicationFailureNotifiersCoordinator( coordinator = PublicationFailureNotifiersCoordinator(
failure_report, [mock_publisher_valid, mock_publisher_valid] failure_report, [mock_zulip_publisher, mock_zulip_publisher]
) )
coordinator.notify_failure() coordinator.notify_failure()
@ -188,18 +192,18 @@ async def test_notifier_coordinator_publication_failed(
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_notifier_coordinator_error( async def test_notifier_coordinator_error(
failure_report, mock_publisher_invalid_response, caplog failure_report, mock_zulip_publisher_invalid_response, caplog
): ):
mock_send = MagicMock() mock_send = MagicMock()
mock_publisher_invalid_response._send = mock_send mock_zulip_publisher_invalid_response._send = mock_send
coordinator = PublicationFailureNotifiersCoordinator( coordinator = PublicationFailureNotifiersCoordinator(
failure_report, failure_report,
[mock_publisher_invalid_response, mock_publisher_invalid_response], [mock_zulip_publisher_invalid_response, mock_zulip_publisher_invalid_response],
) )
with caplog.at_level(logging.CRITICAL): with caplog.at_level(logging.CRITICAL):
coordinator.notify_failure() coordinator.notify_failure()
assert "Failed to send" in caplog.text assert "Failed to notify failure of" in caplog.text
assert failure_report.get_failure_message() in caplog.text assert failure_report.get_failure_message() in caplog.text
# 4 = 2 reports * 2 notifiers # 4 = 2 reports * 2 notifiers
assert mock_send.call_count == 2 assert mock_send.call_count == 2

View File

@ -9,10 +9,15 @@ from mobilizon_reshare.dataclasses.event import (
get_mobilizon_events_with_status, get_mobilizon_events_with_status,
get_mobilizon_events_without_publications, get_mobilizon_events_without_publications,
) )
from mobilizon_reshare.storage.query.read import (
get_all_events,
get_event,
)
from mobilizon_reshare.dataclasses.publication import build_publications_for_event from mobilizon_reshare.dataclasses.publication import build_publications_for_event
from mobilizon_reshare.models.publication import PublicationStatus from mobilizon_reshare.models.publication import PublicationStatus
from mobilizon_reshare.storage.query.read import publications_with_status from mobilizon_reshare.storage.query.read import publications_with_status
from tests import today from tests import today
from tests.commands.test_publish import one_unpublished_event_specification
from tests.conftest import event_0, event_1, event_3 from tests.conftest import event_0, event_1, event_3
from tests.storage import complete_specification from tests.storage import complete_specification
from tests.storage import result_publication from tests.storage import result_publication
@ -153,6 +158,14 @@ async def test_events_without_publications(spec, expected_events, generate_model
assert unpublished_events == expected_events assert unpublished_events == expected_events
@pytest.mark.asyncio
async def test_get_all_events(generate_models):
await generate_models(one_unpublished_event_specification)
all_events = [await get_event(event_0.mobilizon_id)]
assert list(await get_all_events()) == all_events
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"mock_active_publishers, spec, event, n_publications", "mock_active_publishers, spec, event, n_publications",

View File

@ -1,16 +0,0 @@
from uuid import UUID
import pytest
from mobilizon_reshare.dataclasses.event import get_all_mobilizon_events
@pytest.mark.asyncio
async def test_get_all_events(event_generator):
all_events = [
event_generator(mobilizon_id=UUID(int=i), published=False) for i in range(4)
]
for e in all_events:
await e.to_model().save()
assert list(await get_all_mobilizon_events()) == all_events