Compare commits

..

1 commit

Author SHA1 Message Date
dependabot[bot]
908e9d3c21
Bump flake8 from 5.0.4 to 7.1.1
Bumps [flake8](https://github.com/pycqa/flake8) from 5.0.4 to 7.1.1.
- [Commits](https://github.com/pycqa/flake8/compare/5.0.4...7.1.1)

---
updated-dependencies:
- dependency-name: flake8
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 04:12:49 +00:00
16 changed files with 399 additions and 2072 deletions

26
.github/workflows/check-doc.yml vendored Normal file
View file

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

@ -5,10 +5,7 @@ This document describes changes between each past release.
## 6.2.0 (unreleased) ## 6.2.0 (unreleased)
- Add support for python 3.12 (#757) - 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) ## 6.1.5 (2024-03-19)

View file

@ -1,40 +1,60 @@
VIRTUALENV = uv venv VIRTUALENV = python3 -m venv
SPHINX_BUILDDIR = docs/_build
VENV := $(shell realpath $${VIRTUAL_ENV-.venv}) VENV := $(shell realpath $${VIRTUAL_ENV-.venv})
BIN := uv tool run PYTHON = $(VENV)/bin/python3
PIP := uv pip
PYTHON = $(BIN)/python3
DEV_STAMP = $(VENV)/.dev_env_installed.stamp DEV_STAMP = $(VENV)/.dev_env_installed.stamp
INSTALL_STAMP = $(VENV)/.install.stamp INSTALL_STAMP = $(VENV)/.install.stamp
TEMPDIR := $(shell mktemp -d) TEMPDIR := $(shell mktemp -d)
ZOPFLIPNG := zopflipng ZOPFLIPNG := zopflipng
MAGICK_MOGRIFY := mogrify 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 .PHONY: virtualenv
virtualenv: $(PYTHON) virtualenv: $(PYTHON)
$(PYTHON): $(PYTHON):
$(VIRTUALENV) $(VENV) $(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 .PHONY: serve
serve: build-translations ## Run the ihatemoney server serve: install build-translations ## Run the ihatemoney server
@echo 'Running ihatemoney on http://localhost:5000' @echo 'Running ihatemoney on http://localhost:5000'
FLASK_DEBUG=1 FLASK_APP=ihatemoney.wsgi uv run flask run --host=0.0.0.0 FLASK_DEBUG=1 FLASK_APP=ihatemoney.wsgi $(VENV)/bin/flask run --host=0.0.0.0
.PHONY: test .PHONY: test
test: test: install-dev ## Run the tests
uv run --extra dev --extra database pytest . $(VENV)/bin/tox
.PHONY: lint .PHONY: black
lint: black: install-dev ## Run the tests
uv tool run ruff check . $(VENV)/bin/black --target-version=py37 .
uv tool run vermin --no-tips --violations -t=3.8- .
.PHONY: format .PHONY: isort
format: isort: install-dev ## Run the tests
uv tool run ruff format . $(VENV)/bin/isort .
.PHONY: release .PHONY: release
release: # Release a new version (see https://ihatemoney.readthedocs.io/en/latest/contributing.html#how-to-release) release: install-dev ## Release a new version (see https://ihatemoney.readthedocs.io/en/latest/contributing.html#how-to-release)
uv run --extra dev fullreleas $(VENV)/bin/fullrelease
.PHONY: compress-showcase .PHONY: compress-showcase
compress-showcase: compress-showcase:
@ -52,30 +72,27 @@ compress-assets: compress-showcase ## Compress static assets
.PHONY: build-translations .PHONY: build-translations
build-translations: ## Build the translations build-translations: ## Build the translations
uv run --extra dev pybabel compile -d ihatemoney/translations $(VENV)/bin/pybabel compile -d ihatemoney/translations
.PHONY: extract-translations .PHONY: extract-translations
extract-translations: ## Extract new translations from source code extract-translations: ## Extract new translations from source code
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 $(VENV)/bin/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/ $(VENV)/bin/pybabel update -i ihatemoney/messages.pot -d ihatemoney/translations/
.PHONY: create-database-revision .PHONY: create-database-revision
create-database-revision: ## Create a new database revision create-database-revision: ## Create a new database revision
@read -p "Please enter a message describing this revision: " rev_message; \ @read -p "Please enter a message describing this revision: " rev_message; \
uv run python -m ihatemoney.manage db migrate -d ihatemoney/migrations -m "$${rev_message}" $(PYTHON) -m ihatemoney.manage db migrate -d ihatemoney/migrations -m "$${rev_message}"
.PHONY: create-empty-database-revision .PHONY: create-empty-database-revision
create-empty-database-revision: ## Create an empty database revision create-empty-database-revision: ## Create an empty database revision
@read -p "Please enter a message describing this revision: " rev_message; \ @read -p "Please enter a message describing this revision: " rev_message; \
uv run python -m ihatemoney.manage db revision -d ihatemoney/migrations -m "$${rev_message}" $(PYTHON) -m ihatemoney.manage db revision -d ihatemoney/migrations -m "$${rev_message}"
.PHONY: clean .PHONY: clean
clean: ## Destroy the virtual environment clean: ## Destroy the virtual environment
rm -rf .venv 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 .PHONY: help
help: ## Show the help indications 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}' @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 ## Requirements
- **Python**: version 3.8 to 3.12. - **Python**: version 3.7 to 3.12.
- **Backends**: SQLite, PostgreSQL, MariaDB (version 10.3.2 or above), - **Backends**: SQLite, PostgreSQL, MariaDB (version 10.3.2 or above),
Memory. 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. Python files in this project. Be sure to run it locally on your files.
To do so, just run: To do so, just run:
make lint make black isort
You can also integrate them with your dev environment (as a You can also integrate them with your dev environment (as a
*format-on-save* hook, for instance). *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: «Ihatemoney» depends on:
- **Python**: any version from 3.8 to 3.12 will work. - **Python**: any version from 3.7 to 3.12 will work.
- **A database backend**: choose among SQLite, PostgreSQL, MariaDB (>= - **A database backend**: choose among SQLite, PostgreSQL, MariaDB (>=
10.3.2). 10.3.2).
- **Virtual environment** (recommended): [python3-venv]{.title-ref} - **Virtual environment** (recommended): [python3-venv]{.title-ref}

View file

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

View file

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

View file

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

View file

@ -1,9 +1,10 @@
from collections import defaultdict from collections import defaultdict
from datetime import datetime, timedelta, date import datetime
import re import re
from urllib.parse import unquote, urlparse, urlunparse from urllib.parse import unquote, urlparse, urlunparse
from flask import session, url_for from flask import session, url_for
from libfaketime import fake_time
import pytest import pytest
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
@ -1029,7 +1030,9 @@ class TestBudget(IhatemoneyTestCase):
assert """<thead> assert """<thead>
<tr> <tr>
<th>Project</th> <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): def test_dashboard_project_deletion(self):
self.post_project("raclette") self.post_project("raclette")
@ -1143,7 +1146,7 @@ class TestBudget(IhatemoneyTestCase):
assert re.search(re.compile(regex2, re.DOTALL), response.data.decode("utf-8")) 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 # Check monthly expenses again: it should have a single month and the correct amount
august = date(year=2011, month=8, day=1) august = datetime.date(year=2011, month=8, day=1)
assert project.active_months_range() == [august] assert project.active_months_range() == [august]
assert dict(project.monthly_stats[2011]) == {8: 40.0} assert dict(project.monthly_stats[2011]) == {8: 40.0}
@ -1160,11 +1163,11 @@ class TestBudget(IhatemoneyTestCase):
}, },
) )
months = [ months = [
date(year=2011, month=12, day=1), datetime.date(year=2011, month=12, day=1),
date(year=2011, month=11, day=1), datetime.date(year=2011, month=11, day=1),
date(year=2011, month=10, day=1), datetime.date(year=2011, month=10, day=1),
date(year=2011, month=9, day=1), datetime.date(year=2011, month=9, day=1),
date(year=2011, month=8, day=1), datetime.date(year=2011, month=8, day=1),
] ]
amounts_2011 = { amounts_2011 = {
12: 30.0, 12: 30.0,
@ -1217,7 +1220,7 @@ class TestBudget(IhatemoneyTestCase):
"amount": "20", "amount": "20",
}, },
) )
months.append(date(year=2011, month=7, day=1)) months.append(datetime.date(year=2011, month=7, day=1))
amounts_2011[7] = 20.0 amounts_2011[7] = 20.0
assert project.active_months_range() == months assert project.active_months_range() == months
assert dict(project.monthly_stats[2011]) == amounts_2011 assert dict(project.monthly_stats[2011]) == amounts_2011
@ -1234,7 +1237,7 @@ class TestBudget(IhatemoneyTestCase):
"amount": "30", "amount": "30",
}, },
) )
months.insert(0, date(year=2012, month=1, day=1)) months.insert(0, datetime.date(year=2012, month=1, day=1))
amounts_2012 = {1: 30.0} amounts_2012 = {1: 30.0}
assert project.active_months_range() == months assert project.active_months_range() == months
assert dict(project.monthly_stats[2011]) == amounts_2011 assert dict(project.monthly_stats[2011]) == amounts_2011
@ -1867,6 +1870,7 @@ class TestBudget(IhatemoneyTestCase):
""" """
Tests that the RSS feed output content is expected. 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.post_project("raclette", default_currency="EUR")
self.client.post("/raclette/members/add", data={"name": "george"}) 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": "peter"})
@ -1913,10 +1917,12 @@ class TestBudget(IhatemoneyTestCase):
token = project.generate_token("feed") token = project.generate_token("feed")
resp = self.client.get(f"/raclette/feed/{token}.xml") resp = self.client.get(f"/raclette/feed/{token}.xml")
content = resp.data.decode() expected_rss_content = f"""<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
assert ( xmlns:dc="http://purl.org/dc/elements/1.1/"
f"""<channel> xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title>I Hate Money raclette</title> <title>I Hate Money raclette</title>
<description>Latest bills from raclette</description> <description>Latest bills from raclette</description>
<atom:link href="http://localhost/raclette/feed/{token}.xml" rel="self" type="application/rss+xml" /> <atom:link href="http://localhost/raclette/feed/{token}.xml" rel="self" type="application/rss+xml" />
@ -1926,18 +1932,32 @@ class TestBudget(IhatemoneyTestCase):
<guid isPermaLink="false">1</guid> <guid isPermaLink="false">1</guid>
<dc:creator>george</dc:creator> <dc:creator>george</dc:creator>
<description>December 31, 2016 - george, peter, steven : 4.00</description> <description>December 31, 2016 - george, peter, steven : 4.00</description>
""" <pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
in content </item>
) <item>
<title>charcuterie - 15.00</title>
assert """<title>charcuterie - €15.00</title>""" in content <guid isPermaLink="false">2</guid>
assert """<title>vin blanc - €10.00</title>""" in content <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
def test_rss_feed_history_disabled(self): def test_rss_feed_history_disabled(self):
""" """
Tests that RSS feeds is correctly rendered even if the project Tests that RSS feeds is correctly rendered even if the project
history is disabled. history is disabled.
""" """
with fake_time("2023-07-25 12:00:00"):
self.post_project("raclette", default_currency="EUR", project_history=False) 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": "george"})
self.client.post("/raclette/members/add", data={"name": "peter"}) self.client.post("/raclette/members/add", data={"name": "peter"})
@ -1984,12 +2004,44 @@ class TestBudget(IhatemoneyTestCase):
token = project.generate_token("feed") token = project.generate_token("feed")
resp = self.client.get(f"/raclette/feed/{token}.xml") resp = self.client.get(f"/raclette/feed/{token}.xml")
content = resp.data.decode() expected_rss_content = f"""<?xml version="1.0" encoding="utf-8"?>
assert """<title>charcuterie - €15.00</title>""" in content <rss version="2.0"
assert """<title>vin blanc - €10.00</title>""" in content 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
def test_rss_if_modified_since_header(self): def test_rss_if_modified_since_header(self):
# Project creation # Project creation
with fake_time("2023-07-26 13:00:00"):
self.post_project("raclette") self.post_project("raclette")
self.client.post("/raclette/members/add", data={"name": "george"}) self.client.post("/raclette/members/add", data={"name": "george"})
project = self.get_project("raclette") project = self.get_project("raclette")
@ -1997,33 +2049,22 @@ class TestBudget(IhatemoneyTestCase):
resp = self.client.get(f"/raclette/feed/{token}.xml") resp = self.client.get(f"/raclette/feed/{token}.xml")
assert resp.status_code == 200 assert resp.status_code == 200
assert "Last-Modified" in resp.headers.keys() assert resp.headers.get("Last-Modified") == "Wed, 26 Jul 2023 13:00:00 UTC"
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( resp = self.client.get(
f"/raclette/feed/{token}.xml", f"/raclette/feed/{token}.xml",
headers={"If-Modified-Since": before_str}, headers={"If-Modified-Since": "Tue, 26 Jul 2023 12:00:00 UTC"},
) )
assert resp.status_code == 200 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( resp = self.client.get(
f"/raclette/feed/{token}.xml", f"/raclette/feed/{token}.xml",
headers={"If-Modified-Since": after_str}, headers={"If-Modified-Since": "Tue, 26 Jul 2023 14:00:00 UTC"},
) )
assert resp.status_code == 304 assert resp.status_code == 304
# Add bill # Add bill
with fake_time("2023-07-27 13:00:00"):
self.login("raclette") self.login("raclette")
resp = self.client.post( resp = self.client.post(
"/raclette/add", "/raclette/add",
@ -2043,34 +2084,38 @@ class TestBudget(IhatemoneyTestCase):
resp = self.client.get( resp = self.client.get(
f"/raclette/feed/{token}.xml", f"/raclette/feed/{token}.xml",
headers={"If-Modified-Since": before_str}, headers={"If-Modified-Since": "Tue, 27 Jul 2023 12:00:00 UTC"},
) )
assert resp.headers.get("Last-Modified") == "Thu, 27 Jul 2023 13:00:00 UTC"
assert resp.status_code == 200 assert resp.status_code == 200
resp = self.client.get( resp = self.client.get(
f"/raclette/feed/{token}.xml", f"/raclette/feed/{token}.xml",
headers={"If-Modified-Since": after_str}, headers={"If-Modified-Since": "Tue, 27 Jul 2023 14:00:00 UTC"},
) )
assert resp.status_code == 304 assert resp.status_code == 304
def test_rss_etag_headers(self): def test_rss_etag_headers(self):
# Project creation # Project creation
with fake_time("2023-07-26 13:00:00"):
self.post_project("raclette") self.post_project("raclette")
self.client.post("/raclette/members/add", data={"name": "george"}) self.client.post("/raclette/members/add", data={"name": "george"})
project = self.get_project("raclette") project = self.get_project("raclette")
token = project.generate_token("feed") token = project.generate_token("feed")
resp = self.client.get(f"/raclette/feed/{token}.xml") resp = self.client.get(f"/raclette/feed/{token}.xml")
etag = resp.headers.get("ETag") assert resp.headers.get("ETag") == build_etag(
project.id, "2023-07-26T13:00:00"
)
assert resp.status_code == 200 assert resp.status_code == 200
resp = self.client.get( resp = self.client.get(
f"/raclette/feed/{token}.xml", f"/raclette/feed/{token}.xml",
headers={ headers={
"If-None-Match": etag, "If-None-Match": build_etag(project.id, "2023-07-26T12:00:00"),
}, },
) )
assert resp.status_code == 304 assert resp.status_code == 200
resp = self.client.get( resp = self.client.get(
f"/raclette/feed/{token}.xml", f"/raclette/feed/{token}.xml",
@ -2078,9 +2123,10 @@ class TestBudget(IhatemoneyTestCase):
"If-None-Match": build_etag(project.id, "2023-07-26T13:00:00"), "If-None-Match": build_etag(project.id, "2023-07-26T13:00:00"),
}, },
) )
assert resp.status_code == 200 assert resp.status_code == 304
# Add bill # Add bill
with fake_time("2023-07-27 13:00:00"):
self.login("raclette") self.login("raclette")
resp = self.client.post( resp = self.client.post(
"/raclette/add", "/raclette/add",
@ -2097,19 +2143,20 @@ class TestBudget(IhatemoneyTestCase):
) )
assert resp.status_code == 200 assert resp.status_code == 200
assert "The bill has been added" in resp.data.decode() 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( resp = self.client.get(
f"/raclette/feed/{token}.xml", f"/raclette/feed/{token}.xml",
headers={ headers={
"If-None-Match": new_etag, "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
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={
"If-None-Match": build_etag(project.id, "2023-07-27T13:00:00"),
}, },
) )
assert resp.status_code == 304 assert resp.status_code == 304

View file

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

View file

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

View file

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

41
tox.ini Normal file
View file

@ -0,0 +1,41 @@
[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

File diff suppressed because it is too large Load diff