Compare commits

...

11 commits

Author SHA1 Message Date
dependabot[bot]
e061d9314a
Bump docutils from 0.20.1 to 0.21.2
Bumps [docutils](https://docutils.sourceforge.io) from 0.20.1 to 0.21.2.

---
updated-dependencies:
- dependency-name: docutils
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 16:48:18 +00:00
dependabot[bot]
ce20f9adea Update myst-parser requirement from <3,>=2 to >=2,<5
Updates the requirements on [myst-parser](https://github.com/executablebooks/MyST-Parser) to permit the latest version.
- [Release notes](https://github.com/executablebooks/MyST-Parser/releases)
- [Changelog](https://github.com/executablebooks/MyST-Parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/executablebooks/MyST-Parser/compare/v2.0.0...v4.0.0)

---
updated-dependencies:
- dependency-name: myst-parser
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 17:46:40 +01:00
2b21795e94 ci: Pin runners to ubuntu 22.04 2024-12-20 17:46:02 +01:00
dependabot[bot]
0b09476865 Bump vermin from 1.5.2 to 1.6.0
Bumps [vermin](https://github.com/netromdk/vermin) from 1.5.2 to 1.6.0.
- [Release notes](https://github.com/netromdk/vermin/releases)
- [Commits](https://github.com/netromdk/vermin/compare/v1.5.2...v1.6.0)

---
updated-dependencies:
- dependency-name: vermin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 17:32:56 +01:00
dependabot[bot]
62fbeee15c Update pymysql requirement from <1.1,>=0.9 to >=0.9,<1.2
Updates the requirements on [pymysql](https://github.com/PyMySQL/PyMySQL) to permit the latest version.
- [Release notes](https://github.com/PyMySQL/PyMySQL/releases)
- [Changelog](https://github.com/PyMySQL/PyMySQL/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyMySQL/PyMySQL/compare/v0.9.0...v1.1.1)

---
updated-dependencies:
- dependency-name: pymysql
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 17:32:50 +01:00
dependabot[bot]
56e2ff6900 Update wtforms requirement from <3.2,>=2.3.3 to >=2.3.3,<3.3
Updates the requirements on [wtforms](https://github.com/pallets-eco/wtforms) to permit the latest version.
- [Release notes](https://github.com/pallets-eco/wtforms/releases)
- [Changelog](https://github.com/pallets-eco/wtforms/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets-eco/wtforms/compare/2.3.3...3.2.1)

---
updated-dependencies:
- dependency-name: wtforms
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 17:32:42 +01:00
dependabot[bot]
35ac04be20 Update flask requirement from <3,>=2 to >=2,<4
Updates the requirements on [flask](https://github.com/pallets/flask) to permit the latest version.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/2.0.0...3.1.0)

---
updated-dependencies:
- dependency-name: flask
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 17:32:32 +01:00
dependabot[bot]
a9f211d3f6 Update sphinx requirement from <8,>=7.0.1 to >=7.0.1,<9
Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/v8.1.3/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.0.1...v8.1.3)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 17:32:17 +01:00
e568bb05cc tests: remove libfake time from the tests
libfaketime and python-libfaketime seem to cause our CI to fail when
used in conjunction with `uv`. This changes the way the tests are done
so they don't require libfaketime anymore.
2024-12-20 17:17:31 +01:00
86eb9b8662 build: remove support for python 3.7 2024-12-20 17:17:31 +01:00
6e31a9c8b5 Upgrade tooling on the project.
- Replace black by ruff, as it's quicker ;
- Use `uv` wherever possible as a replacement for pip, as it's way faster to run, add an `uv.lock` file which will be synced before the releases and published here ;
- Remove tox, it's too complex for this project and can easily be replaced by `uv` ;
- Apply `ruff` formatting ;
- Update the makefile accordingly ;
- Update the CI accordingly
2024-12-20 17:17:31 +01:00
17 changed files with 2079 additions and 406 deletions

View file

@ -1,26 +0,0 @@
name: Check doc
on:
push:
branches: [ 'master', 'stable-*' ]
pull_request:
branches: [ 'master', 'stable-*' ]
jobs:
test_doc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
cache: 'pip'
cache-dependency-path: '**/pyproject.toml'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install tox
- name: Check we can generate documentation
run: tox -e docs

View file

@ -1,4 +1,4 @@
name: Lint & unit tests
name: CI
on:
push:
@ -8,26 +8,20 @@ on:
jobs:
lint:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v4
with:
python-version: "3.11"
cache: 'pip'
cache-dependency-path: '**/pyproject.toml'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install tox
- name: Run Lint
run: tox -e lint
run: make lint
test:
# Dependency on linting to avoid running our expensive matrix test for nothing
needs: lint
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
# Use postgresql and MariaDB versions of Debian bookworm
services:
postgres:
@ -53,10 +47,11 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"]
python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
dependencies: [normal]
database: [sqlite]
# Test other databases with only a few versions of Python (Debian bullseye has 3.9, bookworm has 3.11)
# Test other databases with only a few versions of Python
# (Debian bullseye has 3.9, bookworm has 3.11)
include:
- python-version: 3.9
dependencies: normal
@ -71,9 +66,6 @@ jobs:
dependencies: normal
database: mariadb
# Try a few variants with the minimal versions supported
- python-version: 3.7
dependencies: minimal
database: sqlite
- python-version: 3.9
dependencies: minimal
database: sqlite
@ -95,32 +87,36 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: '**/pyproject.toml'
- name: Change dependencies to minimal supported versions
run: sed -i -e 's/>=/==/g; s/~=.*==\(.*\)/==\1/g; s/~=/==/g;' pyproject.toml
# This sed comment installs the minimal supported version
# for all versions except for "requires-python"
# This is to detect that the minimum versions are really
# supported, in the CI
run: sed -i -e '/requires-python/!s/>=/==/g; /requires-python/!s/~=.*==\(.*\)/==\1/g; /requires-python/!s/~=/==/g;' pyproject.toml
if: matrix.dependencies == 'minimal'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install tox
# Run tox using the version of Python in `PATH`
- name: Run Tox with sqlite
run: tox -e py
if: matrix.database == 'sqlite'
- name: Run tests
run: uv run --extra dev --extra database pytest .
env:
TESTING_SQLALCHEMY_DATABASE_URI: 'sqlite:///budget.db'
- name: Run Tox with postgresql
run: tox -e py
if: matrix.database == 'postgresql'
env:
TESTING_SQLALCHEMY_DATABASE_URI: 'postgresql+psycopg2://postgres:ihatemoney@localhost:5432/ihatemoney_ci'
- name: Run Tox with mariadb
run: tox -e py
if: matrix.database == 'mariadb'
env:
TESTING_SQLALCHEMY_DATABASE_URI: 'mysql+pymysql://root:ihatemoney@localhost:3306/ihatemoney_ci'
# Setup the DATABASE_URI depending on the matrix we are using.
TESTING_SQLALCHEMY_DATABASE_URI: ${{
matrix.database == 'sqlite'
&& 'sqlite:///budget.db'
|| matrix.database == 'postgresql'
&& 'postgresql+psycopg2://postgres:ihatemoney@localhost:5432/ihatemoney_ci'
|| 'mysql+pymysql://root:ihatemoney@localhost:3306/ihatemoney_ci'
}}
docs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v4
with:
python-version: "3.11"
- name: Build docs
run: make build-docs

View file

@ -10,7 +10,7 @@ on:
jobs:
test:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v2
@ -19,7 +19,7 @@ jobs:
run: docker compose -f docker-compose.test.yml run sut
build_upload:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
needs: test
if: github.event_name != 'pull_request'
steps:

View file

@ -5,7 +5,10 @@ This document describes changes between each past release.
## 6.2.0 (unreleased)
- Add support for python 3.12 (#757)
- Remove support for python 3.7
- Replace the black linter by ruff
- Replace virtualenv and pip by uv
- Remove tox
## 6.1.5 (2024-03-19)

View file

@ -1,60 +1,40 @@
VIRTUALENV = python3 -m venv
SPHINX_BUILDDIR = docs/_build
VIRTUALENV = uv venv
VENV := $(shell realpath $${VIRTUAL_ENV-.venv})
PYTHON = $(VENV)/bin/python3
BIN := uv tool run
PIP := uv pip
PYTHON = $(BIN)/python3
DEV_STAMP = $(VENV)/.dev_env_installed.stamp
INSTALL_STAMP = $(VENV)/.install.stamp
TEMPDIR := $(shell mktemp -d)
ZOPFLIPNG := zopflipng
MAGICK_MOGRIFY := mogrify
.PHONY: all
all: install ## Alias for install
.PHONY: install
install: virtualenv pyproject.toml $(INSTALL_STAMP) ## Install dependencies
$(INSTALL_STAMP):
$(VENV)/bin/pip install -U pip
$(VENV)/bin/pip install -e .
touch $(INSTALL_STAMP)
.PHONY: virtualenv
virtualenv: $(PYTHON)
$(PYTHON):
$(VIRTUALENV) $(VENV)
.PHONY: install-dev
install-dev: virtualenv pyproject.toml $(INSTALL_STAMP) $(DEV_STAMP) ## Install development dependencies
$(DEV_STAMP): $(PYTHON)
$(VENV)/bin/pip install -Ue .[dev]
touch $(DEV_STAMP)
.PHONY: remove-install-stamp
remove-install-stamp:
rm $(INSTALL_STAMP)
.PHONY: update
update: remove-install-stamp install ## Update the dependencies
.PHONY: serve
serve: install build-translations ## Run the ihatemoney server
serve: build-translations ## Run the ihatemoney server
@echo 'Running ihatemoney on http://localhost:5000'
FLASK_DEBUG=1 FLASK_APP=ihatemoney.wsgi $(VENV)/bin/flask run --host=0.0.0.0
FLASK_DEBUG=1 FLASK_APP=ihatemoney.wsgi uv run flask run --host=0.0.0.0
.PHONY: test
test: install-dev ## Run the tests
$(VENV)/bin/tox
test:
uv run --extra dev --extra database pytest .
.PHONY: black
black: install-dev ## Run the tests
$(VENV)/bin/black --target-version=py37 .
.PHONY: lint
lint:
uv tool run ruff check .
uv tool run vermin --no-tips --violations -t=3.8- .
.PHONY: isort
isort: install-dev ## Run the tests
$(VENV)/bin/isort .
.PHONY: format
format:
uv tool run ruff format .
.PHONY: release
release: install-dev ## Release a new version (see https://ihatemoney.readthedocs.io/en/latest/contributing.html#how-to-release)
$(VENV)/bin/fullrelease
release: # Release a new version (see https://ihatemoney.readthedocs.io/en/latest/contributing.html#how-to-release)
uv run --extra dev fullreleas
.PHONY: compress-showcase
compress-showcase:
@ -72,27 +52,30 @@ compress-assets: compress-showcase ## Compress static assets
.PHONY: build-translations
build-translations: ## Build the translations
$(VENV)/bin/pybabel compile -d ihatemoney/translations
uv run --extra dev pybabel compile -d ihatemoney/translations
.PHONY: extract-translations
extract-translations: ## Extract new translations from source code
$(VENV)/bin/pybabel extract --add-comments "I18N:" --strip-comments --omit-header --no-location --mapping-file ihatemoney/babel.cfg -o ihatemoney/messages.pot ihatemoney
$(VENV)/bin/pybabel update -i ihatemoney/messages.pot -d ihatemoney/translations/
uv run --extra dev pybabel extract --add-comments "I18N:" --strip-comments --omit-header --no-location --mapping-file ihatemoney/babel.cfg -o ihatemoney/messages.pot ihatemoney
uv run --extra dev pybabel update -i ihatemoney/messages.pot -d ihatemoney/translations/
.PHONY: create-database-revision
create-database-revision: ## Create a new database revision
@read -p "Please enter a message describing this revision: " rev_message; \
$(PYTHON) -m ihatemoney.manage db migrate -d ihatemoney/migrations -m "$${rev_message}"
uv run python -m ihatemoney.manage db migrate -d ihatemoney/migrations -m "$${rev_message}"
.PHONY: create-empty-database-revision
create-empty-database-revision: ## Create an empty database revision
@read -p "Please enter a message describing this revision: " rev_message; \
$(PYTHON) -m ihatemoney.manage db revision -d ihatemoney/migrations -m "$${rev_message}"
uv run python -m ihatemoney.manage db revision -d ihatemoney/migrations -m "$${rev_message}"
.PHONY: clean
clean: ## Destroy the virtual environment
rm -rf .venv
build-docs:
uv run --extra doc sphinx-build -a -n -b html -d docs/_build/doctrees docs docs/_build/html
.PHONY: help
help: ## Show the help indications
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

View file

@ -22,7 +22,7 @@ highly encouraged to do so.
## Requirements
- **Python**: version 3.7 to 3.12.
- **Python**: version 3.8 to 3.12.
- **Backends**: SQLite, PostgreSQL, MariaDB (version 10.3.2 or above),
Memory.

View file

@ -183,7 +183,7 @@ We are using [black](https://black.readthedocs.io/en/stable/) and
Python files in this project. Be sure to run it locally on your files.
To do so, just run:
make black isort
make lint
You can also integrate them with your dev environment (as a
*format-on-save* hook, for instance).

View file

@ -93,7 +93,7 @@ Some Paas (Platform-as-a-Service), provide a documentation or even a quick insta
«Ihatemoney» depends on:
- **Python**: any version from 3.7 to 3.12 will work.
- **Python**: any version from 3.8 to 3.12 will work.
- **A database backend**: choose among SQLite, PostgreSQL, MariaDB (>=
10.3.2).
- **Virtual environment** (recommended): [python3-venv]{.title-ref}

View file

@ -90,7 +90,6 @@ def get_billform_for(project, set_default=True, **kwargs):
class CommaDecimalField(DecimalField):
"""A class to deal with comma in Decimal Field"""
def process_formdata(self, value):

View file

@ -103,7 +103,7 @@ def validate_configuration(app):
if "MAIL_DEFAULT_SENDER" not in app.config:
app.config["MAIL_DEFAULT_SENDER"] = default_settings.DEFAULT_MAIL_SENDER
if type(app.config["MAIL_DEFAULT_SENDER"]) == tuple:
if type(app.config["MAIL_DEFAULT_SENDER"]) is tuple:
(name, address) = app.config["MAIL_DEFAULT_SENDER"]
app.config["MAIL_DEFAULT_SENDER"] = f"{name} <{address}>"
warnings.warn(

View file

@ -9,7 +9,6 @@ from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
class TestAPI(IhatemoneyTestCase):
"""Tests the API"""
def api_create(

View file

@ -1,10 +1,9 @@
from collections import defaultdict
import datetime
from datetime import datetime, timedelta, date
import re
from urllib.parse import unquote, urlparse, urlunparse
from flask import session, url_for
from libfaketime import fake_time
import pytest
from werkzeug.security import check_password_hash
@ -1030,9 +1029,7 @@ class TestBudget(IhatemoneyTestCase):
assert """<thead>
<tr>
<th>Project</th>
<th>Number of participants</th>""" in resp.data.decode(
"utf-8"
)
<th>Number of participants</th>""" in resp.data.decode("utf-8")
def test_dashboard_project_deletion(self):
self.post_project("raclette")
@ -1146,7 +1143,7 @@ class TestBudget(IhatemoneyTestCase):
assert re.search(re.compile(regex2, re.DOTALL), response.data.decode("utf-8"))
# Check monthly expenses again: it should have a single month and the correct amount
august = datetime.date(year=2011, month=8, day=1)
august = date(year=2011, month=8, day=1)
assert project.active_months_range() == [august]
assert dict(project.monthly_stats[2011]) == {8: 40.0}
@ -1163,11 +1160,11 @@ class TestBudget(IhatemoneyTestCase):
},
)
months = [
datetime.date(year=2011, month=12, day=1),
datetime.date(year=2011, month=11, day=1),
datetime.date(year=2011, month=10, day=1),
datetime.date(year=2011, month=9, day=1),
datetime.date(year=2011, month=8, day=1),
date(year=2011, month=12, day=1),
date(year=2011, month=11, day=1),
date(year=2011, month=10, day=1),
date(year=2011, month=9, day=1),
date(year=2011, month=8, day=1),
]
amounts_2011 = {
12: 30.0,
@ -1220,7 +1217,7 @@ class TestBudget(IhatemoneyTestCase):
"amount": "20",
},
)
months.append(datetime.date(year=2011, month=7, day=1))
months.append(date(year=2011, month=7, day=1))
amounts_2011[7] = 20.0
assert project.active_months_range() == months
assert dict(project.monthly_stats[2011]) == amounts_2011
@ -1237,7 +1234,7 @@ class TestBudget(IhatemoneyTestCase):
"amount": "30",
},
)
months.insert(0, datetime.date(year=2012, month=1, day=1))
months.insert(0, date(year=2012, month=1, day=1))
amounts_2012 = {1: 30.0}
assert project.active_months_range() == months
assert dict(project.monthly_stats[2011]) == amounts_2011
@ -1870,59 +1867,56 @@ class TestBudget(IhatemoneyTestCase):
"""
Tests that the RSS feed output content is expected.
"""
with fake_time("2023-07-25 12:00:00"):
self.post_project("raclette", default_currency="EUR")
self.client.post("/raclette/members/add", data={"name": "george"})
self.client.post("/raclette/members/add", data={"name": "peter"})
self.client.post("/raclette/members/add", data={"name": "steven"})
self.post_project("raclette", default_currency="EUR")
self.client.post("/raclette/members/add", data={"name": "george"})
self.client.post("/raclette/members/add", data={"name": "peter"})
self.client.post("/raclette/members/add", data={"name": "steven"})
self.client.post(
"/raclette/add",
data={
"date": "2016-12-31",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"amount": "12",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2016-12-30",
"what": "charcuterie",
"payer": 2,
"payed_for": [1, 2],
"amount": "15",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2016-12-29",
"what": "vin blanc",
"payer": 2,
"payed_for": [1, 2],
"amount": "10",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2016-12-31",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"amount": "12",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2016-12-30",
"what": "charcuterie",
"payer": 2,
"payed_for": [1, 2],
"amount": "15",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2016-12-29",
"what": "vin blanc",
"payer": 2,
"payed_for": [1, 2],
"amount": "10",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
project = self.get_project("raclette")
token = project.generate_token("feed")
resp = self.client.get(f"/raclette/feed/{token}.xml")
expected_rss_content = f"""<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
content = resp.data.decode()
assert (
f"""<channel>
<title>I Hate Money raclette</title>
<description>Latest bills from raclette</description>
<atom:link href="http://localhost/raclette/feed/{token}.xml" rel="self" type="application/rss+xml" />
@ -1932,190 +1926,151 @@ class TestBudget(IhatemoneyTestCase):
<guid isPermaLink="false">1</guid>
<dc:creator>george</dc:creator>
<description>December 31, 2016 - george, peter, steven : 4.00</description>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
</item>
<item>
<title>charcuterie - 15.00</title>
<guid isPermaLink="false">2</guid>
<dc:creator>peter</dc:creator>
<description>December 30, 2016 - george, peter : 7.50</description>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
</item>
<item>
<title>vin blanc - 10.00</title>
<guid isPermaLink="false">3</guid>
<dc:creator>peter</dc:creator>
<description>December 29, 2016 - george, peter : 5.00</description>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
</item>
</channel>
</rss>""" # noqa: E221, E222, E231, E501
assert resp.data.decode() == expected_rss_content
"""
in content
)
assert """<title>charcuterie - €15.00</title>""" in content
assert """<title>vin blanc - €10.00</title>""" in content
def test_rss_feed_history_disabled(self):
"""
Tests that RSS feeds is correctly rendered even if the project
history is disabled.
"""
with fake_time("2023-07-25 12:00:00"):
self.post_project("raclette", default_currency="EUR", project_history=False)
self.client.post("/raclette/members/add", data={"name": "george"})
self.client.post("/raclette/members/add", data={"name": "peter"})
self.client.post("/raclette/members/add", data={"name": "steven"})
self.post_project("raclette", default_currency="EUR", project_history=False)
self.client.post("/raclette/members/add", data={"name": "george"})
self.client.post("/raclette/members/add", data={"name": "peter"})
self.client.post("/raclette/members/add", data={"name": "steven"})
self.client.post(
"/raclette/add",
data={
"date": "2016-12-31",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"amount": "12",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2016-12-30",
"what": "charcuterie",
"payer": 2,
"payed_for": [1, 2],
"amount": "15",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2016-12-29",
"what": "vin blanc",
"payer": 2,
"payed_for": [1, 2],
"amount": "10",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2016-12-31",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"amount": "12",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2016-12-30",
"what": "charcuterie",
"payer": 2,
"payed_for": [1, 2],
"amount": "15",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2016-12-29",
"what": "vin blanc",
"payer": 2,
"payed_for": [1, 2],
"amount": "10",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
project = self.get_project("raclette")
token = project.generate_token("feed")
resp = self.client.get(f"/raclette/feed/{token}.xml")
expected_rss_content = f"""<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title>I Hate Money raclette</title>
<description>Latest bills from raclette</description>
<atom:link href="http://localhost/raclette/feed/{token}.xml" rel="self" type="application/rss+xml" />
<link>http://localhost/raclette/</link>
<item>
<title>fromage à raclette - 12.00</title>
<guid isPermaLink="false">1</guid>
<dc:creator>george</dc:creator>
<description>December 31, 2016 - george, peter, steven : 4.00</description>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
</item>
<item>
<title>charcuterie - 15.00</title>
<guid isPermaLink="false">2</guid>
<dc:creator>peter</dc:creator>
<description>December 30, 2016 - george, peter : 7.50</description>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
</item>
<item>
<title>vin blanc - 10.00</title>
<guid isPermaLink="false">3</guid>
<dc:creator>peter</dc:creator>
<description>December 29, 2016 - george, peter : 5.00</description>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
</item>
</channel>
</rss>""" # noqa: E221, E222, E231, E501
assert resp.data.decode() == expected_rss_content
content = resp.data.decode()
assert """<title>charcuterie - €15.00</title>""" in content
assert """<title>vin blanc - €10.00</title>""" in content
def test_rss_if_modified_since_header(self):
# Project creation
with fake_time("2023-07-26 13:00:00"):
self.post_project("raclette")
self.client.post("/raclette/members/add", data={"name": "george"})
project = self.get_project("raclette")
token = project.generate_token("feed")
self.post_project("raclette")
self.client.post("/raclette/members/add", data={"name": "george"})
project = self.get_project("raclette")
token = project.generate_token("feed")
resp = self.client.get(f"/raclette/feed/{token}.xml")
assert resp.status_code == 200
assert resp.headers.get("Last-Modified") == "Wed, 26 Jul 2023 13:00:00 UTC"
resp = self.client.get(f"/raclette/feed/{token}.xml")
assert resp.status_code == 200
assert "Last-Modified" in resp.headers.keys()
last_modified = resp.headers.get("Last-Modified")
# Get a date 1 hour before the last modified date
before = datetime.strptime(
last_modified, "%a, %d %b %Y %H:%M:%S %Z"
) - timedelta(hours=1)
before_str = before.strftime("%a, %d %b %Y %H:%M:%S %Z")
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={"If-Modified-Since": "Tue, 26 Jul 2023 12:00:00 UTC"},
headers={"If-Modified-Since": before_str},
)
assert resp.status_code == 200
after = datetime.strptime(
last_modified, "%a, %d %b %Y %H:%M:%S %Z"
) + timedelta(hours=1)
after_str = after.strftime("%a, %d %b %Y %H:%M:%S %Z")
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={"If-Modified-Since": "Tue, 26 Jul 2023 14:00:00 UTC"},
headers={"If-Modified-Since": after_str},
)
assert resp.status_code == 304
# Add bill
with fake_time("2023-07-27 13:00:00"):
self.login("raclette")
resp = self.client.post(
"/raclette/add",
data={
"date": "2016-12-31",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
"amount": "12",
"original_currency": "XXX",
"bill_type": "Expense",
},
follow_redirects=True,
)
assert resp.status_code == 200
assert "The bill has been added" in resp.data.decode()
self.login("raclette")
resp = self.client.post(
"/raclette/add",
data={
"date": "2016-12-31",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
"amount": "12",
"original_currency": "XXX",
"bill_type": "Expense",
},
follow_redirects=True,
)
assert resp.status_code == 200
assert "The bill has been added" in resp.data.decode()
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={"If-Modified-Since": "Tue, 27 Jul 2023 12:00:00 UTC"},
headers={"If-Modified-Since": before_str},
)
assert resp.headers.get("Last-Modified") == "Thu, 27 Jul 2023 13:00:00 UTC"
assert resp.status_code == 200
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={"If-Modified-Since": "Tue, 27 Jul 2023 14:00:00 UTC"},
headers={"If-Modified-Since": after_str},
)
assert resp.status_code == 304
def test_rss_etag_headers(self):
# Project creation
with fake_time("2023-07-26 13:00:00"):
self.post_project("raclette")
self.client.post("/raclette/members/add", data={"name": "george"})
project = self.get_project("raclette")
token = project.generate_token("feed")
self.post_project("raclette")
self.client.post("/raclette/members/add", data={"name": "george"})
project = self.get_project("raclette")
token = project.generate_token("feed")
resp = self.client.get(f"/raclette/feed/{token}.xml")
assert resp.headers.get("ETag") == build_etag(
project.id, "2023-07-26T13:00:00"
)
assert resp.status_code == 200
resp = self.client.get(f"/raclette/feed/{token}.xml")
etag = resp.headers.get("ETag")
assert resp.status_code == 200
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={
"If-None-Match": build_etag(project.id, "2023-07-26T12:00:00"),
"If-None-Match": etag,
},
)
assert resp.status_code == 200
assert resp.status_code == 304
resp = self.client.get(
f"/raclette/feed/{token}.xml",
@ -2123,40 +2078,38 @@ class TestBudget(IhatemoneyTestCase):
"If-None-Match": build_etag(project.id, "2023-07-26T13:00:00"),
},
)
assert resp.status_code == 304
# Add bill
with fake_time("2023-07-27 13:00:00"):
self.login("raclette")
resp = self.client.post(
"/raclette/add",
data={
"date": "2016-12-31",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
"amount": "12",
"bill_type": "Expense",
"original_currency": "XXX",
},
follow_redirects=True,
)
assert resp.status_code == 200
assert "The bill has been added" in resp.data.decode()
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={
"If-None-Match": build_etag(project.id, "2023-07-27T12:00:00"),
},
)
assert resp.headers.get("ETag") == build_etag(project.id, "2023-07-27T13:00:00")
assert resp.status_code == 200
# Add bill
self.login("raclette")
resp = self.client.post(
"/raclette/add",
data={
"date": "2016-12-31",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
"amount": "12",
"bill_type": "Expense",
"original_currency": "XXX",
},
follow_redirects=True,
)
assert resp.status_code == 200
assert "The bill has been added" in resp.data.decode()
etag = resp.headers.get("ETag")
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={"If-None-Match": etag},
)
assert resp.status_code == 200
new_etag = resp.headers.get("ETag")
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={
"If-None-Match": build_etag(project.id, "2023-07-27T13:00:00"),
"If-None-Match": new_etag,
},
)
assert resp.status_code == 304

View file

@ -84,7 +84,6 @@ def flash_email_error(error_message, category="danger"):
class Redirect303(HTTPException, RoutingException):
"""Raise if the map requests a redirect. This is for example the case if
`strict_slashes` are activated and an url that requires a trailing slash.
@ -102,7 +101,6 @@ class Redirect303(HTTPException, RoutingException):
class PrefixedWSGI(object):
"""
Wrap the application in this middleware and configure the
front-end server to add these headers, to let you quietly bind

View file

@ -8,6 +8,7 @@ Basically, this blueprint takes care of the authentication and provides
some shortcuts to make your life better when coding (see `pull_project`
and `add_project_id` for a quick overview)
"""
import datetime
from functools import wraps
import hashlib

View file

@ -5,6 +5,7 @@ build-backend = "hatchling.build"
[project]
name = "ihatemoney"
version = "6.2.0.dev0"
requires-python = ">=3.8"
description = "A simple shared budget manager web application."
readme = "README.md"
license = {file = "LICENSE"}
@ -15,7 +16,6 @@ keywords = ["web", "budget"]
classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
@ -30,50 +30,45 @@ dependencies = [
"cachetools>=4.1,<5",
"debts>=0.5,<1",
"email_validator>=1.0,<3",
"Flask>=2,<4",
"Flask-Babel>=1.0,<4",
"Flask-Cors>=3.0.8,<4",
"Flask-Limiter>=2.6,<3",
"Flask-Mail>=0.9.1,<1",
"Flask-Migrate>=2.5.3,<5", # Not following semantic versioning (e.g. https://github.com/miguelgrinberg/flask-migrate/commit/1af28ba273de6c88544623b8dc02dd539340294b)
"Flask-Migrate>=2.5.3,<5", # Not following semantic versioning (e.g. https://github.com/miguelgrinberg/flask-migrate/commit/1af28ba273de6c88544623b8dc02dd539340294b)
"Flask-RESTful>=0.3.9,<1",
"Flask-SQLAlchemy>=2.4,<3",
"Flask-Talisman>=0.8,<2",
"Flask-WTF>=0.14.3,<2",
"WTForms>=2.3.3,<3.2",
"Flask>=2,<3",
"Werkzeug>=2,<3",
"itsdangerous>=2,<3",
"Jinja2>=3,<4",
"python-dateutil",
"qrcode>=7.1,<8",
"requests>=2.25,<3",
"SQLAlchemy-Continuum>=1.3.12,<2",
"SQLAlchemy>=1.3.0,<1.5", # New 1.4 changes API, see #728
"python-dateutil",
]
"SQLAlchemy>=1.3.0,<1.5",
"SQLAlchemy-Continuum>=1.3.12,<2", # New 1.4 changes API, see #728
"Werkzeug>=2,<3",
"WTForms>=2.3.3,<3.3",]
[project.optional-dependencies]
database = [
# Python 3.11 support starts in 2.9.2
"psycopg2-binary>=2.9.2,<3",
"PyMySQL>=0.9,<1.1",
"psycopg2-binary>=2.9.2,<2.9.9",
"PyMySQL>=0.9,<1.2",
]
dev = [
"black==23.3.0",
"ruff==0.6.8",
"flake8==5.0.4",
"isort==5.11.5",
"vermin==1.5.2",
"vermin==1.6.0",
"pytest>=6.2.5",
"pytest-flask>=1.2.0",
"pytest-libfaketime>=0.1.2",
"tox>=3.14.6",
"zest.releaser>=6.20.1",
]
doc = [
"Sphinx>=7.0.1,<8",
"docutils==0.20.1",
"myst-parser>=2,<3",
"Sphinx>=7.0.1,<9",
"docutils==0.21.2",
"myst-parser>=2,<5",
]
[project.urls]
@ -107,3 +102,7 @@ include = [
"README.md",
"SECURITY.md",
]
[tool.ruff]
exclude = ["ihatemoney/migrations"]

41
tox.ini
View file

@ -1,41 +0,0 @@
[tox]
isolated_build = true
envlist = py312,py311,py310,py39,py38,py37,lint,docs
skip_missing_interpreters = True
[testenv]
passenv = TESTING_SQLALCHEMY_DATABASE_URI
commands =
python --version
py.test --pyargs ihatemoney.tests {posargs}
deps =
-e.[database,dev]
# To be sure we are importing ihatemoney pkg from pip-installed version
changedir = /tmp
[testenv:lint]
commands =
black --check --target-version=py37 .
isort --check --profile black .
flake8 ihatemoney
vermin --no-tips --violations -t=3.7- .
deps =
-e.[dev]
changedir = {toxinidir}
[testenv:docs]
commands =
sphinx-build -a -n -b html -d docs/_build/doctrees docs docs/_build/html
deps =
-e.[doc]
changedir = {toxinidir}
[flake8]
exclude = migrations
max_line_length = 100
extend-ignore =
# See https://github.com/PyCQA/pycodestyle/issues/373
E203,

1809
uv.lock Normal file

File diff suppressed because it is too large Load diff