fix(deps): nommer le pilote PostgreSQL, sinon rien ne demarre

QUA-001, volet dialecte.

`postgresql://` ne veut pas dire "le pilote installe" : SQLAlchemy y lit
psycopg2 et importe ce module a la creation du moteur. requirements.txt
epingle psycopg 3 (`psycopg[binary]`) et pas psycopg2. Une installation
propre demarree sur cette URL leve donc

  ModuleNotFoundError: No module named 'psycopg2'

avant la premiere requete. Verifie dans le .venv du depot, et c est
exactement la forme que Render distribue -- celle que docs/deployment.md et
docs/database-restore.md donnaient en exemple.

normalise_database_url() nomme le pilote quand l URL n en nomme pas.
`postgres://` (alias hérite, abandonne par SQLAlchemy en 1.4) est traite de
meme. Une URL qui nomme deja son pilote est laissee telle quelle, y compris
`postgresql+psycopg2://` : un environnement qui a psycopg2 garde le choix.

La normalisation a lieu apres l application de la configuration passee en
argument, pour couvrir aussi les appels de test. backup.py n avait pas
besoin d etre touche : il retirait deja le suffixe +pilote.

Documentation alignee sur les trois fichiers qui donnaient l exemple, dont
docs/deployment.md qui proposait sqlite:/// pour DATABASE_URL alors que
create_app refuse de demarrer sans PostgreSQL.

Reste de QUA-001, dit franchement
  - les trois paquets parasites (dotenv, login, discord) ne sont plus dans
    requirements.txt : deja retires. psycopg est deja epingle.
  - la consolidation vers des groupes de dependances n est PAS faite. Le
    deploiement est un miroir de fichiers lftp sans etape de construction ;
    les groupes PEP 735 demandent pip >= 25.1 sur une machine dont on ne
    peut pas verifier la version d ici. A revoir avec OPS-011.

14 tests, dont trois qui prouvent que l echec est reel et non theorique.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-08 15:50:35 -04:00
co-authored by Claude Opus 5
parent 51877b46a0
commit 2f40290f00
6 changed files with 146 additions and 6 deletions
+90
View File
@@ -0,0 +1,90 @@
"""Which PostgreSQL driver the application actually asks for — QUA-001.
`postgresql://…` is not "whichever driver is installed". SQLAlchemy reads it
as psycopg2 and imports that module when the engine is created.
requirements.txt pins psycopg 3 and no psycopg2, so a clean install against
the URL Render hands out — the same form docs/database-restore.md documents
— fails before the first request:
ModuleNotFoundError: No module named 'psycopg2'
create_app() now names the driver. These tests pin that down, and the last
one proves the failure is real rather than theoretical.
"""
import pytest
from sqlalchemy import create_engine
from app.app import normalise_database_url
class TestNormalisation:
@pytest.mark.parametrize('given', [
'postgresql://user:pass@host:5432/tryouts',
'postgres://user:pass@host:5432/tryouts',
])
def test_a_driverless_postgres_url_gets_psycopg(self, given):
assert normalise_database_url(given).startswith('postgresql+psycopg://')
def test_the_rest_of_the_url_is_untouched(self):
result = normalise_database_url(
'postgresql://u:p%40s[email protected]:5432/tryouts?sslmode=require')
assert result == (
'postgresql+psycopg://u:p%40s[email protected]:5432/tryouts?sslmode=require')
@pytest.mark.parametrize('given', [
'postgresql+psycopg://user:pass@host/db',
'postgresql+psycopg2://user:pass@host/db',
])
def test_an_explicit_driver_is_left_alone(self, given):
"""Naming psycopg2 stays possible for an environment that has it."""
assert normalise_database_url(given) == given
def test_sqlite_is_left_alone(self):
assert normalise_database_url('sqlite:///x.db') == 'sqlite:///x.db'
@pytest.mark.parametrize('given', [None, '', 'not-a-url'])
def test_nothing_to_normalise(self, given):
assert normalise_database_url(given) == given
class TestTheFactory:
@staticmethod
def _config(uri):
return {
'SECRET_KEY': 'test-secret-not-used-anywhere-real',
'SQLALCHEMY_DATABASE_URI': uri,
'TESTING': True,
'WTF_CSRF_ENABLED': False,
'FORCE_HTTPS': False,
'SESSION_COOKIE_SECURE': False,
'ENABLE_DISCORD_BOT': False,
'AUTO_CREATE_TABLES': False,
'CORS_ALLOWED_ORIGINS': '',
'RATELIMIT_ENABLED': False,
}
def test_the_configured_uri_names_a_driver(self):
from app.app import create_app
app = create_app(self._config('postgresql://u:[email protected]:5432/tryouts'))
assert app.config['SQLALCHEMY_DATABASE_URI'].startswith('postgresql+psycopg://')
def test_the_sqlite_suite_is_unaffected(self, app):
assert app.config['SQLALCHEMY_DATABASE_URI'].startswith('sqlite:///')
class TestTheFailureIsReal:
"""Not a hypothetical: this is what the deployed configuration did."""
def test_psycopg2_is_not_installed(self):
with pytest.raises(ImportError):
import psycopg2 # noqa: F401
def test_a_driverless_url_cannot_build_an_engine(self):
with pytest.raises(ModuleNotFoundError):
create_engine('postgresql://u:p@host/db')
def test_the_normalised_url_can(self):
engine = create_engine(normalise_database_url('postgresql://u:p@host/db'))
assert engine.dialect.driver == 'psycopg'