8.4 KiB
AGENTS.md
Single-package Pyramid 2.0 blog app (ctp_blogr). Python 3.14, SQLite, Jinja2, WTForms,
SQLAlchemy 2.1, pyramid_tm transactions. UI strings, comments, and flash messages are
French; match that when editing views/templates.
Environment
The venv is the parent directory (/Users/phuoc/pyramid2), not inside the repo.
ctp_blogr is installed there in editable mode, so run commands from the repo root with
the venv's python (already on PATH as python). The venv is shared and also carries a
stale editable install of a wiki_test project whose source is gone — don't pip install
into it casually, and don't assume the venv contents match this repo's setup.py.
No linter, formatter, type-checker, pre-commit, CI workflow, or task runner exists. Don't invent one; don't claim you ran one.
Commands
python -m pytest # 2 tests, all in ctp_blogr/tests.py
python -m pytest ctp_blogr/tests.py::TestMyViewSuccessCondition # single test/class
pserve development.ini # waitress on localhost:9280 + debugtoolbar
pserve production.ini # binds *:9280, url_scheme https
pshell development.ini # preloaded: request, tm, dbsession, models
python -c "from pyramid.paster import get_app; get_app('development.ini')" # boot check
pytest.inisetstestpaths = ctp_blograndpython_files = test*.py, so a new test file must be namedtest*.pyinside the package to be collected.coverageis broken:.coveragercstill sayssource = pyramid_blogr(old package name).coverage run -m pytestcollects nothing. Fix the name before trusting any coverage number; don't report "no coverage" as a finding.initialize_ctp_blogr_dbis declared insetup.pybutctp_blogr/scripts/initialize_db.pydoes not exist (onlyscripts/__init__.py) — the console script fails withModuleNotFoundError. There is no working DB-init command.
Architecture / wiring
ctp_blogr/__init__.py::mainis the PasteDeploy app factory. It hardcodes the session secret and auth-ticket secret inline — rotating them logs everyone out.- Auth:
AuthTktAuthenticationPolicy+ACLAuthorizationPolicywithroot_factory='ctp_blogr.security.RootFactory'. The whole ACL is 3 lines insecurity.py:view→Authenticated,manage→group:administrators, thenDENY_ALL. - Admin is not a DB flag.
groupfinder(services/user.py) hardcodesuserid == 'admin'→['group:administrators']. Any other user gets[], i.e. logged in but no manage rights. Only/usersrequiresmanage. - Views bind to routes via
@view_config(route_name=...); routes are declared centrally inroutes.py. Adding a URL means editing both. models/__init__.pyincludespyramid_tm+pyramid_retryand reifiesrequest.dbsession.settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'is set there — removing it breaks commit/abort behaviour.- Write path: view →
request.dbsession.add(...)→ implicit commit bypyramid_tm. Do not callcommit()yourself. - Query layer lives in
services/(BlogRecordService,UserService) — all classmethods takingrequest. Keep DB access out of views. - Read filtering is duplicated per-method: anonymous users only ever see
status == 'publié'(by_criteria,by_tag,get_last_created).by_iddoes not filter — any post is readable by direct URL. Preserve that asymmetry or you change access behaviour. - Post bodies are Markdown;
views/blog.pyrunsmarkdown.markdown(..., extensions=['footnotes'])and rewritesstatic/→<app_url>/static/so relative image paths resolve. - Renderer strings are inconsistent on purpose-ish: most views use
'../templates/x.jinja2'(relative to the package), butdefault.py::user_edituses the asset spec'ctp_blogr:templates/user_edit.jinja2'. Both work; don't "normalize" one without checking the jinja2 search path. forms.pycontains dead forms from another app (HistoForm,AllocationForm,ActifForm,Actif2Form) matching orphaned DB tableshisto,allocation,actifs. Ignore them.
Database
ctp_blogr.sqliteis committed and is the real dev database (~99 posts, 8 tags, usersadminandphuoc). Bothdevelopment.iniandproduction.inipoint at it via%(here)s..gitignorelists*.sqlite, but the file is already tracked, so the ignore rule does nothing — every write view'spyramid_tmcommit is a real diff to review.- The DB contains an
alembic_versiontable (revb8f8216d72c7) but there is noalembic.iniand nomigrations/directory in this repo. Alembic is installed in the venv, yet nothing here is wired to it. So the schema cannot be regenerated. - The DB file is the source of truth for schema. Changing a model means: apply the
equivalent
ALTER TABLEtoctp_blogr.sqliteby hand, and tell the user migrations are missing rather than pretendingBase.metadata.create_all()covers them.create_all()is only used by the in-memory test fixture (tests.py::BaseTest.init_database). Tagsis defined inmodels/blog_record.pybut is not imported inmodels/__init__.py(onlyUserandBlogRecordare, beforeconfigure_mappers()). It still works only because importingblog_recorddefines the class as a side effect. Import it explicitly if you touch mapper config.
Known-broken (verified, do not re-diagnose)
views/default.py:142calls baredelete(request, user.id)—deleteis neither imported nor defined. Submitting the delete button on/user_editraisesNameError. The intended call isUserService.delete(request, user.id).views/default.py:18—homehardcodesBlogRecordService.by_id(request, 95)and then readsentry.title/entry.bodywith noNonecheck. The home page raisesAttributeErroron any database where id 95 doesn't exist (e.g. a freshly created one).home.jinja2:18hardcodes the same id for the "select another drawing" link.services/blog_record.pyusesquery.get(_id)— legacy SQLAlchemy 1.x API. Works on 2.1.3 but emitsLegacyAPIWarning; prefersession.get(Model, id)when touching that line.routes.py:5—'/blog/{id:\d+}/{slug}'is a non-raw string, so Python 3.12+ emitsSyntaxWarning: "\d" is an invalid escape sequence. Prefix withrif you edit it.models/meta.pyusessqlalchemy.ext.declarative.declarative_base(MovedIn20Warning).- Unauthorized requests return HTTP 200, not 403 (verified with and without
pyramid_debugtoolbar).forbidden_view_configrenderslogin.jinja2and the status stays 200. Don't write tests asserting 403, and don't assume 200 means "the page worked" — check for the login form when smoke-testing protected routes (/users,/tags,/blog_edit/{id},/tag_edit/{id},/user_edit/{name}).
Testing
ctp_blogr/tests.py has only two tests and they are duplicates that both assert the same
success case (test_failing_view is a copy-paste of test_passing_view). There is no coverage
of auth, forms, or the DB. BaseTest sets up sqlite:///:memory: with
Base.metadata.create_all() and wraps the session in transaction.manager, aborting in
teardown. WebTest is installed and is the right tool for new view tests — see
pyramid.testing.DummyRequest usage there for the lighter alternative.
Conventions
- 4-space indent, no type hints, no docstrings on views. Comments are frequently French and
slightly garbled English/French mixes (
# interdire le car '/' dans le titre) — leave them. @view_configrenderer paths and view return dicts follow the existing pattern; views return plain dicts consumed by the matching Jinja2 template.- Form submission is detected by string key presence:
if 'form.submitted' in request.params, plus'form.update_created'and'form.deleted'for the extra buttons. Buttons must setname="form.submitted"etc. del form.idbeforeform.populate_obj(entry)is a deliberate guard against overwriting the primary key. Keep it./blog_bytag/{tag}/{retour}—retouris a route name (request.route_url(retour)), not a URL. Callers pass e.g.retour='home'.- One commit, currently on
master.mainandorigin/*point at the same commit, so branch choice is not settled — ask before creating branches or pushing to the self-hostedhttps://git.caotek.fr/phuoc/ctp_blogr.git. Commit only when asked.