From 7a68954f6edab387f01897867f6792fa18d9229d Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Feb 2024 12:11:44 -0700 Subject: [PATCH 01/15] upgrade the wiki2/installation chapter --- docs/tutorials/wiki2/installation.rst | 60 +++++++++--------- .../wiki2/src/installation/.coveragerc | 2 - .../wiki2/src/installation/CHANGES.txt | 4 -- .../wiki2/src/installation/MANIFEST.in | 2 +- .../wiki2/src/installation/README.md | 60 ++++++++++++++++++ .../wiki2/src/installation/README.txt | 44 ------------- .../wiki2/src/installation/development.ini | 8 +-- .../wiki2/src/installation/production.ini | 8 +-- .../wiki2/src/installation/pyproject.toml | 58 ++++++++++++++++++ .../wiki2/src/installation/pytest.ini | 6 -- .../tutorials/wiki2/src/installation/setup.py | 61 ------------------- .../wiki2/src/installation/testing.ini | 8 +-- .../src/installation/tutorial/models/meta.py | 24 +++----- .../installation/tutorial/models/mymodel.py | 15 ++--- .../installation/tutorial/views/default.py | 8 +-- 15 files changed, 179 insertions(+), 189 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/installation/.coveragerc delete mode 100644 docs/tutorials/wiki2/src/installation/CHANGES.txt create mode 100644 docs/tutorials/wiki2/src/installation/README.md delete mode 100644 docs/tutorials/wiki2/src/installation/README.txt create mode 100644 docs/tutorials/wiki2/src/installation/pyproject.toml delete mode 100644 docs/tutorials/wiki2/src/installation/pytest.ini delete mode 100644 docs/tutorials/wiki2/src/installation/setup.py diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 844d11ca00..707a6a1091 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -177,19 +177,14 @@ The console will show ``pip`` checking for packages and installing missing packa .. code-block:: bash - Successfully installed Jinja2-2.11.2 Mako-1.1.3 MarkupSafe-1.1.1 PasteDeploy-2.1.1 Pygments-2.7.3 SQLAlchemy-1.3.22 WebTest-2.0.35 alembic-1.4.3 attrs-20.3.0 beautifulsoup4-4.9.3 coverage-5.3.1 hupper-1.10.2 iniconfig-1.1.1 packaging-20.8 plaster-1.0 plaster-pastedeploy-0.7 pluggy-0.13.1 py-1.10.0 pyparsing-2.4.7 pyramid-1.10.5 pyramid-debugtoolbar-4.9 pyramid-jinja2-2.8 pyramid-mako-1.1.0 pyramid-retry-2.1.1 pyramid-tm-2.4 pytest-6.2.1 pytest-cov-2.10.1 python-dateutil-2.8.1 python-editor-1.0.4 repoze.lru-0.7 six-1.15.0 soupsieve-2.1 toml-0.10.2 transaction-3.0.1 translationstring-1.4 tutorial venusian-3.0.0 waitress-1.4.4 webob-1.8.6 zope.deprecation-4.4.0 zope.interface-5.2.0 zope.sqlalchemy-1.3 + Successfully installed Mako-1.3.2 PasteDeploy-3.1.0 Pygments-2.17.2 SQLAlchemy-2.0.25 WebTest-3.0.0 alembic-1.13.1 beautifulsoup4-4.12.3 coverage-7.4.1 greenlet-3.0.3 hupper-1.12.1 iniconfig-2.0.0 jinja2-3.1.3 markupsafe-2.1.5 packaging-23.2 plaster-1.1.2 plaster-pastedeploy-1.0.1 pluggy-1.4.0 pyramid-2.0.2 pyramid-debugtoolbar-4.12 pyramid-jinja2-2.10 pyramid-mako-1.1.0 pyramid-retry-2.1.1 pyramid-tm-2.5 pytest-8.0.0 pytest-cov-4.1.0 soupsieve-2.5 transaction-4.0 translationstring-1.4 tutorial-0.0 typing-extensions-4.9.0 venusian-3.1.0 waitress-2.1.2 webob-1.8.7 zope.deprecation-5.0 zope.interface-6.1 zope.sqlalchemy-3.1 -Testing requirements are defined in our project's ``setup.py`` file, in the ``tests_require`` and ``extras_require`` stanzas. +Testing requirements are defined in our project's ``pyproject.toml`` file in a ``testing`` optional dependency. -.. literalinclude:: src/installation/setup.py +.. literalinclude:: src/installation/pyproject.toml :language: python :lineno-match: - :lines: 25-29 - -.. literalinclude:: src/installation/setup.py - :language: python - :lineno-match: - :lines: 49-51 + :lines: 34-39 .. _initialize_db_wiki2: @@ -219,11 +214,11 @@ The output to your console should be something like this: .. code-block:: text - 2021-01-07 05:15:57,709 INFO [alembic.runtime.migration:155][MainThread] Context impl SQLiteImpl. - 2021-01-07 05:15:57,709 INFO [alembic.runtime.migration:162][MainThread] Will assume non-transactional DDL. - 2021-01-07 05:15:57,712 INFO [alembic.autogenerate.compare:134][MainThread] Detected added table 'models' - 2021-01-07 05:15:57,712 INFO [alembic.autogenerate.compare:588][MainThread] Detected added index 'my_index' on '['name']' - Generating /tutorial/tutorial/alembic/versions/20210107_d7ab09c3fdec.py ... done + 2024-02-04 12:02:28,828 INFO [alembic.runtime.migration:216][MainThread] Context impl SQLiteImpl. + 2024-02-04 12:02:28,828 INFO [alembic.runtime.migration:219][MainThread] Will assume non-transactional DDL. + 2024-02-04 12:02:28,832 INFO [alembic.autogenerate.compare:189][MainThread] Detected added table 'models' + 2024-02-04 12:02:28,832 INFO [alembic.autogenerate.compare:633][MainThread] Detected added index ''my_index'' on '('name',)' + Generating /Users/michael/work/oss/pyramid/tutorial/tutorial/alembic/versions/20240204_4b6614165904.py ... done Upgrade to that revision. @@ -245,9 +240,9 @@ The output to your console should be something like this: .. code-block:: text - 2021-01-07 05:16:21,558 INFO [alembic.runtime.migration:155][MainThread] Context impl SQLiteImpl. - 2021-01-07 05:16:21,558 INFO [alembic.runtime.migration:162][MainThread] Will assume non-transactional DDL. - 2021-01-07 05:16:21,560 INFO [alembic.runtime.migration:517][MainThread] Running upgrade -> d7ab09c3fdec, init + 2024-02-04 12:03:04,738 INFO [alembic.runtime.migration:216][MainThread] Context impl SQLiteImpl. + 2024-02-04 12:03:04,738 INFO [alembic.runtime.migration:219][MainThread] Will assume non-transactional DDL. + 2024-02-04 12:03:04,739 INFO [alembic.runtime.migration:622][MainThread] Running upgrade -> 4b6614165904, init .. _load_data_wiki2: @@ -337,24 +332,26 @@ If successful, you will see output something like this: .. code-block:: bash - ======================== test session starts ======================== - platform darwin -- Python 3.9.0, pytest-6.2.1, py-1.10.0, pluggy-0.13.1 - rootdir: /tutorial, inifile: pytest.ini, testpaths: tutorial, tests - plugins: cov-2.10.1 + ====================================== test session starts ====================================== + platform darwin -- Python 3.11.7, pytest-8.0.0, pluggy-1.4.0 + rootdir: /Users/michael/work/oss/pyramid/tutorial + configfile: pyproject.toml + testpaths: tutorial, tests + plugins: cov-4.1.0 collected 5 items - tests/test_functional.py .. [ 40%] - tests/test_views.py ... [100%] - - ---------- coverage: platform darwin, python 3.9.0-final-0 ----------- + tests/test_functional.py .. [ 40%] + tests/test_views.py ... [100%] + + ---------- coverage: platform darwin, python 3.11.7-final-0 ---------- Name Stmts Miss Cover Missing ---------------------------------------------------------------------------------- tutorial/__init__.py 8 0 100% tutorial/alembic/env.py 23 4 83% 28-30, 56 - tutorial/alembic/versions/20200106_8c274fe5f3c4.py 12 2 83% 31-32 - tutorial/models/__init__.py 32 2 94% 71, 82 - tutorial/models/meta.py 5 0 100% - tutorial/models/mymodel.py 8 0 100% + tutorial/alembic/versions/20240204_4b6614165904.py 12 2 83% 31-32 + tutorial/models/__init__.py 32 2 94% 111, 122 + tutorial/models/meta.py 4 0 100% + tutorial/models/mymodel.py 10 0 100% tutorial/pshell.py 7 5 29% 5-13 tutorial/routes.py 3 0 100% tutorial/scripts/__init__.py 0 0 100% @@ -363,13 +360,12 @@ If successful, you will see output something like this: tutorial/views/default.py 13 0 100% tutorial/views/notfound.py 5 0 100% ---------------------------------------------------------------------------------- - TOTAL 138 27 80% + TOTAL 139 27 81% - ===================== 5 passed in 0.77 seconds ====================== + ================================= 5 passed, 6 warnings in 0.54s ================================= Our package doesn't quite have 100% test coverage. - .. _test_and_coverage_cookiecutter_defaults_sql: Test and coverage cookiecutter defaults diff --git a/docs/tutorials/wiki2/src/installation/.coveragerc b/docs/tutorials/wiki2/src/installation/.coveragerc deleted file mode 100644 index 5db0e79cff..0000000000 --- a/docs/tutorials/wiki2/src/installation/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[run] -source = tutorial diff --git a/docs/tutorials/wiki2/src/installation/CHANGES.txt b/docs/tutorials/wiki2/src/installation/CHANGES.txt deleted file mode 100644 index 14b902fd10..0000000000 --- a/docs/tutorials/wiki2/src/installation/CHANGES.txt +++ /dev/null @@ -1,4 +0,0 @@ -0.0 ---- - -- Initial version. diff --git a/docs/tutorials/wiki2/src/installation/MANIFEST.in b/docs/tutorials/wiki2/src/installation/MANIFEST.in index b4624fd1ca..201692c1bc 100644 --- a/docs/tutorials/wiki2/src/installation/MANIFEST.in +++ b/docs/tutorials/wiki2/src/installation/MANIFEST.in @@ -1,4 +1,4 @@ -include *.txt *.ini *.cfg *.rst +include *.txt *.ini *.cfg *.rst *.toml recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 recursive-include tests * recursive-exclude * __pycache__ diff --git a/docs/tutorials/wiki2/src/installation/README.md b/docs/tutorials/wiki2/src/installation/README.md new file mode 100644 index 0000000000..5ac1209d01 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/README.md @@ -0,0 +1,60 @@ +# myproj + +## Getting Started + +- Change directory into your newly created project if not already there. Your + current directory should be the same as this `README.md` file and `pyproject.toml`. + + ``` + cd tutorial + ``` + +- Create a Python virtual environment, if not already created. + + ``` + python3 -m venv env + ``` + +- Upgrade packaging tools, if necessary. + + ``` + env/bin/pip install --upgrade pip + ``` + +- Install the project in editable mode with its testing requirements. + + ``` + env/bin/pip install -e ".[testing]" + ``` + +- Initialize and upgrade the database using Alembic. + + - Generate your first revision. + + ``` + env/bin/alembic -c development.ini revision --autogenerate -m "init" + ``` + + - Upgrade to that revision. + + ``` + env/bin/alembic -c development.ini upgrade head + ``` + +- Load default data into the database using a script. + + ``` + env/bin/initialize_tutorial_db development.ini + ``` + +- Run your project's tests. + + ``` + env/bin/pytest + ``` + +- Run your project. + + ``` + env/bin/pserve development.ini + ``` diff --git a/docs/tutorials/wiki2/src/installation/README.txt b/docs/tutorials/wiki2/src/installation/README.txt deleted file mode 100644 index ed6b88b49e..0000000000 --- a/docs/tutorials/wiki2/src/installation/README.txt +++ /dev/null @@ -1,44 +0,0 @@ -myproj -====== - -Getting Started ---------------- - -- Change directory into your newly created project if not already there. Your - current directory should be the same as this README.txt file and setup.py. - - cd tutorial - -- Create a Python virtual environment, if not already created. - - python3 -m venv env - -- Upgrade packaging tools, if necessary. - - env/bin/pip install --upgrade pip setuptools - -- Install the project in editable mode with its testing requirements. - - env/bin/pip install -e ".[testing]" - -- Initialize and upgrade the database using Alembic. - - - Generate your first revision. - - env/bin/alembic -c development.ini revision --autogenerate -m "init" - - - Upgrade to that revision. - - env/bin/alembic -c development.ini upgrade head - -- Load default data into the database using a script. - - env/bin/initialize_tutorial_db development.ini - -- Run your project's tests. - - env/bin/pytest - -- Run your project. - - env/bin/pserve development.ini diff --git a/docs/tutorials/wiki2/src/installation/development.ini b/docs/tutorials/wiki2/src/installation/development.ini index f02c4b1b6d..e7baeed371 100644 --- a/docs/tutorials/wiki2/src/installation/development.ini +++ b/docs/tutorials/wiki2/src/installation/development.ini @@ -25,16 +25,16 @@ retry.attempts = 3 [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/installation/production.ini b/docs/tutorials/wiki2/src/installation/production.ini index f8e83f21f2..f636aaba31 100644 --- a/docs/tutorials/wiki2/src/installation/production.ini +++ b/docs/tutorials/wiki2/src/installation/production.ini @@ -19,16 +19,16 @@ retry.attempts = 3 [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = *:6543 diff --git a/docs/tutorials/wiki2/src/installation/pyproject.toml b/docs/tutorials/wiki2/src/installation/pyproject.toml new file mode 100644 index 0000000000..d03ba48820 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +version = "0.0" +name = "tutorial" +authors = [] +description = "myproj" +readme = "README.md" +keywords = ["web", "pyramid", "pylons"] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", +] +requires-python = ">=3.8" +dependencies = [ + "plaster_pastedeploy", + "pyramid", + "pyramid_jinja2", + "pyramid_debugtoolbar", + "waitress", + "alembic", + "pyramid_retry", + "pyramid_tm", + "SQLAlchemy", + "transaction", + "zope.sqlalchemy", +] + +[project.optional-dependencies] +testing = [ + "WebTest", + "pytest", + "pytest-cov", +] + +[project.scripts] +initialize_tutorial_db = "tutorial.scripts.initialize_db:main" + +[project.entry-points."paste.app_factory"] +main = "tutorial:main" + +[tool.setuptools.packages.find] +exclude = ["tests"] + +[tool.coverage.run] +source = "tutorial" + +[tool.pytest.ini_options] +addopts = "--strict-markers" +testpaths = [ + "tutorial", + "tests", +] diff --git a/docs/tutorials/wiki2/src/installation/pytest.ini b/docs/tutorials/wiki2/src/installation/pytest.ini deleted file mode 100644 index 3df78fe9dd..0000000000 --- a/docs/tutorials/wiki2/src/installation/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -addopts = --strict-markers - -testpaths = - tutorial - tests diff --git a/docs/tutorials/wiki2/src/installation/setup.py b/docs/tutorials/wiki2/src/installation/setup.py deleted file mode 100644 index 5e7a0111dd..0000000000 --- a/docs/tutorials/wiki2/src/installation/setup.py +++ /dev/null @@ -1,61 +0,0 @@ -import os - -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'README.txt')) as f: - README = f.read() -with open(os.path.join(here, 'CHANGES.txt')) as f: - CHANGES = f.read() - -requires = [ - 'plaster_pastedeploy', - 'pyramid', - 'pyramid_jinja2', - 'pyramid_debugtoolbar', - 'waitress', - 'alembic', - 'pyramid_retry', - 'pyramid_tm', - 'SQLAlchemy', - 'transaction', - 'zope.sqlalchemy', -] - -tests_require = [ - 'WebTest', - 'pytest', - 'pytest-cov', -] - -setup( - name='tutorial', - version='0.0', - description='myproj', - long_description=README + '\n\n' + CHANGES, - classifiers=[ - 'Programming Language :: Python', - 'Framework :: Pyramid', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', - ], - author='', - author_email='', - url='', - keywords='web pyramid pylons', - packages=find_packages(exclude=['tests']), - include_package_data=True, - zip_safe=False, - extras_require={ - 'testing': tests_require, - }, - install_requires=requires, - entry_points={ - 'paste.app_factory': [ - 'main = tutorial:main', - ], - 'console_scripts': [ - 'initialize_tutorial_db=tutorial.scripts.initialize_db:main', - ], - }, -) diff --git a/docs/tutorials/wiki2/src/installation/testing.ini b/docs/tutorials/wiki2/src/installation/testing.ini index 5caa1a8dc2..503cf30188 100644 --- a/docs/tutorials/wiki2/src/installation/testing.ini +++ b/docs/tutorials/wiki2/src/installation/testing.ini @@ -19,16 +19,16 @@ retry.attempts = 3 [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py b/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py index d659c78573..62d52fcf3e 100644 --- a/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py @@ -1,16 +1,12 @@ -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.schema import MetaData +from sqlalchemy import MetaData +from sqlalchemy.orm import DeclarativeBase -# Recommended naming convention used by Alembic, as various different database -# providers will autogenerate vastly different names making migrations more -# difficult. See: https://alembic.sqlalchemy.org/en/latest/naming.html -NAMING_CONVENTION = { - "ix": "ix_%(column_0_label)s", - "uq": "uq_%(table_name)s_%(column_0_name)s", - "ck": "ck_%(table_name)s_%(constraint_name)s", - "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", - "pk": "pk_%(table_name)s" -} -metadata = MetaData(naming_convention=NAMING_CONVENTION) -Base = declarative_base(metadata=metadata) +class Base(DeclarativeBase): + metadata = MetaData(naming_convention={ + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_`%(constraint_name)s`", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + }) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py index d65a01a422..947c7b65f0 100644 --- a/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py @@ -1,18 +1,15 @@ -from sqlalchemy import ( - Column, - Index, - Integer, - Text, -) +from sqlalchemy import Index, Integer, Text +from sqlalchemy.orm import Mapped, mapped_column +from typing import Optional from .meta import Base class MyModel(Base): __tablename__ = 'models' - id = Column(Integer, primary_key=True) - name = Column(Text) - value = Column(Integer) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[Optional[str]] + value: Mapped[Optional[int]] Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/views/default.py b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py index a0f654d385..3eeb7f26de 100644 --- a/docs/tutorials/wiki2/src/installation/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py @@ -1,6 +1,6 @@ from pyramid.view import view_config from pyramid.response import Response -from sqlalchemy.exc import SQLAlchemyError +import sqlalchemy as sa from .. import models @@ -8,9 +8,9 @@ @view_config(route_name='home', renderer='tutorial:templates/mytemplate.jinja2') def my_view(request): try: - query = request.dbsession.query(models.MyModel) - one = query.filter(models.MyModel.name == 'one').one() - except SQLAlchemyError: + query = sa.select(models.MyModel).where(models.MyModel.name == 'one') + one = request.dbsession.execute(query).scalar_one() + except sa.exc.SQLAlchemyError: return Response(db_err_msg, content_type='text/plain', status=500) return {'one': one, 'project': 'myproj'} From b44a55f6a9070b7531222e85f12fcc17fe0c255e Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Feb 2024 12:15:19 -0700 Subject: [PATCH 02/15] fix design chapter --- docs/tutorials/wiki2/design.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index e3b35d24af..f7cbc5fdc1 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -13,7 +13,7 @@ Overall We choose to use :term:`reStructuredText` markup in the wiki text. Translation from reStructuredText to HTML is provided by the widely used ``docutils`` Python module. We will add this module to the dependency list in the project's -``setup.py`` file. +``pyproject.toml`` file. Models ====== From 3a1d24397616a47f75b63b6367a78975dc546526 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Feb 2024 13:10:13 -0700 Subject: [PATCH 03/15] upgrade basiclayout chapter --- docs/tutorials/wiki2/basiclayout.rst | 25 ++------ .../wiki2/src/basiclayout/.coveragerc | 2 - .../wiki2/src/basiclayout/CHANGES.txt | 4 -- .../wiki2/src/basiclayout/MANIFEST.in | 2 +- .../tutorials/wiki2/src/basiclayout/README.md | 60 ++++++++++++++++++ .../wiki2/src/basiclayout/README.txt | 44 ------------- .../wiki2/src/basiclayout/development.ini | 8 +-- .../wiki2/src/basiclayout/production.ini | 8 +-- .../wiki2/src/basiclayout/pyproject.toml | 58 ++++++++++++++++++ .../wiki2/src/basiclayout/pytest.ini | 6 -- docs/tutorials/wiki2/src/basiclayout/setup.py | 61 ------------------- .../wiki2/src/basiclayout/testing.ini | 8 +-- .../src/basiclayout/tutorial/models/meta.py | 28 ++++----- .../basiclayout/tutorial/models/mymodel.py | 15 ++--- .../src/basiclayout/tutorial/views/default.py | 8 +-- .../src/installation/tutorial/models/meta.py | 4 ++ 16 files changed, 164 insertions(+), 177 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/basiclayout/.coveragerc delete mode 100644 docs/tutorials/wiki2/src/basiclayout/CHANGES.txt create mode 100644 docs/tutorials/wiki2/src/basiclayout/README.md delete mode 100644 docs/tutorials/wiki2/src/basiclayout/README.txt create mode 100644 docs/tutorials/wiki2/src/basiclayout/pyproject.toml delete mode 100644 docs/tutorials/wiki2/src/basiclayout/pytest.ini delete mode 100644 docs/tutorials/wiki2/src/basiclayout/setup.py diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index ef78e052b0..63171fa99f 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -218,29 +218,14 @@ following: :linenos: :language: py -``meta.py`` contains imports and support code for defining the models. We -create a dictionary ``NAMING_CONVENTION`` as well for consistent naming of -support objects like indices and constraints. +``meta.py`` contains imports and support code for defining the models. -.. literalinclude:: src/basiclayout/tutorial/models/meta.py - :end-before: metadata - :linenos: - :language: py - -Next we create a ``metadata`` object from the class -:class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value -for the ``naming_convention`` argument. +The core goal of ``meta.py`` is to define a declarative base class (``Base``) that links all of our models together into a shared :class:`sqlalchemy.schema.MetaData`. -A ``MetaData`` object represents the table and other schema definitions for a -single database. We also need to create a declarative ``Base`` object to use as -a base class for our models. Our models will inherit from this ``Base``, which -will attach the tables to the ``metadata`` we created, and define our -application's database schema. +We create the :class:`sqlalchemy.schema.MetaData` with a ``naming_convention`` to support properly naming objects when generating migrations with ``alembic``. -.. literalinclude:: src/basiclayout/tutorial/models/meta.py - :lines: 15-16 - :lineno-match: - :language: py +Any object inheriting from the new ``Base`` will be attached to ``metadata`` and +are able to reference eachother in relationships by name. Next open ``tutorial/models/mymodel.py``, which should already contain the following: diff --git a/docs/tutorials/wiki2/src/basiclayout/.coveragerc b/docs/tutorials/wiki2/src/basiclayout/.coveragerc deleted file mode 100644 index 5db0e79cff..0000000000 --- a/docs/tutorials/wiki2/src/basiclayout/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[run] -source = tutorial diff --git a/docs/tutorials/wiki2/src/basiclayout/CHANGES.txt b/docs/tutorials/wiki2/src/basiclayout/CHANGES.txt deleted file mode 100644 index 14b902fd10..0000000000 --- a/docs/tutorials/wiki2/src/basiclayout/CHANGES.txt +++ /dev/null @@ -1,4 +0,0 @@ -0.0 ---- - -- Initial version. diff --git a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in index b4624fd1ca..201692c1bc 100644 --- a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in +++ b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in @@ -1,4 +1,4 @@ -include *.txt *.ini *.cfg *.rst +include *.txt *.ini *.cfg *.rst *.toml recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 recursive-include tests * recursive-exclude * __pycache__ diff --git a/docs/tutorials/wiki2/src/basiclayout/README.md b/docs/tutorials/wiki2/src/basiclayout/README.md new file mode 100644 index 0000000000..5ac1209d01 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/README.md @@ -0,0 +1,60 @@ +# myproj + +## Getting Started + +- Change directory into your newly created project if not already there. Your + current directory should be the same as this `README.md` file and `pyproject.toml`. + + ``` + cd tutorial + ``` + +- Create a Python virtual environment, if not already created. + + ``` + python3 -m venv env + ``` + +- Upgrade packaging tools, if necessary. + + ``` + env/bin/pip install --upgrade pip + ``` + +- Install the project in editable mode with its testing requirements. + + ``` + env/bin/pip install -e ".[testing]" + ``` + +- Initialize and upgrade the database using Alembic. + + - Generate your first revision. + + ``` + env/bin/alembic -c development.ini revision --autogenerate -m "init" + ``` + + - Upgrade to that revision. + + ``` + env/bin/alembic -c development.ini upgrade head + ``` + +- Load default data into the database using a script. + + ``` + env/bin/initialize_tutorial_db development.ini + ``` + +- Run your project's tests. + + ``` + env/bin/pytest + ``` + +- Run your project. + + ``` + env/bin/pserve development.ini + ``` diff --git a/docs/tutorials/wiki2/src/basiclayout/README.txt b/docs/tutorials/wiki2/src/basiclayout/README.txt deleted file mode 100644 index ed6b88b49e..0000000000 --- a/docs/tutorials/wiki2/src/basiclayout/README.txt +++ /dev/null @@ -1,44 +0,0 @@ -myproj -====== - -Getting Started ---------------- - -- Change directory into your newly created project if not already there. Your - current directory should be the same as this README.txt file and setup.py. - - cd tutorial - -- Create a Python virtual environment, if not already created. - - python3 -m venv env - -- Upgrade packaging tools, if necessary. - - env/bin/pip install --upgrade pip setuptools - -- Install the project in editable mode with its testing requirements. - - env/bin/pip install -e ".[testing]" - -- Initialize and upgrade the database using Alembic. - - - Generate your first revision. - - env/bin/alembic -c development.ini revision --autogenerate -m "init" - - - Upgrade to that revision. - - env/bin/alembic -c development.ini upgrade head - -- Load default data into the database using a script. - - env/bin/initialize_tutorial_db development.ini - -- Run your project's tests. - - env/bin/pytest - -- Run your project. - - env/bin/pserve development.ini diff --git a/docs/tutorials/wiki2/src/basiclayout/development.ini b/docs/tutorials/wiki2/src/basiclayout/development.ini index f02c4b1b6d..e7baeed371 100644 --- a/docs/tutorials/wiki2/src/basiclayout/development.ini +++ b/docs/tutorials/wiki2/src/basiclayout/development.ini @@ -25,16 +25,16 @@ retry.attempts = 3 [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini index f8e83f21f2..f636aaba31 100644 --- a/docs/tutorials/wiki2/src/basiclayout/production.ini +++ b/docs/tutorials/wiki2/src/basiclayout/production.ini @@ -19,16 +19,16 @@ retry.attempts = 3 [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = *:6543 diff --git a/docs/tutorials/wiki2/src/basiclayout/pyproject.toml b/docs/tutorials/wiki2/src/basiclayout/pyproject.toml new file mode 100644 index 0000000000..d03ba48820 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +version = "0.0" +name = "tutorial" +authors = [] +description = "myproj" +readme = "README.md" +keywords = ["web", "pyramid", "pylons"] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", +] +requires-python = ">=3.8" +dependencies = [ + "plaster_pastedeploy", + "pyramid", + "pyramid_jinja2", + "pyramid_debugtoolbar", + "waitress", + "alembic", + "pyramid_retry", + "pyramid_tm", + "SQLAlchemy", + "transaction", + "zope.sqlalchemy", +] + +[project.optional-dependencies] +testing = [ + "WebTest", + "pytest", + "pytest-cov", +] + +[project.scripts] +initialize_tutorial_db = "tutorial.scripts.initialize_db:main" + +[project.entry-points."paste.app_factory"] +main = "tutorial:main" + +[tool.setuptools.packages.find] +exclude = ["tests"] + +[tool.coverage.run] +source = "tutorial" + +[tool.pytest.ini_options] +addopts = "--strict-markers" +testpaths = [ + "tutorial", + "tests", +] diff --git a/docs/tutorials/wiki2/src/basiclayout/pytest.ini b/docs/tutorials/wiki2/src/basiclayout/pytest.ini deleted file mode 100644 index 3df78fe9dd..0000000000 --- a/docs/tutorials/wiki2/src/basiclayout/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -addopts = --strict-markers - -testpaths = - tutorial - tests diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py deleted file mode 100644 index 5e7a0111dd..0000000000 --- a/docs/tutorials/wiki2/src/basiclayout/setup.py +++ /dev/null @@ -1,61 +0,0 @@ -import os - -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'README.txt')) as f: - README = f.read() -with open(os.path.join(here, 'CHANGES.txt')) as f: - CHANGES = f.read() - -requires = [ - 'plaster_pastedeploy', - 'pyramid', - 'pyramid_jinja2', - 'pyramid_debugtoolbar', - 'waitress', - 'alembic', - 'pyramid_retry', - 'pyramid_tm', - 'SQLAlchemy', - 'transaction', - 'zope.sqlalchemy', -] - -tests_require = [ - 'WebTest', - 'pytest', - 'pytest-cov', -] - -setup( - name='tutorial', - version='0.0', - description='myproj', - long_description=README + '\n\n' + CHANGES, - classifiers=[ - 'Programming Language :: Python', - 'Framework :: Pyramid', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', - ], - author='', - author_email='', - url='', - keywords='web pyramid pylons', - packages=find_packages(exclude=['tests']), - include_package_data=True, - zip_safe=False, - extras_require={ - 'testing': tests_require, - }, - install_requires=requires, - entry_points={ - 'paste.app_factory': [ - 'main = tutorial:main', - ], - 'console_scripts': [ - 'initialize_tutorial_db=tutorial.scripts.initialize_db:main', - ], - }, -) diff --git a/docs/tutorials/wiki2/src/basiclayout/testing.ini b/docs/tutorials/wiki2/src/basiclayout/testing.ini index 5caa1a8dc2..503cf30188 100644 --- a/docs/tutorials/wiki2/src/basiclayout/testing.ini +++ b/docs/tutorials/wiki2/src/basiclayout/testing.ini @@ -19,16 +19,16 @@ retry.attempts = 3 [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py index d659c78573..38712bfd2c 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py @@ -1,16 +1,16 @@ -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.schema import MetaData +from sqlalchemy import MetaData +from sqlalchemy.orm import DeclarativeBase -# Recommended naming convention used by Alembic, as various different database -# providers will autogenerate vastly different names making migrations more -# difficult. See: https://alembic.sqlalchemy.org/en/latest/naming.html -NAMING_CONVENTION = { - "ix": "ix_%(column_0_label)s", - "uq": "uq_%(table_name)s_%(column_0_name)s", - "ck": "ck_%(table_name)s_%(constraint_name)s", - "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", - "pk": "pk_%(table_name)s" -} -metadata = MetaData(naming_convention=NAMING_CONVENTION) -Base = declarative_base(metadata=metadata) +class Base(DeclarativeBase): + # Recommended naming convention used by Alembic, as various different + # database providers will autogenerate vastly different names making + # migrations more difficult. + # See: https://alembic.sqlalchemy.org/en/latest/naming.html + metadata = MetaData(naming_convention={ + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_`%(constraint_name)s`", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + }) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py index d65a01a422..947c7b65f0 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py @@ -1,18 +1,15 @@ -from sqlalchemy import ( - Column, - Index, - Integer, - Text, -) +from sqlalchemy import Index, Integer, Text +from sqlalchemy.orm import Mapped, mapped_column +from typing import Optional from .meta import Base class MyModel(Base): __tablename__ = 'models' - id = Column(Integer, primary_key=True) - name = Column(Text) - value = Column(Integer) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[Optional[str]] + value: Mapped[Optional[int]] Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py index a0f654d385..3eeb7f26de 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py @@ -1,6 +1,6 @@ from pyramid.view import view_config from pyramid.response import Response -from sqlalchemy.exc import SQLAlchemyError +import sqlalchemy as sa from .. import models @@ -8,9 +8,9 @@ @view_config(route_name='home', renderer='tutorial:templates/mytemplate.jinja2') def my_view(request): try: - query = request.dbsession.query(models.MyModel) - one = query.filter(models.MyModel.name == 'one').one() - except SQLAlchemyError: + query = sa.select(models.MyModel).where(models.MyModel.name == 'one') + one = request.dbsession.execute(query).scalar_one() + except sa.exc.SQLAlchemyError: return Response(db_err_msg, content_type='text/plain', status=500) return {'one': one, 'project': 'myproj'} diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py b/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py index 62d52fcf3e..38712bfd2c 100644 --- a/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py @@ -3,6 +3,10 @@ class Base(DeclarativeBase): + # Recommended naming convention used by Alembic, as various different + # database providers will autogenerate vastly different names making + # migrations more difficult. + # See: https://alembic.sqlalchemy.org/en/latest/naming.html metadata = MetaData(naming_convention={ "ix": "ix_%(column_0_label)s", "uq": "uq_%(table_name)s_%(column_0_name)s", From 28afd5d1e79d618ed0522029e926fd9219d31c0d Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Feb 2024 13:51:57 -0700 Subject: [PATCH 04/15] upgrade definingmodels chapter --- docs/tutorials/wiki2/definingmodels.rst | 82 ++++++++++--------- docs/tutorials/wiki2/installation.rst | 4 +- .../basiclayout/tutorial/models/mymodel.py | 2 +- .../installation/tutorial/models/mymodel.py | 2 +- docs/tutorials/wiki2/src/models/.coveragerc | 2 - docs/tutorials/wiki2/src/models/CHANGES.txt | 4 - docs/tutorials/wiki2/src/models/MANIFEST.in | 2 +- docs/tutorials/wiki2/src/models/README.md | 60 ++++++++++++++ docs/tutorials/wiki2/src/models/README.txt | 44 ---------- .../wiki2/src/models/development.ini | 8 +- .../tutorials/wiki2/src/models/production.ini | 8 +- .../tutorials/wiki2/src/models/pyproject.toml | 59 +++++++++++++ docs/tutorials/wiki2/src/models/pytest.ini | 6 -- docs/tutorials/wiki2/src/models/setup.py | 62 -------------- docs/tutorials/wiki2/src/models/testing.ini | 8 +- .../wiki2/src/models/tutorial/models/meta.py | 28 +++---- .../wiki2/src/models/tutorial/models/page.py | 20 ++--- .../wiki2/src/models/tutorial/models/user.py | 17 ++-- .../src/models/tutorial/views/default.py | 8 +- 19 files changed, 213 insertions(+), 213 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/models/.coveragerc delete mode 100644 docs/tutorials/wiki2/src/models/CHANGES.txt create mode 100644 docs/tutorials/wiki2/src/models/README.md delete mode 100644 docs/tutorials/wiki2/src/models/README.txt create mode 100644 docs/tutorials/wiki2/src/models/pyproject.toml delete mode 100644 docs/tutorials/wiki2/src/models/pytest.ini delete mode 100644 docs/tutorials/wiki2/src/models/setup.py diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index e419fdf0a2..b9b02ed127 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -17,22 +17,21 @@ be to define a wiki page :term:`domain model`. but this is only a convention and not a requirement. -Declaring dependencies in our ``setup.py`` file -=============================================== +Declaring dependencies in our ``pyproject.toml`` file +===================================================== The models code in our application will depend on a package which is not a dependency of the original "tutorial" application. The original "tutorial" application was generated by the cookiecutter; it doesn't know about our custom application requirements. -We need to add a dependency, the `bcrypt `_ package, to our ``tutorial`` -package's ``setup.py`` file by assigning this dependency to the ``requires`` -parameter in the ``setup()`` function. +We need to add a dependency, the `bcrypt `_ package, to our ``tutorial`` package's ``pyproject.toml`` file. +Dependencies are defined via the ``dependencies`` key in the ``[project]`` section. -Open ``tutorial/setup.py`` and edit it to look like the following by adding ``bcrypt`` and sorting the packages: +Open ``tutorial/pyproject.toml`` and edit it to look like the following by adding ``bcrypt`` and sorting the packages: -.. literalinclude:: src/models/setup.py - :lines: 11-24 +.. literalinclude:: src/models/pyproject.toml + :lines: 20-33 :linenos: :lineno-match: :emphasize-lines: 3 @@ -56,7 +55,7 @@ Since a new software dependency was added, you will need to run ``pip install the newly added dependency distribution. Make sure your current working directory is the root of the project (the -directory in which ``setup.py`` lives) and execute the following command. +directory in which ``pyproject.toml`` lives) and execute the following command. On Unix: @@ -75,7 +74,7 @@ like the following. .. code-block:: text - Successfully installed bcrypt-3.2.0 cffi-1.14.4 pycparser-2.20 tutorial + Successfully installed bcrypt-4.1.2 tutorial-0.0 Remove ``mymodel.py`` @@ -96,19 +95,24 @@ Create a new file ``tutorial/models/user.py`` with the following contents: This is a very basic model for a user who can authenticate with our wiki. -We discussed briefly in the previous chapter that our models will inherit from -an SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This will -attach the model to our schema. +We discussed briefly in the previous chapter that our models will inherit from an SQLAlchemy :func:`sqlalchemy.orm.DeclarativeBase`. +This will attach the model to our schema. -As you can see, our ``User`` class has a class-level attribute -``__tablename__`` which equals the string ``users``. Our ``User`` class will -also have class-level attributes named ``id``, ``name``, ``password_hash``, -and ``role`` (all instances of :class:`sqlalchemy.schema.Column`). These will -map to columns in the ``users`` table. The ``id`` attribute will be the primary -key in the table. The ``name`` attribute will be a text column, each value of -which needs to be unique within the column. The ``password_hash`` is a nullable -text attribute that will contain a securely hashed password. Finally, the -``role`` text attribute will hold the role of the user. +As you can see, our ``User`` class has a class-level attribute ``__tablename__`` which equals the string ``users``. +Our ``User`` class will also have class-level attributes named ``id``, ``name``, ``password_hash``, and ``role``. +These attributes will map to columns in the ``users`` table. +The ``id`` attribute will be the primary key in the table. +The ``name`` attribute will be a text column, each value of which needs to be unique within the column. +The ``password_hash`` is a nullable text attribute that will contain a securely hashed password. +Finally, the ``role`` text attribute will hold the role of the user. + +.. note:: + + Read more about how SQLAlchemy defines columns in the ORM at :ref:`sqla:orm_declarative_table_config_toplevel`. + Every column has a base schema inferred from the ``Mapped[...]`` type annotation. + This can define the Python type, for which SQLAlchemy maintains default database type mappings. + It also defines whether the attribute is nullable. + Any further schema attributes like primary keys, unique keys, or overrides to the database types, can be done by also defining the ``mapped_column``. There are two helper methods that will help us later when using the user objects. The first is ``set_password`` which will take a raw password and @@ -118,10 +122,11 @@ the hashed value of the submitted password against the hashed value of the password stored in the user's record in the database. If the two hashed values match, then the submitted password is valid, and we can authenticate the user. -We hash passwords so that it is impossible to decrypt them and use them to -authenticate in the application. If we stored passwords foolishly in clear -text, then anyone with access to the database could retrieve any password to -authenticate as any user. +We hash passwords so that it is impossible to decipher them and use them to authenticate in the application. +If we stored passwords foolishly in clear text and we lost control of the database, all of our users could be compromised. + +Notice that we configured a ``created_pages`` relationship, which will make more sense after you create the ``Page`` object in the next section. +This relationship is the reverse side of the ``creator`` one-to-many relationship on the ``Page`` and allows a user object to access a list of all pages in which the user is the creator. Add ``page.py`` @@ -141,7 +146,7 @@ here is the ``creator_id`` column, which is a foreign key referencing the want to relate ``User`` objects with ``Page`` objects, we also define a ``creator`` attribute as an ORM-level mapping between the two tables. SQLAlchemy will automatically populate this value using the foreign key -referencing the user. Since the foreign key has ``nullable=False``, we are +referencing the user. Since ``creator`` attribute / foreign key is not marked as optional, we are guaranteed that an instance of ``page`` will have a corresponding ``page.creator``, which will be a ``User`` instance. @@ -190,20 +195,19 @@ Success executing these commands will generate output similar to the following. .. code-block:: text - 2021-01-07 08:00:14,550 INFO [alembic.runtime.migration:155][MainThread] Context impl SQLiteImpl. - 2021-01-07 08:00:14,551 INFO [alembic.runtime.migration:158][MainThread] Will assume non-transactional DDL. - 2021-01-07 08:00:14,553 INFO [alembic.autogenerate.compare:134][MainThread] Detected added table 'users' - 2021-01-07 08:00:14,553 INFO [alembic.autogenerate.compare:134][MainThread] Detected added table 'pages' - 2021-01-07 08:00:14,558 INFO [alembic.autogenerate.compare:622][MainThread] Detected removed index 'my_index' on 'models' - 2021-01-07 08:00:14,558 INFO [alembic.autogenerate.compare:176][MainThread] Detected removed table 'models' - Generating /tutorial/tutorial/alembic/versions/20210107_bc9a3dead43a.py ... done + 2024-02-04 13:29:23,664 INFO [alembic.runtime.migration:216][MainThread] Context impl SQLiteImpl. + 2024-02-04 13:29:23,665 INFO [alembic.runtime.migration:219][MainThread] Will assume non-transactional DDL. + 2024-02-04 13:29:23,667 INFO [alembic.autogenerate.compare:189][MainThread] Detected added table 'users' + 2024-02-04 13:29:23,667 INFO [alembic.autogenerate.compare:189][MainThread] Detected added table 'pages' + 2024-02-04 13:29:23,672 INFO [alembic.autogenerate.compare:672][MainThread] Detected removed index 'my_index' on 'models' + 2024-02-04 13:29:23,672 INFO [alembic.autogenerate.compare:230][MainThread] Detected removed table 'models' + Generating /tutorial/alembic/versions/20240204_07f9d6b626b2.py ... done .. code-block:: text - 2021-01-07 08:00:21,318 INFO [alembic.runtime.migration:155][MainThread] Context impl SQLiteImpl. - 2021-01-07 08:00:21,318 INFO [alembic.runtime.migration:158][MainThread] Will assume non-transactional DDL. - 2021-01-07 08:00:21,320 INFO [alembic.runtime.migration:517][MainThread] Running upgrade 90658c4a9673 -> bc9a3dead43a, use new models Page and User - + 2024-02-04 13:32:48,735 INFO [alembic.runtime.migration:216][MainThread] Context impl SQLiteImpl. + 2024-02-04 13:32:48,735 INFO [alembic.runtime.migration:219][MainThread] Will assume non-transactional DDL. + 2024-02-04 13:32:48,737 INFO [alembic.runtime.migration:622][MainThread] Running upgrade 4b6614165904 -> 07f9d6b626b2, use new models Page and User .. _wiki2_alembic_overview: @@ -235,7 +239,7 @@ command, as we did in the installation step of this tutorial. .. note:: - The command is named ``initialize_tutorial_db`` because of the mapping defined in the ``[console_scripts]`` entry point of our project's ``setup.py`` file. + The command is named ``initialize_tutorial_db`` because of the mapping defined in the ``[project.scripts]`` section of our project's ``pyproject.toml`` file. Since we've changed our model, we need to make changes to our ``initialize_db.py`` script. In particular, we'll replace our import of diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 707a6a1091..072625f803 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -218,7 +218,7 @@ The output to your console should be something like this: 2024-02-04 12:02:28,828 INFO [alembic.runtime.migration:219][MainThread] Will assume non-transactional DDL. 2024-02-04 12:02:28,832 INFO [alembic.autogenerate.compare:189][MainThread] Detected added table 'models' 2024-02-04 12:02:28,832 INFO [alembic.autogenerate.compare:633][MainThread] Detected added index ''my_index'' on '('name',)' - Generating /Users/michael/work/oss/pyramid/tutorial/tutorial/alembic/versions/20240204_4b6614165904.py ... done + Generating /tutorial/tutorial/alembic/versions/20240204_4b6614165904.py ... done Upgrade to that revision. @@ -334,7 +334,7 @@ If successful, you will see output something like this: ====================================== test session starts ====================================== platform darwin -- Python 3.11.7, pytest-8.0.0, pluggy-1.4.0 - rootdir: /Users/michael/work/oss/pyramid/tutorial + rootdir: /tutorial configfile: pyproject.toml testpaths: tutorial, tests plugins: cov-4.1.0 diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py index 947c7b65f0..2b9c7da112 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py @@ -1,4 +1,4 @@ -from sqlalchemy import Index, Integer, Text +from sqlalchemy import Index from sqlalchemy.orm import Mapped, mapped_column from typing import Optional diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py index 947c7b65f0..2b9c7da112 100644 --- a/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py @@ -1,4 +1,4 @@ -from sqlalchemy import Index, Integer, Text +from sqlalchemy import Index from sqlalchemy.orm import Mapped, mapped_column from typing import Optional diff --git a/docs/tutorials/wiki2/src/models/.coveragerc b/docs/tutorials/wiki2/src/models/.coveragerc deleted file mode 100644 index 5db0e79cff..0000000000 --- a/docs/tutorials/wiki2/src/models/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[run] -source = tutorial diff --git a/docs/tutorials/wiki2/src/models/CHANGES.txt b/docs/tutorials/wiki2/src/models/CHANGES.txt deleted file mode 100644 index 14b902fd10..0000000000 --- a/docs/tutorials/wiki2/src/models/CHANGES.txt +++ /dev/null @@ -1,4 +0,0 @@ -0.0 ---- - -- Initial version. diff --git a/docs/tutorials/wiki2/src/models/MANIFEST.in b/docs/tutorials/wiki2/src/models/MANIFEST.in index b4624fd1ca..201692c1bc 100644 --- a/docs/tutorials/wiki2/src/models/MANIFEST.in +++ b/docs/tutorials/wiki2/src/models/MANIFEST.in @@ -1,4 +1,4 @@ -include *.txt *.ini *.cfg *.rst +include *.txt *.ini *.cfg *.rst *.toml recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 recursive-include tests * recursive-exclude * __pycache__ diff --git a/docs/tutorials/wiki2/src/models/README.md b/docs/tutorials/wiki2/src/models/README.md new file mode 100644 index 0000000000..5ac1209d01 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/README.md @@ -0,0 +1,60 @@ +# myproj + +## Getting Started + +- Change directory into your newly created project if not already there. Your + current directory should be the same as this `README.md` file and `pyproject.toml`. + + ``` + cd tutorial + ``` + +- Create a Python virtual environment, if not already created. + + ``` + python3 -m venv env + ``` + +- Upgrade packaging tools, if necessary. + + ``` + env/bin/pip install --upgrade pip + ``` + +- Install the project in editable mode with its testing requirements. + + ``` + env/bin/pip install -e ".[testing]" + ``` + +- Initialize and upgrade the database using Alembic. + + - Generate your first revision. + + ``` + env/bin/alembic -c development.ini revision --autogenerate -m "init" + ``` + + - Upgrade to that revision. + + ``` + env/bin/alembic -c development.ini upgrade head + ``` + +- Load default data into the database using a script. + + ``` + env/bin/initialize_tutorial_db development.ini + ``` + +- Run your project's tests. + + ``` + env/bin/pytest + ``` + +- Run your project. + + ``` + env/bin/pserve development.ini + ``` diff --git a/docs/tutorials/wiki2/src/models/README.txt b/docs/tutorials/wiki2/src/models/README.txt deleted file mode 100644 index ed6b88b49e..0000000000 --- a/docs/tutorials/wiki2/src/models/README.txt +++ /dev/null @@ -1,44 +0,0 @@ -myproj -====== - -Getting Started ---------------- - -- Change directory into your newly created project if not already there. Your - current directory should be the same as this README.txt file and setup.py. - - cd tutorial - -- Create a Python virtual environment, if not already created. - - python3 -m venv env - -- Upgrade packaging tools, if necessary. - - env/bin/pip install --upgrade pip setuptools - -- Install the project in editable mode with its testing requirements. - - env/bin/pip install -e ".[testing]" - -- Initialize and upgrade the database using Alembic. - - - Generate your first revision. - - env/bin/alembic -c development.ini revision --autogenerate -m "init" - - - Upgrade to that revision. - - env/bin/alembic -c development.ini upgrade head - -- Load default data into the database using a script. - - env/bin/initialize_tutorial_db development.ini - -- Run your project's tests. - - env/bin/pytest - -- Run your project. - - env/bin/pserve development.ini diff --git a/docs/tutorials/wiki2/src/models/development.ini b/docs/tutorials/wiki2/src/models/development.ini index f02c4b1b6d..e7baeed371 100644 --- a/docs/tutorials/wiki2/src/models/development.ini +++ b/docs/tutorials/wiki2/src/models/development.ini @@ -25,16 +25,16 @@ retry.attempts = 3 [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/models/production.ini b/docs/tutorials/wiki2/src/models/production.ini index f8e83f21f2..f636aaba31 100644 --- a/docs/tutorials/wiki2/src/models/production.ini +++ b/docs/tutorials/wiki2/src/models/production.ini @@ -19,16 +19,16 @@ retry.attempts = 3 [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = *:6543 diff --git a/docs/tutorials/wiki2/src/models/pyproject.toml b/docs/tutorials/wiki2/src/models/pyproject.toml new file mode 100644 index 0000000000..973944f98d --- /dev/null +++ b/docs/tutorials/wiki2/src/models/pyproject.toml @@ -0,0 +1,59 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +version = "0.0" +name = "tutorial" +authors = [] +description = "myproj" +readme = "README.md" +keywords = ["web", "pyramid", "pylons"] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", +] +requires-python = ">=3.8" +dependencies = [ + "alembic", + "bcrypt", + "plaster_pastedeploy", + "pyramid", + "pyramid_debugtoolbar", + "pyramid_jinja2", + "pyramid_retry", + "pyramid_tm", + "SQLAlchemy", + "transaction", + "waitress", + "zope.sqlalchemy", +] + +[project.optional-dependencies] +testing = [ + "WebTest", + "pytest", + "pytest-cov", +] + +[project.scripts] +initialize_tutorial_db = "tutorial.scripts.initialize_db:main" + +[project.entry-points."paste.app_factory"] +main = "tutorial:main" + +[tool.setuptools.packages.find] +exclude = ["tests"] + +[tool.coverage.run] +source = "tutorial" + +[tool.pytest.ini_options] +addopts = "--strict-markers" +testpaths = [ + "tutorial", + "tests", +] diff --git a/docs/tutorials/wiki2/src/models/pytest.ini b/docs/tutorials/wiki2/src/models/pytest.ini deleted file mode 100644 index 3df78fe9dd..0000000000 --- a/docs/tutorials/wiki2/src/models/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -addopts = --strict-markers - -testpaths = - tutorial - tests diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py deleted file mode 100644 index fbd848136e..0000000000 --- a/docs/tutorials/wiki2/src/models/setup.py +++ /dev/null @@ -1,62 +0,0 @@ -import os - -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'README.txt')) as f: - README = f.read() -with open(os.path.join(here, 'CHANGES.txt')) as f: - CHANGES = f.read() - -requires = [ - 'alembic', - 'bcrypt', - 'plaster_pastedeploy', - 'pyramid', - 'pyramid_debugtoolbar', - 'pyramid_jinja2', - 'pyramid_retry', - 'pyramid_tm', - 'SQLAlchemy', - 'transaction', - 'waitress', - 'zope.sqlalchemy', -] - -tests_require = [ - 'WebTest', - 'pytest', - 'pytest-cov', -] - -setup( - name='tutorial', - version='0.0', - description='myproj', - long_description=README + '\n\n' + CHANGES, - classifiers=[ - 'Programming Language :: Python', - 'Framework :: Pyramid', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', - ], - author='', - author_email='', - url='', - keywords='web pyramid pylons', - packages=find_packages(exclude=['tests']), - include_package_data=True, - zip_safe=False, - extras_require={ - 'testing': tests_require, - }, - install_requires=requires, - entry_points={ - 'paste.app_factory': [ - 'main = tutorial:main', - ], - 'console_scripts': [ - 'initialize_tutorial_db=tutorial.scripts.initialize_db:main', - ], - }, -) diff --git a/docs/tutorials/wiki2/src/models/testing.ini b/docs/tutorials/wiki2/src/models/testing.ini index 5caa1a8dc2..503cf30188 100644 --- a/docs/tutorials/wiki2/src/models/testing.ini +++ b/docs/tutorials/wiki2/src/models/testing.ini @@ -19,16 +19,16 @@ retry.attempts = 3 [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py index d659c78573..38712bfd2c 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py @@ -1,16 +1,16 @@ -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.schema import MetaData +from sqlalchemy import MetaData +from sqlalchemy.orm import DeclarativeBase -# Recommended naming convention used by Alembic, as various different database -# providers will autogenerate vastly different names making migrations more -# difficult. See: https://alembic.sqlalchemy.org/en/latest/naming.html -NAMING_CONVENTION = { - "ix": "ix_%(column_0_label)s", - "uq": "uq_%(table_name)s_%(column_0_name)s", - "ck": "ck_%(table_name)s_%(constraint_name)s", - "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", - "pk": "pk_%(table_name)s" -} -metadata = MetaData(naming_convention=NAMING_CONVENTION) -Base = declarative_base(metadata=metadata) +class Base(DeclarativeBase): + # Recommended naming convention used by Alembic, as various different + # database providers will autogenerate vastly different names making + # migrations more difficult. + # See: https://alembic.sqlalchemy.org/en/latest/naming.html + metadata = MetaData(naming_convention={ + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_`%(constraint_name)s`", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + }) diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/page.py b/docs/tutorials/wiki2/src/models/tutorial/models/page.py index 74ff1faf85..5a1c885c92 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/page.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/page.py @@ -1,10 +1,6 @@ -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - Text, -) -from sqlalchemy.orm import relationship +import bcrypt +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship from .meta import Base @@ -12,9 +8,9 @@ class Page(Base): """ The SQLAlchemy declarative model class for a Page object. """ __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, nullable=False, unique=True) - data = Column(Text, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(unique=True) + data: Mapped[str] - creator_id = Column(ForeignKey('users.id'), nullable=False) - creator = relationship('User', backref='created_pages') + creator_id: Mapped[int] = mapped_column(ForeignKey('users.id')) + creator: Mapped['User'] = relationship('User', back_populates='created_pages') diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py index 9228b48f74..d85d890c63 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py @@ -1,9 +1,6 @@ import bcrypt -from sqlalchemy import ( - Column, - Integer, - Text, -) +from sqlalchemy.orm import Mapped, mapped_column, relationship +from typing import Optional from .meta import Base @@ -11,11 +8,11 @@ class User(Base): """ The SQLAlchemy declarative model class for a User object. """ __tablename__ = 'users' - id = Column(Integer, primary_key=True) - name = Column(Text, nullable=False, unique=True) - role = Column(Text, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(unique=True) + role: Mapped[str] - password_hash = Column(Text) + password_hash: Mapped[Optional[str]] def set_password(self, pw): pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) @@ -26,3 +23,5 @@ def check_password(self, pw): expected_hash = self.password_hash.encode('utf8') return bcrypt.checkpw(pw.encode('utf8'), expected_hash) return False + + created_pages: Mapped['Page'] = relationship('Page', back_populates='creator') diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/default.py b/docs/tutorials/wiki2/src/models/tutorial/views/default.py index a0f654d385..3eeb7f26de 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/models/tutorial/views/default.py @@ -1,6 +1,6 @@ from pyramid.view import view_config from pyramid.response import Response -from sqlalchemy.exc import SQLAlchemyError +import sqlalchemy as sa from .. import models @@ -8,9 +8,9 @@ @view_config(route_name='home', renderer='tutorial:templates/mytemplate.jinja2') def my_view(request): try: - query = request.dbsession.query(models.MyModel) - one = query.filter(models.MyModel.name == 'one').one() - except SQLAlchemyError: + query = sa.select(models.MyModel).where(models.MyModel.name == 'one') + one = request.dbsession.execute(query).scalar_one() + except sa.exc.SQLAlchemyError: return Response(db_err_msg, content_type='text/plain', status=500) return {'one': one, 'project': 'myproj'} From 569f0a53d0bedecceb1365a0159c9fe514f198a9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Feb 2024 14:41:18 -0700 Subject: [PATCH 05/15] add missing migration files --- .../alembic/versions/20240204_4b6614165904.py | 33 ++++++++++++ .../alembic/versions/20240204_4b6614165904.py | 33 ++++++++++++ .../alembic/versions/20240204_07f9d6b626b2.py | 52 +++++++++++++++++++ .../alembic/versions/20240204_4b6614165904.py | 33 ++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/versions/20240204_4b6614165904.py create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/alembic/versions/20240204_4b6614165904.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/alembic/versions/20240204_07f9d6b626b2.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/alembic/versions/20240204_4b6614165904.py diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/versions/20240204_4b6614165904.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/versions/20240204_4b6614165904.py new file mode 100644 index 0000000000..4f7a97d300 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/versions/20240204_4b6614165904.py @@ -0,0 +1,33 @@ +"""init + +Revision ID: 4b6614165904 +Revises: +Create Date: 2024-02-04 14:32:46.784813 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4b6614165904' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('models', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('value', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_models')) + ) + op.create_index('my_index', 'models', ['name'], unique=True, mysql_length=255) + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('my_index', table_name='models', mysql_length=255) + op.drop_table('models') + # ### end Alembic commands ### diff --git a/docs/tutorials/wiki2/src/installation/tutorial/alembic/versions/20240204_4b6614165904.py b/docs/tutorials/wiki2/src/installation/tutorial/alembic/versions/20240204_4b6614165904.py new file mode 100644 index 0000000000..4f7a97d300 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/alembic/versions/20240204_4b6614165904.py @@ -0,0 +1,33 @@ +"""init + +Revision ID: 4b6614165904 +Revises: +Create Date: 2024-02-04 14:32:46.784813 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4b6614165904' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('models', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('value', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_models')) + ) + op.create_index('my_index', 'models', ['name'], unique=True, mysql_length=255) + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('my_index', table_name='models', mysql_length=255) + op.drop_table('models') + # ### end Alembic commands ### diff --git a/docs/tutorials/wiki2/src/models/tutorial/alembic/versions/20240204_07f9d6b626b2.py b/docs/tutorials/wiki2/src/models/tutorial/alembic/versions/20240204_07f9d6b626b2.py new file mode 100644 index 0000000000..329c002083 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/alembic/versions/20240204_07f9d6b626b2.py @@ -0,0 +1,52 @@ +"""use new models Page and User + +Revision ID: 07f9d6b626b2 +Revises: 4b6614165904 +Create Date: 2024-02-04 14:39:12.885858 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '07f9d6b626b2' +down_revision = '4b6614165904' +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('role', sa.String(), nullable=False), + sa.Column('password_hash', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_users')), + sa.UniqueConstraint('name', name=op.f('uq_users_name')) + ) + op.create_table('pages', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('data', sa.String(), nullable=False), + sa.Column('creator_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['creator_id'], ['users.id'], name=op.f('fk_pages_creator_id_users')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_pages')), + sa.UniqueConstraint('name', name=op.f('uq_pages_name')) + ) + op.drop_index('my_index', table_name='models') + op.drop_table('models') + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('models', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.VARCHAR(), nullable=True), + sa.Column('value', sa.INTEGER(), nullable=True), + sa.PrimaryKeyConstraint('id', name='pk_models') + ) + op.create_index('my_index', 'models', ['name'], unique=1) + op.drop_table('pages') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/docs/tutorials/wiki2/src/models/tutorial/alembic/versions/20240204_4b6614165904.py b/docs/tutorials/wiki2/src/models/tutorial/alembic/versions/20240204_4b6614165904.py new file mode 100644 index 0000000000..4f7a97d300 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/alembic/versions/20240204_4b6614165904.py @@ -0,0 +1,33 @@ +"""init + +Revision ID: 4b6614165904 +Revises: +Create Date: 2024-02-04 14:32:46.784813 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4b6614165904' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('models', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('value', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_models')) + ) + op.create_index('my_index', 'models', ['name'], unique=True, mysql_length=255) + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('my_index', table_name='models', mysql_length=255) + op.drop_table('models') + # ### end Alembic commands ### From d3c8d93cfb1282f328ecd133df6565a2df92779f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Feb 2024 15:29:37 -0700 Subject: [PATCH 06/15] update the definingviews chapter --- docs/tutorials/wiki2/definingmodels.rst | 2 +- docs/tutorials/wiki2/definingviews.rst | 19 +++--- .../wiki2/src/models/tutorial/models/page.py | 2 +- .../wiki2/src/models/tutorial/models/user.py | 4 +- docs/tutorials/wiki2/src/views/.coveragerc | 2 - docs/tutorials/wiki2/src/views/CHANGES.txt | 4 -- docs/tutorials/wiki2/src/views/MANIFEST.in | 2 +- docs/tutorials/wiki2/src/views/README.md | 60 ++++++++++++++++++ docs/tutorials/wiki2/src/views/README.txt | 44 ------------- .../tutorials/wiki2/src/views/development.ini | 8 +-- docs/tutorials/wiki2/src/views/production.ini | 8 +-- docs/tutorials/wiki2/src/views/pyproject.toml | 60 ++++++++++++++++++ docs/tutorials/wiki2/src/views/pytest.ini | 6 -- docs/tutorials/wiki2/src/views/setup.py | 63 ------------------- docs/tutorials/wiki2/src/views/testing.ini | 8 +-- .../alembic/versions/20240204_07f9d6b626b2.py | 52 +++++++++++++++ .../alembic/versions/20240204_4b6614165904.py | 33 ++++++++++ .../wiki2/src/views/tutorial/models/meta.py | 28 ++++----- .../wiki2/src/views/tutorial/models/page.py | 20 +++--- .../wiki2/src/views/tutorial/models/user.py | 17 +++-- .../wiki2/src/views/tutorial/views/default.py | 27 +++++--- 21 files changed, 281 insertions(+), 188 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/views/.coveragerc delete mode 100644 docs/tutorials/wiki2/src/views/CHANGES.txt create mode 100644 docs/tutorials/wiki2/src/views/README.md delete mode 100644 docs/tutorials/wiki2/src/views/README.txt create mode 100644 docs/tutorials/wiki2/src/views/pyproject.toml delete mode 100644 docs/tutorials/wiki2/src/views/pytest.ini delete mode 100644 docs/tutorials/wiki2/src/views/setup.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/alembic/versions/20240204_07f9d6b626b2.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/alembic/versions/20240204_4b6614165904.py diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index b9b02ed127..45a3167a7d 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -28,7 +28,7 @@ custom application requirements. We need to add a dependency, the `bcrypt `_ package, to our ``tutorial`` package's ``pyproject.toml`` file. Dependencies are defined via the ``dependencies`` key in the ``[project]`` section. -Open ``tutorial/pyproject.toml`` and edit it to look like the following by adding ``bcrypt`` and sorting the packages: +Open ``pyproject.toml`` and edit it to look like the following by adding ``bcrypt`` and sorting the packages: .. literalinclude:: src/models/pyproject.toml :lines: 20-33 diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index c4712faf08..8c397f8809 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -24,14 +24,13 @@ Remember in the previous chapter we added a new dependency of the ``bcrypt`` package. Again, the view code in our application will depend on a package which is not a dependency of the original "tutorial" application. -We need to add a dependency on the ``docutils`` package to our ``tutorial`` -package's ``setup.py`` file by assigning this dependency to the ``requires`` -list. +We need to add a dependency on the ``docutils`` package, to our ``tutorial`` package's ``pyproject.toml`` file. +Dependencies are defined via the ``dependencies`` key in the ``[project]`` section. -Open ``tutorial/setup.py`` and edit it to look like the following: +Open ``pyproject.toml`` and edit it to look like the following: -.. literalinclude:: src/views/setup.py - :lines: 11-25 +.. literalinclude:: src/views/pyproject.toml + :lines: 20-34 :lineno-match: :emphasize-lines: 4 :language: python @@ -186,7 +185,7 @@ The ``view_wiki`` view function Following is the code for the ``view_wiki`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 16-19 + :lines: 17-20 :lineno-match: :linenos: :language: python @@ -211,7 +210,7 @@ The ``view_page`` view function Here is the code for the ``view_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 21-41 + :lines: 22-46 :lineno-match: :linenos: :language: python @@ -264,7 +263,7 @@ The ``edit_page`` view function Here is the code for the ``edit_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 43-55 + :lines: 48-64 :lineno-match: :linenos: :language: python @@ -301,7 +300,7 @@ The ``add_page`` view function Here is the code for the ``add_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 57- + :lines: 66- :lineno-match: :linenos: :language: python diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/page.py b/docs/tutorials/wiki2/src/models/tutorial/models/page.py index 5a1c885c92..6cc9aae3cb 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/page.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/page.py @@ -13,4 +13,4 @@ class Page(Base): data: Mapped[str] creator_id: Mapped[int] = mapped_column(ForeignKey('users.id')) - creator: Mapped['User'] = relationship('User', back_populates='created_pages') + creator: Mapped['User'] = relationship(back_populates='created_pages') diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py index d85d890c63..926a66e645 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py @@ -1,6 +1,6 @@ import bcrypt from sqlalchemy.orm import Mapped, mapped_column, relationship -from typing import Optional +from typing import List, Optional from .meta import Base @@ -24,4 +24,4 @@ def check_password(self, pw): return bcrypt.checkpw(pw.encode('utf8'), expected_hash) return False - created_pages: Mapped['Page'] = relationship('Page', back_populates='creator') + created_pages: Mapped[List['Page']] = relationship(back_populates='creator') diff --git a/docs/tutorials/wiki2/src/views/.coveragerc b/docs/tutorials/wiki2/src/views/.coveragerc deleted file mode 100644 index 5db0e79cff..0000000000 --- a/docs/tutorials/wiki2/src/views/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[run] -source = tutorial diff --git a/docs/tutorials/wiki2/src/views/CHANGES.txt b/docs/tutorials/wiki2/src/views/CHANGES.txt deleted file mode 100644 index 14b902fd10..0000000000 --- a/docs/tutorials/wiki2/src/views/CHANGES.txt +++ /dev/null @@ -1,4 +0,0 @@ -0.0 ---- - -- Initial version. diff --git a/docs/tutorials/wiki2/src/views/MANIFEST.in b/docs/tutorials/wiki2/src/views/MANIFEST.in index b4624fd1ca..201692c1bc 100644 --- a/docs/tutorials/wiki2/src/views/MANIFEST.in +++ b/docs/tutorials/wiki2/src/views/MANIFEST.in @@ -1,4 +1,4 @@ -include *.txt *.ini *.cfg *.rst +include *.txt *.ini *.cfg *.rst *.toml recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 recursive-include tests * recursive-exclude * __pycache__ diff --git a/docs/tutorials/wiki2/src/views/README.md b/docs/tutorials/wiki2/src/views/README.md new file mode 100644 index 0000000000..5ac1209d01 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/README.md @@ -0,0 +1,60 @@ +# myproj + +## Getting Started + +- Change directory into your newly created project if not already there. Your + current directory should be the same as this `README.md` file and `pyproject.toml`. + + ``` + cd tutorial + ``` + +- Create a Python virtual environment, if not already created. + + ``` + python3 -m venv env + ``` + +- Upgrade packaging tools, if necessary. + + ``` + env/bin/pip install --upgrade pip + ``` + +- Install the project in editable mode with its testing requirements. + + ``` + env/bin/pip install -e ".[testing]" + ``` + +- Initialize and upgrade the database using Alembic. + + - Generate your first revision. + + ``` + env/bin/alembic -c development.ini revision --autogenerate -m "init" + ``` + + - Upgrade to that revision. + + ``` + env/bin/alembic -c development.ini upgrade head + ``` + +- Load default data into the database using a script. + + ``` + env/bin/initialize_tutorial_db development.ini + ``` + +- Run your project's tests. + + ``` + env/bin/pytest + ``` + +- Run your project. + + ``` + env/bin/pserve development.ini + ``` diff --git a/docs/tutorials/wiki2/src/views/README.txt b/docs/tutorials/wiki2/src/views/README.txt deleted file mode 100644 index ed6b88b49e..0000000000 --- a/docs/tutorials/wiki2/src/views/README.txt +++ /dev/null @@ -1,44 +0,0 @@ -myproj -====== - -Getting Started ---------------- - -- Change directory into your newly created project if not already there. Your - current directory should be the same as this README.txt file and setup.py. - - cd tutorial - -- Create a Python virtual environment, if not already created. - - python3 -m venv env - -- Upgrade packaging tools, if necessary. - - env/bin/pip install --upgrade pip setuptools - -- Install the project in editable mode with its testing requirements. - - env/bin/pip install -e ".[testing]" - -- Initialize and upgrade the database using Alembic. - - - Generate your first revision. - - env/bin/alembic -c development.ini revision --autogenerate -m "init" - - - Upgrade to that revision. - - env/bin/alembic -c development.ini upgrade head - -- Load default data into the database using a script. - - env/bin/initialize_tutorial_db development.ini - -- Run your project's tests. - - env/bin/pytest - -- Run your project. - - env/bin/pserve development.ini diff --git a/docs/tutorials/wiki2/src/views/development.ini b/docs/tutorials/wiki2/src/views/development.ini index f02c4b1b6d..e7baeed371 100644 --- a/docs/tutorials/wiki2/src/views/development.ini +++ b/docs/tutorials/wiki2/src/views/development.ini @@ -25,16 +25,16 @@ retry.attempts = 3 [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/views/production.ini b/docs/tutorials/wiki2/src/views/production.ini index f8e83f21f2..f636aaba31 100644 --- a/docs/tutorials/wiki2/src/views/production.ini +++ b/docs/tutorials/wiki2/src/views/production.ini @@ -19,16 +19,16 @@ retry.attempts = 3 [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = *:6543 diff --git a/docs/tutorials/wiki2/src/views/pyproject.toml b/docs/tutorials/wiki2/src/views/pyproject.toml new file mode 100644 index 0000000000..2b054c5784 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +version = "0.0" +name = "tutorial" +authors = [] +description = "myproj" +readme = "README.md" +keywords = ["web", "pyramid", "pylons"] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", +] +requires-python = ">=3.8" +dependencies = [ + "alembic", + "bcrypt", + "docutils", + "plaster_pastedeploy", + "pyramid", + "pyramid_debugtoolbar", + "pyramid_jinja2", + "pyramid_retry", + "pyramid_tm", + "SQLAlchemy", + "transaction", + "waitress", + "zope.sqlalchemy", +] + +[project.optional-dependencies] +testing = [ + "WebTest", + "pytest", + "pytest-cov", +] + +[project.scripts] +initialize_tutorial_db = "tutorial.scripts.initialize_db:main" + +[project.entry-points."paste.app_factory"] +main = "tutorial:main" + +[tool.setuptools.packages.find] +exclude = ["tests"] + +[tool.coverage.run] +source = "tutorial" + +[tool.pytest.ini_options] +addopts = "--strict-markers" +testpaths = [ + "tutorial", + "tests", +] diff --git a/docs/tutorials/wiki2/src/views/pytest.ini b/docs/tutorials/wiki2/src/views/pytest.ini deleted file mode 100644 index 3df78fe9dd..0000000000 --- a/docs/tutorials/wiki2/src/views/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -addopts = --strict-markers - -testpaths = - tutorial - tests diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py deleted file mode 100644 index 12eabaff26..0000000000 --- a/docs/tutorials/wiki2/src/views/setup.py +++ /dev/null @@ -1,63 +0,0 @@ -import os - -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'README.txt')) as f: - README = f.read() -with open(os.path.join(here, 'CHANGES.txt')) as f: - CHANGES = f.read() - -requires = [ - 'alembic', - 'bcrypt', - 'docutils', - 'plaster_pastedeploy', - 'pyramid', - 'pyramid_debugtoolbar', - 'pyramid_jinja2', - 'pyramid_retry', - 'pyramid_tm', - 'SQLAlchemy', - 'transaction', - 'waitress', - 'zope.sqlalchemy', -] - -tests_require = [ - 'WebTest', - 'pytest', - 'pytest-cov', -] - -setup( - name='tutorial', - version='0.0', - description='myproj', - long_description=README + '\n\n' + CHANGES, - classifiers=[ - 'Programming Language :: Python', - 'Framework :: Pyramid', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', - ], - author='', - author_email='', - url='', - keywords='web pyramid pylons', - packages=find_packages(exclude=['tests']), - include_package_data=True, - zip_safe=False, - extras_require={ - 'testing': tests_require, - }, - install_requires=requires, - entry_points={ - 'paste.app_factory': [ - 'main = tutorial:main', - ], - 'console_scripts': [ - 'initialize_tutorial_db=tutorial.scripts.initialize_db:main', - ], - }, -) diff --git a/docs/tutorials/wiki2/src/views/testing.ini b/docs/tutorials/wiki2/src/views/testing.ini index 5caa1a8dc2..503cf30188 100644 --- a/docs/tutorials/wiki2/src/views/testing.ini +++ b/docs/tutorials/wiki2/src/views/testing.ini @@ -19,16 +19,16 @@ retry.attempts = 3 [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/views/tutorial/alembic/versions/20240204_07f9d6b626b2.py b/docs/tutorials/wiki2/src/views/tutorial/alembic/versions/20240204_07f9d6b626b2.py new file mode 100644 index 0000000000..329c002083 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/alembic/versions/20240204_07f9d6b626b2.py @@ -0,0 +1,52 @@ +"""use new models Page and User + +Revision ID: 07f9d6b626b2 +Revises: 4b6614165904 +Create Date: 2024-02-04 14:39:12.885858 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '07f9d6b626b2' +down_revision = '4b6614165904' +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('role', sa.String(), nullable=False), + sa.Column('password_hash', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_users')), + sa.UniqueConstraint('name', name=op.f('uq_users_name')) + ) + op.create_table('pages', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('data', sa.String(), nullable=False), + sa.Column('creator_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['creator_id'], ['users.id'], name=op.f('fk_pages_creator_id_users')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_pages')), + sa.UniqueConstraint('name', name=op.f('uq_pages_name')) + ) + op.drop_index('my_index', table_name='models') + op.drop_table('models') + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('models', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.VARCHAR(), nullable=True), + sa.Column('value', sa.INTEGER(), nullable=True), + sa.PrimaryKeyConstraint('id', name='pk_models') + ) + op.create_index('my_index', 'models', ['name'], unique=1) + op.drop_table('pages') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/docs/tutorials/wiki2/src/views/tutorial/alembic/versions/20240204_4b6614165904.py b/docs/tutorials/wiki2/src/views/tutorial/alembic/versions/20240204_4b6614165904.py new file mode 100644 index 0000000000..4f7a97d300 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/alembic/versions/20240204_4b6614165904.py @@ -0,0 +1,33 @@ +"""init + +Revision ID: 4b6614165904 +Revises: +Create Date: 2024-02-04 14:32:46.784813 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4b6614165904' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('models', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('value', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_models')) + ) + op.create_index('my_index', 'models', ['name'], unique=True, mysql_length=255) + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('my_index', table_name='models', mysql_length=255) + op.drop_table('models') + # ### end Alembic commands ### diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py index d659c78573..38712bfd2c 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py @@ -1,16 +1,16 @@ -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.schema import MetaData +from sqlalchemy import MetaData +from sqlalchemy.orm import DeclarativeBase -# Recommended naming convention used by Alembic, as various different database -# providers will autogenerate vastly different names making migrations more -# difficult. See: https://alembic.sqlalchemy.org/en/latest/naming.html -NAMING_CONVENTION = { - "ix": "ix_%(column_0_label)s", - "uq": "uq_%(table_name)s_%(column_0_name)s", - "ck": "ck_%(table_name)s_%(constraint_name)s", - "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", - "pk": "pk_%(table_name)s" -} -metadata = MetaData(naming_convention=NAMING_CONVENTION) -Base = declarative_base(metadata=metadata) +class Base(DeclarativeBase): + # Recommended naming convention used by Alembic, as various different + # database providers will autogenerate vastly different names making + # migrations more difficult. + # See: https://alembic.sqlalchemy.org/en/latest/naming.html + metadata = MetaData(naming_convention={ + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_`%(constraint_name)s`", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + }) diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/page.py b/docs/tutorials/wiki2/src/views/tutorial/models/page.py index 74ff1faf85..6cc9aae3cb 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/page.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/page.py @@ -1,10 +1,6 @@ -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - Text, -) -from sqlalchemy.orm import relationship +import bcrypt +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship from .meta import Base @@ -12,9 +8,9 @@ class Page(Base): """ The SQLAlchemy declarative model class for a Page object. """ __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, nullable=False, unique=True) - data = Column(Text, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(unique=True) + data: Mapped[str] - creator_id = Column(ForeignKey('users.id'), nullable=False) - creator = relationship('User', backref='created_pages') + creator_id: Mapped[int] = mapped_column(ForeignKey('users.id')) + creator: Mapped['User'] = relationship(back_populates='created_pages') diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/user.py b/docs/tutorials/wiki2/src/views/tutorial/models/user.py index 9228b48f74..926a66e645 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/user.py @@ -1,9 +1,6 @@ import bcrypt -from sqlalchemy import ( - Column, - Integer, - Text, -) +from sqlalchemy.orm import Mapped, mapped_column, relationship +from typing import List, Optional from .meta import Base @@ -11,11 +8,11 @@ class User(Base): """ The SQLAlchemy declarative model class for a User object. """ __tablename__ = 'users' - id = Column(Integer, primary_key=True) - name = Column(Text, nullable=False, unique=True) - role = Column(Text, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(unique=True) + role: Mapped[str] - password_hash = Column(Text) + password_hash: Mapped[Optional[str]] def set_password(self, pw): pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) @@ -26,3 +23,5 @@ def check_password(self, pw): expected_hash = self.password_hash.encode('utf8') return bcrypt.checkpw(pw.encode('utf8'), expected_hash) return False + + created_pages: Mapped[List['Page']] = relationship(back_populates='creator') diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py index df0e4cb9e6..7feb6671f4 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -6,6 +6,7 @@ ) from pyramid.view import view_config import re +import sqlalchemy as sa from .. import models @@ -21,13 +22,17 @@ def view_wiki(request): @view_config(route_name='view_page', renderer='tutorial:templates/view.jinja2') def view_page(request): pagename = request.matchdict['pagename'] - page = request.dbsession.query(models.Page).filter_by(name=pagename).first() + page = request.dbsession.scalars( + sa.select(models.Page).where(models.Page.name == pagename) + ).one_or_none() if page is None: raise HTTPNotFound('No such page') def add_link(match): word = match.group(1) - exists = request.dbsession.query(models.Page).filter_by(name=word).all() + exists = request.dbsession.execute( + sa.select(sa.exists(models.Page)).where(models.Page.name == word) + ).scalar() if exists: view_url = request.route_url('view_page', pagename=word) return '%s' % (view_url, escape(word)) @@ -43,7 +48,11 @@ def add_link(match): @view_config(route_name='edit_page', renderer='tutorial:templates/edit.jinja2') def edit_page(request): pagename = request.matchdict['pagename'] - page = request.dbsession.query(models.Page).filter_by(name=pagename).one() + page = request.dbsession.scalars( + sa.select(models.Page).where(models.Page.name == pagename) + ).one_or_none() + if page is None: + raise HTTPNotFound('No such page') if request.method == 'POST': page.data = request.params['body'] next_url = request.route_url('view_page', pagename=page.name) @@ -57,14 +66,18 @@ def edit_page(request): @view_config(route_name='add_page', renderer='tutorial:templates/edit.jinja2') def add_page(request): pagename = request.matchdict['pagename'] - if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0: + exists = request.dbsession.execute( + sa.select(sa.exists(models.Page)).where(models.Page.name == pagename) + ).scalar() + if exists: next_url = request.route_url('edit_page', pagename=pagename) return HTTPSeeOther(location=next_url) if request.method == 'POST': body = request.params['body'] - page = models.Page(name=pagename, data=body) - page.creator = ( - request.dbsession.query(models.User).filter_by(name='editor').one()) + creator = request.dbsession.scalars( + sa.select(models.User).where(models.User.name == 'editor') + ).one() + page = models.Page(name=pagename, data=body, creator=creator) request.dbsession.add(page) next_url = request.route_url('view_page', pagename=pagename) return HTTPSeeOther(location=next_url) From 286a7a3ee8705b28fb82252daa37896d63ae6741 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Feb 2024 16:14:01 -0700 Subject: [PATCH 07/15] update authentication chapter --- docs/tutorials/wiki2/authentication.rst | 8 +-- .../wiki2/src/authentication/.coveragerc | 2 - .../wiki2/src/authentication/CHANGES.txt | 4 -- .../wiki2/src/authentication/MANIFEST.in | 2 +- .../wiki2/src/authentication/README.md | 60 ++++++++++++++++++ .../wiki2/src/authentication/README.txt | 44 ------------- .../wiki2/src/authentication/development.ini | 8 +-- .../wiki2/src/authentication/production.ini | 8 +-- .../wiki2/src/authentication/pyproject.toml | 60 ++++++++++++++++++ .../wiki2/src/authentication/pytest.ini | 6 -- .../wiki2/src/authentication/setup.py | 63 ------------------- .../wiki2/src/authentication/testing.ini | 8 +-- .../alembic/versions/20240204_07f9d6b626b2.py | 52 +++++++++++++++ .../alembic/versions/20240204_4b6614165904.py | 33 ++++++++++ .../authentication/tutorial/models/meta.py | 28 ++++----- .../authentication/tutorial/models/page.py | 20 +++--- .../authentication/tutorial/models/user.py | 17 +++-- .../src/authentication/tutorial/security.py | 2 +- .../src/authentication/tutorial/views/auth.py | 9 ++- .../authentication/tutorial/views/default.py | 23 +++++-- 20 files changed, 274 insertions(+), 183 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/authentication/.coveragerc delete mode 100644 docs/tutorials/wiki2/src/authentication/CHANGES.txt create mode 100644 docs/tutorials/wiki2/src/authentication/README.md delete mode 100644 docs/tutorials/wiki2/src/authentication/README.txt create mode 100644 docs/tutorials/wiki2/src/authentication/pyproject.toml delete mode 100644 docs/tutorials/wiki2/src/authentication/pytest.ini delete mode 100644 docs/tutorials/wiki2/src/authentication/setup.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/20240204_07f9d6b626b2.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/20240204_4b6614165904.py diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index e8a770491e..dba10f4a05 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -131,9 +131,9 @@ Insert the highlighted line. In the same file, now edit the ``edit_page`` view function: .. literalinclude:: src/authentication/tutorial/views/default.py - :lines: 44-59 + :lines: 49-68 :lineno-match: - :emphasize-lines: 5-7 + :emphasize-lines: 9-11 :language: python Only the highlighted lines need to be changed. @@ -144,9 +144,9 @@ If the user either is not logged in or the user is not the page's creator In the same file, now edit the ``add_page`` view function: .. literalinclude:: src/authentication/tutorial/views/default.py - :lines: 61- + :lines: 70- :lineno-match: - :emphasize-lines: 3-5,13 + :emphasize-lines: 3-5,15 :language: python Only the highlighted lines need to be changed. diff --git a/docs/tutorials/wiki2/src/authentication/.coveragerc b/docs/tutorials/wiki2/src/authentication/.coveragerc deleted file mode 100644 index 5db0e79cff..0000000000 --- a/docs/tutorials/wiki2/src/authentication/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[run] -source = tutorial diff --git a/docs/tutorials/wiki2/src/authentication/CHANGES.txt b/docs/tutorials/wiki2/src/authentication/CHANGES.txt deleted file mode 100644 index 14b902fd10..0000000000 --- a/docs/tutorials/wiki2/src/authentication/CHANGES.txt +++ /dev/null @@ -1,4 +0,0 @@ -0.0 ---- - -- Initial version. diff --git a/docs/tutorials/wiki2/src/authentication/MANIFEST.in b/docs/tutorials/wiki2/src/authentication/MANIFEST.in index b4624fd1ca..201692c1bc 100644 --- a/docs/tutorials/wiki2/src/authentication/MANIFEST.in +++ b/docs/tutorials/wiki2/src/authentication/MANIFEST.in @@ -1,4 +1,4 @@ -include *.txt *.ini *.cfg *.rst +include *.txt *.ini *.cfg *.rst *.toml recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 recursive-include tests * recursive-exclude * __pycache__ diff --git a/docs/tutorials/wiki2/src/authentication/README.md b/docs/tutorials/wiki2/src/authentication/README.md new file mode 100644 index 0000000000..5ac1209d01 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/README.md @@ -0,0 +1,60 @@ +# myproj + +## Getting Started + +- Change directory into your newly created project if not already there. Your + current directory should be the same as this `README.md` file and `pyproject.toml`. + + ``` + cd tutorial + ``` + +- Create a Python virtual environment, if not already created. + + ``` + python3 -m venv env + ``` + +- Upgrade packaging tools, if necessary. + + ``` + env/bin/pip install --upgrade pip + ``` + +- Install the project in editable mode with its testing requirements. + + ``` + env/bin/pip install -e ".[testing]" + ``` + +- Initialize and upgrade the database using Alembic. + + - Generate your first revision. + + ``` + env/bin/alembic -c development.ini revision --autogenerate -m "init" + ``` + + - Upgrade to that revision. + + ``` + env/bin/alembic -c development.ini upgrade head + ``` + +- Load default data into the database using a script. + + ``` + env/bin/initialize_tutorial_db development.ini + ``` + +- Run your project's tests. + + ``` + env/bin/pytest + ``` + +- Run your project. + + ``` + env/bin/pserve development.ini + ``` diff --git a/docs/tutorials/wiki2/src/authentication/README.txt b/docs/tutorials/wiki2/src/authentication/README.txt deleted file mode 100644 index ed6b88b49e..0000000000 --- a/docs/tutorials/wiki2/src/authentication/README.txt +++ /dev/null @@ -1,44 +0,0 @@ -myproj -====== - -Getting Started ---------------- - -- Change directory into your newly created project if not already there. Your - current directory should be the same as this README.txt file and setup.py. - - cd tutorial - -- Create a Python virtual environment, if not already created. - - python3 -m venv env - -- Upgrade packaging tools, if necessary. - - env/bin/pip install --upgrade pip setuptools - -- Install the project in editable mode with its testing requirements. - - env/bin/pip install -e ".[testing]" - -- Initialize and upgrade the database using Alembic. - - - Generate your first revision. - - env/bin/alembic -c development.ini revision --autogenerate -m "init" - - - Upgrade to that revision. - - env/bin/alembic -c development.ini upgrade head - -- Load default data into the database using a script. - - env/bin/initialize_tutorial_db development.ini - -- Run your project's tests. - - env/bin/pytest - -- Run your project. - - env/bin/pserve development.ini diff --git a/docs/tutorials/wiki2/src/authentication/development.ini b/docs/tutorials/wiki2/src/authentication/development.ini index 7fda4cb7b9..7dcb83da0c 100644 --- a/docs/tutorials/wiki2/src/authentication/development.ini +++ b/docs/tutorials/wiki2/src/authentication/development.ini @@ -27,16 +27,16 @@ auth.secret = seekrit [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/authentication/production.ini b/docs/tutorials/wiki2/src/authentication/production.ini index 8e878a7073..b4c18b119a 100644 --- a/docs/tutorials/wiki2/src/authentication/production.ini +++ b/docs/tutorials/wiki2/src/authentication/production.ini @@ -21,16 +21,16 @@ auth.secret = real-seekrit [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = *:6543 diff --git a/docs/tutorials/wiki2/src/authentication/pyproject.toml b/docs/tutorials/wiki2/src/authentication/pyproject.toml new file mode 100644 index 0000000000..2b054c5784 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +version = "0.0" +name = "tutorial" +authors = [] +description = "myproj" +readme = "README.md" +keywords = ["web", "pyramid", "pylons"] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", +] +requires-python = ">=3.8" +dependencies = [ + "alembic", + "bcrypt", + "docutils", + "plaster_pastedeploy", + "pyramid", + "pyramid_debugtoolbar", + "pyramid_jinja2", + "pyramid_retry", + "pyramid_tm", + "SQLAlchemy", + "transaction", + "waitress", + "zope.sqlalchemy", +] + +[project.optional-dependencies] +testing = [ + "WebTest", + "pytest", + "pytest-cov", +] + +[project.scripts] +initialize_tutorial_db = "tutorial.scripts.initialize_db:main" + +[project.entry-points."paste.app_factory"] +main = "tutorial:main" + +[tool.setuptools.packages.find] +exclude = ["tests"] + +[tool.coverage.run] +source = "tutorial" + +[tool.pytest.ini_options] +addopts = "--strict-markers" +testpaths = [ + "tutorial", + "tests", +] diff --git a/docs/tutorials/wiki2/src/authentication/pytest.ini b/docs/tutorials/wiki2/src/authentication/pytest.ini deleted file mode 100644 index 3df78fe9dd..0000000000 --- a/docs/tutorials/wiki2/src/authentication/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -addopts = --strict-markers - -testpaths = - tutorial - tests diff --git a/docs/tutorials/wiki2/src/authentication/setup.py b/docs/tutorials/wiki2/src/authentication/setup.py deleted file mode 100644 index 12eabaff26..0000000000 --- a/docs/tutorials/wiki2/src/authentication/setup.py +++ /dev/null @@ -1,63 +0,0 @@ -import os - -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'README.txt')) as f: - README = f.read() -with open(os.path.join(here, 'CHANGES.txt')) as f: - CHANGES = f.read() - -requires = [ - 'alembic', - 'bcrypt', - 'docutils', - 'plaster_pastedeploy', - 'pyramid', - 'pyramid_debugtoolbar', - 'pyramid_jinja2', - 'pyramid_retry', - 'pyramid_tm', - 'SQLAlchemy', - 'transaction', - 'waitress', - 'zope.sqlalchemy', -] - -tests_require = [ - 'WebTest', - 'pytest', - 'pytest-cov', -] - -setup( - name='tutorial', - version='0.0', - description='myproj', - long_description=README + '\n\n' + CHANGES, - classifiers=[ - 'Programming Language :: Python', - 'Framework :: Pyramid', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', - ], - author='', - author_email='', - url='', - keywords='web pyramid pylons', - packages=find_packages(exclude=['tests']), - include_package_data=True, - zip_safe=False, - extras_require={ - 'testing': tests_require, - }, - install_requires=requires, - entry_points={ - 'paste.app_factory': [ - 'main = tutorial:main', - ], - 'console_scripts': [ - 'initialize_tutorial_db=tutorial.scripts.initialize_db:main', - ], - }, -) diff --git a/docs/tutorials/wiki2/src/authentication/testing.ini b/docs/tutorials/wiki2/src/authentication/testing.ini index d3c601f164..095666d67b 100644 --- a/docs/tutorials/wiki2/src/authentication/testing.ini +++ b/docs/tutorials/wiki2/src/authentication/testing.ini @@ -21,16 +21,16 @@ auth.secret = test-seekrit [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/20240204_07f9d6b626b2.py b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/20240204_07f9d6b626b2.py new file mode 100644 index 0000000000..329c002083 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/20240204_07f9d6b626b2.py @@ -0,0 +1,52 @@ +"""use new models Page and User + +Revision ID: 07f9d6b626b2 +Revises: 4b6614165904 +Create Date: 2024-02-04 14:39:12.885858 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '07f9d6b626b2' +down_revision = '4b6614165904' +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('role', sa.String(), nullable=False), + sa.Column('password_hash', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_users')), + sa.UniqueConstraint('name', name=op.f('uq_users_name')) + ) + op.create_table('pages', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('data', sa.String(), nullable=False), + sa.Column('creator_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['creator_id'], ['users.id'], name=op.f('fk_pages_creator_id_users')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_pages')), + sa.UniqueConstraint('name', name=op.f('uq_pages_name')) + ) + op.drop_index('my_index', table_name='models') + op.drop_table('models') + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('models', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.VARCHAR(), nullable=True), + sa.Column('value', sa.INTEGER(), nullable=True), + sa.PrimaryKeyConstraint('id', name='pk_models') + ) + op.create_index('my_index', 'models', ['name'], unique=1) + op.drop_table('pages') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/20240204_4b6614165904.py b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/20240204_4b6614165904.py new file mode 100644 index 0000000000..4f7a97d300 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/20240204_4b6614165904.py @@ -0,0 +1,33 @@ +"""init + +Revision ID: 4b6614165904 +Revises: +Create Date: 2024-02-04 14:32:46.784813 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4b6614165904' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('models', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('value', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_models')) + ) + op.create_index('my_index', 'models', ['name'], unique=True, mysql_length=255) + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('my_index', table_name='models', mysql_length=255) + op.drop_table('models') + # ### end Alembic commands ### diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py index d659c78573..38712bfd2c 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py @@ -1,16 +1,16 @@ -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.schema import MetaData +from sqlalchemy import MetaData +from sqlalchemy.orm import DeclarativeBase -# Recommended naming convention used by Alembic, as various different database -# providers will autogenerate vastly different names making migrations more -# difficult. See: https://alembic.sqlalchemy.org/en/latest/naming.html -NAMING_CONVENTION = { - "ix": "ix_%(column_0_label)s", - "uq": "uq_%(table_name)s_%(column_0_name)s", - "ck": "ck_%(table_name)s_%(constraint_name)s", - "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", - "pk": "pk_%(table_name)s" -} -metadata = MetaData(naming_convention=NAMING_CONVENTION) -Base = declarative_base(metadata=metadata) +class Base(DeclarativeBase): + # Recommended naming convention used by Alembic, as various different + # database providers will autogenerate vastly different names making + # migrations more difficult. + # See: https://alembic.sqlalchemy.org/en/latest/naming.html + metadata = MetaData(naming_convention={ + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_`%(constraint_name)s`", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + }) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py index 74ff1faf85..6cc9aae3cb 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py @@ -1,10 +1,6 @@ -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - Text, -) -from sqlalchemy.orm import relationship +import bcrypt +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship from .meta import Base @@ -12,9 +8,9 @@ class Page(Base): """ The SQLAlchemy declarative model class for a Page object. """ __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, nullable=False, unique=True) - data = Column(Text, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(unique=True) + data: Mapped[str] - creator_id = Column(ForeignKey('users.id'), nullable=False) - creator = relationship('User', backref='created_pages') + creator_id: Mapped[int] = mapped_column(ForeignKey('users.id')) + creator: Mapped['User'] = relationship(back_populates='created_pages') diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py index 9228b48f74..926a66e645 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py @@ -1,9 +1,6 @@ import bcrypt -from sqlalchemy import ( - Column, - Integer, - Text, -) +from sqlalchemy.orm import Mapped, mapped_column, relationship +from typing import List, Optional from .meta import Base @@ -11,11 +8,11 @@ class User(Base): """ The SQLAlchemy declarative model class for a User object. """ __tablename__ = 'users' - id = Column(Integer, primary_key=True) - name = Column(Text, nullable=False, unique=True) - role = Column(Text, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(unique=True) + role: Mapped[str] - password_hash = Column(Text) + password_hash: Mapped[Optional[str]] def set_password(self, pw): pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) @@ -26,3 +23,5 @@ def check_password(self, pw): expected_hash = self.password_hash.encode('utf8') return bcrypt.checkpw(pw.encode('utf8'), expected_hash) return False + + created_pages: Mapped[List['Page']] = relationship(back_populates='creator') diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/security.py b/docs/tutorials/wiki2/src/authentication/tutorial/security.py index e0d8ed965f..d3d332ec15 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/security.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/security.py @@ -16,7 +16,7 @@ def load_identity(self, request): return None userid = identity['userid'] - user = request.dbsession.query(models.User).get(userid) + user = request.dbsession.get(models.User, userid) return user def identity(self, request): diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py index 807ff34647..e97908b63c 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py @@ -8,6 +8,7 @@ forbidden_view_config, view_config, ) +import sqlalchemy as sa from .. import models @@ -22,11 +23,9 @@ def login(request): if request.method == 'POST': login = request.params['login'] password = request.params['password'] - user = ( - request.dbsession.query(models.User) - .filter_by(name=login) - .first() - ) + user = request.dbsession.scalars( + sa.select(models.User).where(models.User.name == login) + ).one_or_none() if user is not None and user.check_password(password): new_csrf_token(request) headers = remember(request, user.id) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py index 4fb715737e..50ce361cd3 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py @@ -7,6 +7,7 @@ ) from pyramid.view import view_config import re +import sqlalchemy as sa from .. import models @@ -22,13 +23,17 @@ def view_wiki(request): @view_config(route_name='view_page', renderer='tutorial:templates/view.jinja2') def view_page(request): pagename = request.matchdict['pagename'] - page = request.dbsession.query(models.Page).filter_by(name=pagename).first() + page = request.dbsession.scalars( + sa.select(models.Page).where(models.Page.name == pagename) + ).one_or_none() if page is None: raise HTTPNotFound('No such page') def add_link(match): word = match.group(1) - exists = request.dbsession.query(models.Page).filter_by(name=word).all() + exists = request.dbsession.execute( + sa.select(sa.exists(models.Page)).where(models.Page.name == word) + ).scalar() if exists: view_url = request.route_url('view_page', pagename=word) return '%s' % (view_url, escape(word)) @@ -44,7 +49,11 @@ def add_link(match): @view_config(route_name='edit_page', renderer='tutorial:templates/edit.jinja2') def edit_page(request): pagename = request.matchdict['pagename'] - page = request.dbsession.query(models.Page).filter_by(name=pagename).one() + page = request.dbsession.scalars( + sa.select(models.Page).where(models.Page.name == pagename) + ).one_or_none() + if page is None: + raise HTTPNotFound('No such page') user = request.identity if user is None or (user.role != 'editor' and page.creator != user): raise HTTPForbidden @@ -64,13 +73,15 @@ def add_page(request): if user is None or user.role not in ('editor', 'basic'): raise HTTPForbidden pagename = request.matchdict['pagename'] - if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0: + exists = request.dbsession.execute( + sa.select(sa.exists(models.Page)).where(models.Page.name == pagename) + ).scalar() + if exists: next_url = request.route_url('edit_page', pagename=pagename) return HTTPSeeOther(location=next_url) if request.method == 'POST': body = request.params['body'] - page = models.Page(name=pagename, data=body) - page.creator = request.identity + page = models.Page(name=pagename, data=body, creator=user) request.dbsession.add(page) next_url = request.route_url('view_page', pagename=pagename) return HTTPSeeOther(location=next_url) From 3c61799661ea5c36ae97326574a3dc7a695ce6aa Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Feb 2024 16:58:08 -0700 Subject: [PATCH 08/15] update authorization chapter --- docs/tutorials/wiki2/authorization.rst | 16 ++--- .../wiki2/src/authorization/.coveragerc | 2 - .../wiki2/src/authorization/CHANGES.txt | 4 -- .../wiki2/src/authorization/MANIFEST.in | 2 +- .../wiki2/src/authorization/README.md | 60 ++++++++++++++++++ .../wiki2/src/authorization/README.txt | 44 ------------- .../wiki2/src/authorization/development.ini | 8 +-- .../wiki2/src/authorization/production.ini | 8 +-- .../wiki2/src/authorization/pyproject.toml | 60 ++++++++++++++++++ .../wiki2/src/authorization/pytest.ini | 6 -- .../wiki2/src/authorization/setup.py | 63 ------------------- .../wiki2/src/authorization/testing.ini | 8 +-- .../alembic/versions/20240204_07f9d6b626b2.py | 52 +++++++++++++++ .../alembic/versions/20240204_4b6614165904.py | 33 ++++++++++ .../src/authorization/tutorial/models/meta.py | 28 ++++----- .../src/authorization/tutorial/models/page.py | 20 +++--- .../src/authorization/tutorial/models/user.py | 17 +++-- .../src/authorization/tutorial/routes.py | 10 ++- .../src/authorization/tutorial/security.py | 2 +- .../src/authorization/tutorial/views/auth.py | 9 ++- .../authorization/tutorial/views/default.py | 8 ++- 21 files changed, 274 insertions(+), 186 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/authorization/.coveragerc delete mode 100644 docs/tutorials/wiki2/src/authorization/CHANGES.txt create mode 100644 docs/tutorials/wiki2/src/authorization/README.md delete mode 100644 docs/tutorials/wiki2/src/authorization/README.txt create mode 100644 docs/tutorials/wiki2/src/authorization/pyproject.toml delete mode 100644 docs/tutorials/wiki2/src/authorization/pytest.ini delete mode 100644 docs/tutorials/wiki2/src/authorization/setup.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/20240204_07f9d6b626b2.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/20240204_4b6614165904.py diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index e0c59a5b63..84b05b8634 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -89,7 +89,7 @@ Open the file ``tutorial/routes.py`` and edit the following lines: .. literalinclude:: src/authorization/tutorial/routes.py :linenos: - :emphasize-lines: 1-11,18- + :emphasize-lines: 1-11,19- :language: python The highlighted lines need to be edited or added. @@ -101,7 +101,7 @@ the principals of either ``role:editor`` or ``role:basic`` to have the ``create`` permission: .. literalinclude:: src/authorization/tutorial/routes.py - :lines: 31-39 + :lines: 35-43 :lineno-match: :emphasize-lines: 5-9 :language: python @@ -110,7 +110,7 @@ The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by declaring a ``factory`` on the route: .. literalinclude:: src/authorization/tutorial/routes.py - :lines: 19-20 + :lines: 20-21 :lineno-match: :emphasize-lines: 1-2 :language: python @@ -119,7 +119,7 @@ The ``PageResource`` class defines the :term:`ACL` for a ``Page``. It uses an actual ``Page`` object to determine *who* can do *what* to the page. .. literalinclude:: src/authorization/tutorial/routes.py - :lines: 48- + :lines: 54- :lineno-match: :emphasize-lines: 5-10 :language: python @@ -128,7 +128,7 @@ The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and ``edit_page`` routes by declaring a ``factory`` on the routes: .. literalinclude:: src/authorization/tutorial/routes.py - :lines: 18-22 + :lines: 19-23 :lineno-match: :emphasize-lines: 1,4-5 :language: python @@ -157,7 +157,7 @@ Edit the ``view_page`` view to declare the ``view`` permission, and remove the explicit checks within the view: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 18-23 + :lines: 19-24 :lineno-match: :emphasize-lines: 1-2,4 :language: python @@ -171,7 +171,7 @@ the view logic. Edit the ``edit_page`` view to declare the ``edit`` permission: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 38-42 + :lines: 41-45 :lineno-match: :emphasize-lines: 1-2,4 :language: python @@ -179,7 +179,7 @@ Edit the ``edit_page`` view to declare the ``edit`` permission: Edit the ``add_page`` view to declare the ``create`` permission: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 52-56 + :lines: 55-59 :lineno-match: :emphasize-lines: 1-2,4 :language: python diff --git a/docs/tutorials/wiki2/src/authorization/.coveragerc b/docs/tutorials/wiki2/src/authorization/.coveragerc deleted file mode 100644 index 5db0e79cff..0000000000 --- a/docs/tutorials/wiki2/src/authorization/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[run] -source = tutorial diff --git a/docs/tutorials/wiki2/src/authorization/CHANGES.txt b/docs/tutorials/wiki2/src/authorization/CHANGES.txt deleted file mode 100644 index 14b902fd10..0000000000 --- a/docs/tutorials/wiki2/src/authorization/CHANGES.txt +++ /dev/null @@ -1,4 +0,0 @@ -0.0 ---- - -- Initial version. diff --git a/docs/tutorials/wiki2/src/authorization/MANIFEST.in b/docs/tutorials/wiki2/src/authorization/MANIFEST.in index b4624fd1ca..201692c1bc 100644 --- a/docs/tutorials/wiki2/src/authorization/MANIFEST.in +++ b/docs/tutorials/wiki2/src/authorization/MANIFEST.in @@ -1,4 +1,4 @@ -include *.txt *.ini *.cfg *.rst +include *.txt *.ini *.cfg *.rst *.toml recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 recursive-include tests * recursive-exclude * __pycache__ diff --git a/docs/tutorials/wiki2/src/authorization/README.md b/docs/tutorials/wiki2/src/authorization/README.md new file mode 100644 index 0000000000..5ac1209d01 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/README.md @@ -0,0 +1,60 @@ +# myproj + +## Getting Started + +- Change directory into your newly created project if not already there. Your + current directory should be the same as this `README.md` file and `pyproject.toml`. + + ``` + cd tutorial + ``` + +- Create a Python virtual environment, if not already created. + + ``` + python3 -m venv env + ``` + +- Upgrade packaging tools, if necessary. + + ``` + env/bin/pip install --upgrade pip + ``` + +- Install the project in editable mode with its testing requirements. + + ``` + env/bin/pip install -e ".[testing]" + ``` + +- Initialize and upgrade the database using Alembic. + + - Generate your first revision. + + ``` + env/bin/alembic -c development.ini revision --autogenerate -m "init" + ``` + + - Upgrade to that revision. + + ``` + env/bin/alembic -c development.ini upgrade head + ``` + +- Load default data into the database using a script. + + ``` + env/bin/initialize_tutorial_db development.ini + ``` + +- Run your project's tests. + + ``` + env/bin/pytest + ``` + +- Run your project. + + ``` + env/bin/pserve development.ini + ``` diff --git a/docs/tutorials/wiki2/src/authorization/README.txt b/docs/tutorials/wiki2/src/authorization/README.txt deleted file mode 100644 index ed6b88b49e..0000000000 --- a/docs/tutorials/wiki2/src/authorization/README.txt +++ /dev/null @@ -1,44 +0,0 @@ -myproj -====== - -Getting Started ---------------- - -- Change directory into your newly created project if not already there. Your - current directory should be the same as this README.txt file and setup.py. - - cd tutorial - -- Create a Python virtual environment, if not already created. - - python3 -m venv env - -- Upgrade packaging tools, if necessary. - - env/bin/pip install --upgrade pip setuptools - -- Install the project in editable mode with its testing requirements. - - env/bin/pip install -e ".[testing]" - -- Initialize and upgrade the database using Alembic. - - - Generate your first revision. - - env/bin/alembic -c development.ini revision --autogenerate -m "init" - - - Upgrade to that revision. - - env/bin/alembic -c development.ini upgrade head - -- Load default data into the database using a script. - - env/bin/initialize_tutorial_db development.ini - -- Run your project's tests. - - env/bin/pytest - -- Run your project. - - env/bin/pserve development.ini diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini index 7fda4cb7b9..7dcb83da0c 100644 --- a/docs/tutorials/wiki2/src/authorization/development.ini +++ b/docs/tutorials/wiki2/src/authorization/development.ini @@ -27,16 +27,16 @@ auth.secret = seekrit [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini index 8e878a7073..b4c18b119a 100644 --- a/docs/tutorials/wiki2/src/authorization/production.ini +++ b/docs/tutorials/wiki2/src/authorization/production.ini @@ -21,16 +21,16 @@ auth.secret = real-seekrit [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = *:6543 diff --git a/docs/tutorials/wiki2/src/authorization/pyproject.toml b/docs/tutorials/wiki2/src/authorization/pyproject.toml new file mode 100644 index 0000000000..2b054c5784 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +version = "0.0" +name = "tutorial" +authors = [] +description = "myproj" +readme = "README.md" +keywords = ["web", "pyramid", "pylons"] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", +] +requires-python = ">=3.8" +dependencies = [ + "alembic", + "bcrypt", + "docutils", + "plaster_pastedeploy", + "pyramid", + "pyramid_debugtoolbar", + "pyramid_jinja2", + "pyramid_retry", + "pyramid_tm", + "SQLAlchemy", + "transaction", + "waitress", + "zope.sqlalchemy", +] + +[project.optional-dependencies] +testing = [ + "WebTest", + "pytest", + "pytest-cov", +] + +[project.scripts] +initialize_tutorial_db = "tutorial.scripts.initialize_db:main" + +[project.entry-points."paste.app_factory"] +main = "tutorial:main" + +[tool.setuptools.packages.find] +exclude = ["tests"] + +[tool.coverage.run] +source = "tutorial" + +[tool.pytest.ini_options] +addopts = "--strict-markers" +testpaths = [ + "tutorial", + "tests", +] diff --git a/docs/tutorials/wiki2/src/authorization/pytest.ini b/docs/tutorials/wiki2/src/authorization/pytest.ini deleted file mode 100644 index 3df78fe9dd..0000000000 --- a/docs/tutorials/wiki2/src/authorization/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -addopts = --strict-markers - -testpaths = - tutorial - tests diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py deleted file mode 100644 index 12eabaff26..0000000000 --- a/docs/tutorials/wiki2/src/authorization/setup.py +++ /dev/null @@ -1,63 +0,0 @@ -import os - -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'README.txt')) as f: - README = f.read() -with open(os.path.join(here, 'CHANGES.txt')) as f: - CHANGES = f.read() - -requires = [ - 'alembic', - 'bcrypt', - 'docutils', - 'plaster_pastedeploy', - 'pyramid', - 'pyramid_debugtoolbar', - 'pyramid_jinja2', - 'pyramid_retry', - 'pyramid_tm', - 'SQLAlchemy', - 'transaction', - 'waitress', - 'zope.sqlalchemy', -] - -tests_require = [ - 'WebTest', - 'pytest', - 'pytest-cov', -] - -setup( - name='tutorial', - version='0.0', - description='myproj', - long_description=README + '\n\n' + CHANGES, - classifiers=[ - 'Programming Language :: Python', - 'Framework :: Pyramid', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', - ], - author='', - author_email='', - url='', - keywords='web pyramid pylons', - packages=find_packages(exclude=['tests']), - include_package_data=True, - zip_safe=False, - extras_require={ - 'testing': tests_require, - }, - install_requires=requires, - entry_points={ - 'paste.app_factory': [ - 'main = tutorial:main', - ], - 'console_scripts': [ - 'initialize_tutorial_db=tutorial.scripts.initialize_db:main', - ], - }, -) diff --git a/docs/tutorials/wiki2/src/authorization/testing.ini b/docs/tutorials/wiki2/src/authorization/testing.ini index d3c601f164..095666d67b 100644 --- a/docs/tutorials/wiki2/src/authorization/testing.ini +++ b/docs/tutorials/wiki2/src/authorization/testing.ini @@ -21,16 +21,16 @@ auth.secret = test-seekrit [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/20240204_07f9d6b626b2.py b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/20240204_07f9d6b626b2.py new file mode 100644 index 0000000000..329c002083 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/20240204_07f9d6b626b2.py @@ -0,0 +1,52 @@ +"""use new models Page and User + +Revision ID: 07f9d6b626b2 +Revises: 4b6614165904 +Create Date: 2024-02-04 14:39:12.885858 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '07f9d6b626b2' +down_revision = '4b6614165904' +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('role', sa.String(), nullable=False), + sa.Column('password_hash', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_users')), + sa.UniqueConstraint('name', name=op.f('uq_users_name')) + ) + op.create_table('pages', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('data', sa.String(), nullable=False), + sa.Column('creator_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['creator_id'], ['users.id'], name=op.f('fk_pages_creator_id_users')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_pages')), + sa.UniqueConstraint('name', name=op.f('uq_pages_name')) + ) + op.drop_index('my_index', table_name='models') + op.drop_table('models') + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('models', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.VARCHAR(), nullable=True), + sa.Column('value', sa.INTEGER(), nullable=True), + sa.PrimaryKeyConstraint('id', name='pk_models') + ) + op.create_index('my_index', 'models', ['name'], unique=1) + op.drop_table('pages') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/20240204_4b6614165904.py b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/20240204_4b6614165904.py new file mode 100644 index 0000000000..4f7a97d300 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/20240204_4b6614165904.py @@ -0,0 +1,33 @@ +"""init + +Revision ID: 4b6614165904 +Revises: +Create Date: 2024-02-04 14:32:46.784813 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4b6614165904' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('models', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('value', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_models')) + ) + op.create_index('my_index', 'models', ['name'], unique=True, mysql_length=255) + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('my_index', table_name='models', mysql_length=255) + op.drop_table('models') + # ### end Alembic commands ### diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py index d659c78573..38712bfd2c 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py @@ -1,16 +1,16 @@ -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.schema import MetaData +from sqlalchemy import MetaData +from sqlalchemy.orm import DeclarativeBase -# Recommended naming convention used by Alembic, as various different database -# providers will autogenerate vastly different names making migrations more -# difficult. See: https://alembic.sqlalchemy.org/en/latest/naming.html -NAMING_CONVENTION = { - "ix": "ix_%(column_0_label)s", - "uq": "uq_%(table_name)s_%(column_0_name)s", - "ck": "ck_%(table_name)s_%(constraint_name)s", - "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", - "pk": "pk_%(table_name)s" -} -metadata = MetaData(naming_convention=NAMING_CONVENTION) -Base = declarative_base(metadata=metadata) +class Base(DeclarativeBase): + # Recommended naming convention used by Alembic, as various different + # database providers will autogenerate vastly different names making + # migrations more difficult. + # See: https://alembic.sqlalchemy.org/en/latest/naming.html + metadata = MetaData(naming_convention={ + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_`%(constraint_name)s`", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + }) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py index 74ff1faf85..6cc9aae3cb 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py @@ -1,10 +1,6 @@ -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - Text, -) -from sqlalchemy.orm import relationship +import bcrypt +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship from .meta import Base @@ -12,9 +8,9 @@ class Page(Base): """ The SQLAlchemy declarative model class for a Page object. """ __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, nullable=False, unique=True) - data = Column(Text, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(unique=True) + data: Mapped[str] - creator_id = Column(ForeignKey('users.id'), nullable=False) - creator = relationship('User', backref='created_pages') + creator_id: Mapped[int] = mapped_column(ForeignKey('users.id')) + creator: Mapped['User'] = relationship(back_populates='created_pages') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py index 9228b48f74..926a66e645 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py @@ -1,9 +1,6 @@ import bcrypt -from sqlalchemy import ( - Column, - Integer, - Text, -) +from sqlalchemy.orm import Mapped, mapped_column, relationship +from typing import List, Optional from .meta import Base @@ -11,11 +8,11 @@ class User(Base): """ The SQLAlchemy declarative model class for a User object. """ __tablename__ = 'users' - id = Column(Integer, primary_key=True) - name = Column(Text, nullable=False, unique=True) - role = Column(Text, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(unique=True) + role: Mapped[str] - password_hash = Column(Text) + password_hash: Mapped[Optional[str]] def set_password(self, pw): pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) @@ -26,3 +23,5 @@ def check_password(self, pw): expected_hash = self.password_hash.encode('utf8') return bcrypt.checkpw(pw.encode('utf8'), expected_hash) return False + + created_pages: Mapped[List['Page']] = relationship(back_populates='creator') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py index fb352604de..7ee816132c 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py @@ -6,6 +6,7 @@ HTTPNotFound, HTTPSeeOther, ) +import sqlalchemy as sa from . import models @@ -23,7 +24,10 @@ def includeme(config): def new_page_factory(request): pagename = request.matchdict['pagename'] - if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0: + exists = request.dbsession.execute( + sa.select(sa.exists(models.Page)).where(models.Page.name == pagename) + ).scalar() + if exists: next_url = request.route_url('edit_page', pagename=pagename) raise HTTPSeeOther(location=next_url) return NewPage(pagename) @@ -40,7 +44,9 @@ def __acl__(self): def page_factory(request): pagename = request.matchdict['pagename'] - page = request.dbsession.query(models.Page).filter_by(name=pagename).first() + page = request.dbsession.scalars( + sa.select(models.Page).where(models.Page.name == pagename) + ).one_or_none() if page is None: raise HTTPNotFound return PageResource(page) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py index 18f0bd4c71..1f8fda0992 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py @@ -22,7 +22,7 @@ def load_identity(self, request): return None userid = identity['userid'] - user = request.dbsession.query(models.User).get(userid) + user = request.dbsession.get(models.User, userid) return user def identity(self, request): diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py index 807ff34647..e97908b63c 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py @@ -8,6 +8,7 @@ forbidden_view_config, view_config, ) +import sqlalchemy as sa from .. import models @@ -22,11 +23,9 @@ def login(request): if request.method == 'POST': login = request.params['login'] password = request.params['password'] - user = ( - request.dbsession.query(models.User) - .filter_by(name=login) - .first() - ) + user = request.dbsession.scalars( + sa.select(models.User).where(models.User.name == login) + ).one_or_none() if user is not None and user.check_password(password): new_csrf_token(request) headers = remember(request, user.id) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py index 4a2a66c84a..50c34cbb5e 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -3,6 +3,7 @@ from pyramid.httpexceptions import HTTPSeeOther from pyramid.view import view_config import re +import sqlalchemy as sa from .. import models @@ -22,7 +23,9 @@ def view_page(request): def add_link(match): word = match.group(1) - exists = request.dbsession.query(models.Page).filter_by(name=word).all() + exists = request.dbsession.execute( + sa.select(sa.exists(models.Page)).where(models.Page.name == word) + ).scalar() if exists: view_url = request.route_url('view_page', pagename=word) return '%s' % (view_url, escape(word)) @@ -55,8 +58,7 @@ def add_page(request): pagename = request.context.pagename if request.method == 'POST': body = request.params['body'] - page = models.Page(name=pagename, data=body) - page.creator = request.identity + page = models.Page(name=pagename, data=body, creator=request.identity) request.dbsession.add(page) next_url = request.route_url('view_page', pagename=pagename) return HTTPSeeOther(location=next_url) From 66c6c321b267e112220cfcfd81772923e3691e77 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Feb 2024 17:00:54 -0700 Subject: [PATCH 09/15] backport fix to authentication --- .../wiki2/src/authentication/tutorial/views/default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py index 50ce361cd3..bf37875f49 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py @@ -81,7 +81,7 @@ def add_page(request): return HTTPSeeOther(location=next_url) if request.method == 'POST': body = request.params['body'] - page = models.Page(name=pagename, data=body, creator=user) + page = models.Page(name=pagename, data=body, creator=request.identity) request.dbsession.add(page) next_url = request.route_url('view_page', pagename=pagename) return HTTPSeeOther(location=next_url) From 94ddd8d0519aeb1fe151059048ac365b18574cd0 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Feb 2024 17:05:54 -0700 Subject: [PATCH 10/15] upgrade tests chapter --- docs/tutorials/wiki2/src/tests/.coveragerc | 2 - docs/tutorials/wiki2/src/tests/CHANGES.txt | 4 -- docs/tutorials/wiki2/src/tests/MANIFEST.in | 2 +- docs/tutorials/wiki2/src/tests/README.md | 60 ++++++++++++++++++ docs/tutorials/wiki2/src/tests/README.txt | 44 ------------- .../tutorials/wiki2/src/tests/development.ini | 8 +-- docs/tutorials/wiki2/src/tests/production.ini | 8 +-- docs/tutorials/wiki2/src/tests/pyproject.toml | 60 ++++++++++++++++++ docs/tutorials/wiki2/src/tests/pytest.ini | 6 -- docs/tutorials/wiki2/src/tests/setup.py | 63 ------------------- docs/tutorials/wiki2/src/tests/testing.ini | 8 +-- .../alembic/versions/20240204_07f9d6b626b2.py | 52 +++++++++++++++ .../alembic/versions/20240204_4b6614165904.py | 33 ++++++++++ .../wiki2/src/tests/tutorial/models/meta.py | 28 ++++----- .../wiki2/src/tests/tutorial/models/page.py | 20 +++--- .../wiki2/src/tests/tutorial/models/user.py | 17 +++-- .../wiki2/src/tests/tutorial/routes.py | 10 ++- .../wiki2/src/tests/tutorial/security.py | 2 +- .../wiki2/src/tests/tutorial/views/auth.py | 9 ++- .../wiki2/src/tests/tutorial/views/default.py | 8 ++- docs/tutorials/wiki2/tests.rst | 19 ++++-- 21 files changed, 280 insertions(+), 183 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/tests/.coveragerc delete mode 100644 docs/tutorials/wiki2/src/tests/CHANGES.txt create mode 100644 docs/tutorials/wiki2/src/tests/README.md delete mode 100644 docs/tutorials/wiki2/src/tests/README.txt create mode 100644 docs/tutorials/wiki2/src/tests/pyproject.toml delete mode 100644 docs/tutorials/wiki2/src/tests/pytest.ini delete mode 100644 docs/tutorials/wiki2/src/tests/setup.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/20240204_07f9d6b626b2.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/20240204_4b6614165904.py diff --git a/docs/tutorials/wiki2/src/tests/.coveragerc b/docs/tutorials/wiki2/src/tests/.coveragerc deleted file mode 100644 index 5db0e79cff..0000000000 --- a/docs/tutorials/wiki2/src/tests/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[run] -source = tutorial diff --git a/docs/tutorials/wiki2/src/tests/CHANGES.txt b/docs/tutorials/wiki2/src/tests/CHANGES.txt deleted file mode 100644 index 14b902fd10..0000000000 --- a/docs/tutorials/wiki2/src/tests/CHANGES.txt +++ /dev/null @@ -1,4 +0,0 @@ -0.0 ---- - -- Initial version. diff --git a/docs/tutorials/wiki2/src/tests/MANIFEST.in b/docs/tutorials/wiki2/src/tests/MANIFEST.in index b4624fd1ca..201692c1bc 100644 --- a/docs/tutorials/wiki2/src/tests/MANIFEST.in +++ b/docs/tutorials/wiki2/src/tests/MANIFEST.in @@ -1,4 +1,4 @@ -include *.txt *.ini *.cfg *.rst +include *.txt *.ini *.cfg *.rst *.toml recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 recursive-include tests * recursive-exclude * __pycache__ diff --git a/docs/tutorials/wiki2/src/tests/README.md b/docs/tutorials/wiki2/src/tests/README.md new file mode 100644 index 0000000000..5ac1209d01 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/README.md @@ -0,0 +1,60 @@ +# myproj + +## Getting Started + +- Change directory into your newly created project if not already there. Your + current directory should be the same as this `README.md` file and `pyproject.toml`. + + ``` + cd tutorial + ``` + +- Create a Python virtual environment, if not already created. + + ``` + python3 -m venv env + ``` + +- Upgrade packaging tools, if necessary. + + ``` + env/bin/pip install --upgrade pip + ``` + +- Install the project in editable mode with its testing requirements. + + ``` + env/bin/pip install -e ".[testing]" + ``` + +- Initialize and upgrade the database using Alembic. + + - Generate your first revision. + + ``` + env/bin/alembic -c development.ini revision --autogenerate -m "init" + ``` + + - Upgrade to that revision. + + ``` + env/bin/alembic -c development.ini upgrade head + ``` + +- Load default data into the database using a script. + + ``` + env/bin/initialize_tutorial_db development.ini + ``` + +- Run your project's tests. + + ``` + env/bin/pytest + ``` + +- Run your project. + + ``` + env/bin/pserve development.ini + ``` diff --git a/docs/tutorials/wiki2/src/tests/README.txt b/docs/tutorials/wiki2/src/tests/README.txt deleted file mode 100644 index ed6b88b49e..0000000000 --- a/docs/tutorials/wiki2/src/tests/README.txt +++ /dev/null @@ -1,44 +0,0 @@ -myproj -====== - -Getting Started ---------------- - -- Change directory into your newly created project if not already there. Your - current directory should be the same as this README.txt file and setup.py. - - cd tutorial - -- Create a Python virtual environment, if not already created. - - python3 -m venv env - -- Upgrade packaging tools, if necessary. - - env/bin/pip install --upgrade pip setuptools - -- Install the project in editable mode with its testing requirements. - - env/bin/pip install -e ".[testing]" - -- Initialize and upgrade the database using Alembic. - - - Generate your first revision. - - env/bin/alembic -c development.ini revision --autogenerate -m "init" - - - Upgrade to that revision. - - env/bin/alembic -c development.ini upgrade head - -- Load default data into the database using a script. - - env/bin/initialize_tutorial_db development.ini - -- Run your project's tests. - - env/bin/pytest - -- Run your project. - - env/bin/pserve development.ini diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini index 7fda4cb7b9..7dcb83da0c 100644 --- a/docs/tutorials/wiki2/src/tests/development.ini +++ b/docs/tutorials/wiki2/src/tests/development.ini @@ -27,16 +27,16 @@ auth.secret = seekrit [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini index 8e878a7073..b4c18b119a 100644 --- a/docs/tutorials/wiki2/src/tests/production.ini +++ b/docs/tutorials/wiki2/src/tests/production.ini @@ -21,16 +21,16 @@ auth.secret = real-seekrit [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = *:6543 diff --git a/docs/tutorials/wiki2/src/tests/pyproject.toml b/docs/tutorials/wiki2/src/tests/pyproject.toml new file mode 100644 index 0000000000..2b054c5784 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +version = "0.0" +name = "tutorial" +authors = [] +description = "myproj" +readme = "README.md" +keywords = ["web", "pyramid", "pylons"] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", +] +requires-python = ">=3.8" +dependencies = [ + "alembic", + "bcrypt", + "docutils", + "plaster_pastedeploy", + "pyramid", + "pyramid_debugtoolbar", + "pyramid_jinja2", + "pyramid_retry", + "pyramid_tm", + "SQLAlchemy", + "transaction", + "waitress", + "zope.sqlalchemy", +] + +[project.optional-dependencies] +testing = [ + "WebTest", + "pytest", + "pytest-cov", +] + +[project.scripts] +initialize_tutorial_db = "tutorial.scripts.initialize_db:main" + +[project.entry-points."paste.app_factory"] +main = "tutorial:main" + +[tool.setuptools.packages.find] +exclude = ["tests"] + +[tool.coverage.run] +source = "tutorial" + +[tool.pytest.ini_options] +addopts = "--strict-markers" +testpaths = [ + "tutorial", + "tests", +] diff --git a/docs/tutorials/wiki2/src/tests/pytest.ini b/docs/tutorials/wiki2/src/tests/pytest.ini deleted file mode 100644 index 3df78fe9dd..0000000000 --- a/docs/tutorials/wiki2/src/tests/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -addopts = --strict-markers - -testpaths = - tutorial - tests diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py deleted file mode 100644 index 12eabaff26..0000000000 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ /dev/null @@ -1,63 +0,0 @@ -import os - -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'README.txt')) as f: - README = f.read() -with open(os.path.join(here, 'CHANGES.txt')) as f: - CHANGES = f.read() - -requires = [ - 'alembic', - 'bcrypt', - 'docutils', - 'plaster_pastedeploy', - 'pyramid', - 'pyramid_debugtoolbar', - 'pyramid_jinja2', - 'pyramid_retry', - 'pyramid_tm', - 'SQLAlchemy', - 'transaction', - 'waitress', - 'zope.sqlalchemy', -] - -tests_require = [ - 'WebTest', - 'pytest', - 'pytest-cov', -] - -setup( - name='tutorial', - version='0.0', - description='myproj', - long_description=README + '\n\n' + CHANGES, - classifiers=[ - 'Programming Language :: Python', - 'Framework :: Pyramid', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', - ], - author='', - author_email='', - url='', - keywords='web pyramid pylons', - packages=find_packages(exclude=['tests']), - include_package_data=True, - zip_safe=False, - extras_require={ - 'testing': tests_require, - }, - install_requires=requires, - entry_points={ - 'paste.app_factory': [ - 'main = tutorial:main', - ], - 'console_scripts': [ - 'initialize_tutorial_db=tutorial.scripts.initialize_db:main', - ], - }, -) diff --git a/docs/tutorials/wiki2/src/tests/testing.ini b/docs/tutorials/wiki2/src/tests/testing.ini index d3c601f164..095666d67b 100644 --- a/docs/tutorials/wiki2/src/tests/testing.ini +++ b/docs/tutorials/wiki2/src/tests/testing.ini @@ -21,16 +21,16 @@ auth.secret = test-seekrit [pshell] setup = tutorial.pshell.setup -### -# wsgi server configuration -### - [alembic] # path to migration scripts script_location = tutorial/alembic file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s # file_template = %%(rev)s_%%(slug)s +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main listen = localhost:6543 diff --git a/docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/20240204_07f9d6b626b2.py b/docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/20240204_07f9d6b626b2.py new file mode 100644 index 0000000000..329c002083 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/20240204_07f9d6b626b2.py @@ -0,0 +1,52 @@ +"""use new models Page and User + +Revision ID: 07f9d6b626b2 +Revises: 4b6614165904 +Create Date: 2024-02-04 14:39:12.885858 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '07f9d6b626b2' +down_revision = '4b6614165904' +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('role', sa.String(), nullable=False), + sa.Column('password_hash', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_users')), + sa.UniqueConstraint('name', name=op.f('uq_users_name')) + ) + op.create_table('pages', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('data', sa.String(), nullable=False), + sa.Column('creator_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['creator_id'], ['users.id'], name=op.f('fk_pages_creator_id_users')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_pages')), + sa.UniqueConstraint('name', name=op.f('uq_pages_name')) + ) + op.drop_index('my_index', table_name='models') + op.drop_table('models') + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('models', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.VARCHAR(), nullable=True), + sa.Column('value', sa.INTEGER(), nullable=True), + sa.PrimaryKeyConstraint('id', name='pk_models') + ) + op.create_index('my_index', 'models', ['name'], unique=1) + op.drop_table('pages') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/20240204_4b6614165904.py b/docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/20240204_4b6614165904.py new file mode 100644 index 0000000000..4f7a97d300 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/20240204_4b6614165904.py @@ -0,0 +1,33 @@ +"""init + +Revision ID: 4b6614165904 +Revises: +Create Date: 2024-02-04 14:32:46.784813 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4b6614165904' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('models', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('value', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_models')) + ) + op.create_index('my_index', 'models', ['name'], unique=True, mysql_length=255) + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('my_index', table_name='models', mysql_length=255) + op.drop_table('models') + # ### end Alembic commands ### diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py index d659c78573..38712bfd2c 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py @@ -1,16 +1,16 @@ -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.schema import MetaData +from sqlalchemy import MetaData +from sqlalchemy.orm import DeclarativeBase -# Recommended naming convention used by Alembic, as various different database -# providers will autogenerate vastly different names making migrations more -# difficult. See: https://alembic.sqlalchemy.org/en/latest/naming.html -NAMING_CONVENTION = { - "ix": "ix_%(column_0_label)s", - "uq": "uq_%(table_name)s_%(column_0_name)s", - "ck": "ck_%(table_name)s_%(constraint_name)s", - "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", - "pk": "pk_%(table_name)s" -} -metadata = MetaData(naming_convention=NAMING_CONVENTION) -Base = declarative_base(metadata=metadata) +class Base(DeclarativeBase): + # Recommended naming convention used by Alembic, as various different + # database providers will autogenerate vastly different names making + # migrations more difficult. + # See: https://alembic.sqlalchemy.org/en/latest/naming.html + metadata = MetaData(naming_convention={ + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_`%(constraint_name)s`", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + }) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/page.py b/docs/tutorials/wiki2/src/tests/tutorial/models/page.py index 74ff1faf85..6cc9aae3cb 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/page.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/page.py @@ -1,10 +1,6 @@ -from sqlalchemy import ( - Column, - ForeignKey, - Integer, - Text, -) -from sqlalchemy.orm import relationship +import bcrypt +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship from .meta import Base @@ -12,9 +8,9 @@ class Page(Base): """ The SQLAlchemy declarative model class for a Page object. """ __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, nullable=False, unique=True) - data = Column(Text, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(unique=True) + data: Mapped[str] - creator_id = Column(ForeignKey('users.id'), nullable=False) - creator = relationship('User', backref='created_pages') + creator_id: Mapped[int] = mapped_column(ForeignKey('users.id')) + creator: Mapped['User'] = relationship(back_populates='created_pages') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py index 9228b48f74..926a66e645 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py @@ -1,9 +1,6 @@ import bcrypt -from sqlalchemy import ( - Column, - Integer, - Text, -) +from sqlalchemy.orm import Mapped, mapped_column, relationship +from typing import List, Optional from .meta import Base @@ -11,11 +8,11 @@ class User(Base): """ The SQLAlchemy declarative model class for a User object. """ __tablename__ = 'users' - id = Column(Integer, primary_key=True) - name = Column(Text, nullable=False, unique=True) - role = Column(Text, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(unique=True) + role: Mapped[str] - password_hash = Column(Text) + password_hash: Mapped[Optional[str]] def set_password(self, pw): pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) @@ -26,3 +23,5 @@ def check_password(self, pw): expected_hash = self.password_hash.encode('utf8') return bcrypt.checkpw(pw.encode('utf8'), expected_hash) return False + + created_pages: Mapped[List['Page']] = relationship(back_populates='creator') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/routes.py b/docs/tutorials/wiki2/src/tests/tutorial/routes.py index fb352604de..7ee816132c 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/routes.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/routes.py @@ -6,6 +6,7 @@ HTTPNotFound, HTTPSeeOther, ) +import sqlalchemy as sa from . import models @@ -23,7 +24,10 @@ def includeme(config): def new_page_factory(request): pagename = request.matchdict['pagename'] - if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0: + exists = request.dbsession.execute( + sa.select(sa.exists(models.Page)).where(models.Page.name == pagename) + ).scalar() + if exists: next_url = request.route_url('edit_page', pagename=pagename) raise HTTPSeeOther(location=next_url) return NewPage(pagename) @@ -40,7 +44,9 @@ def __acl__(self): def page_factory(request): pagename = request.matchdict['pagename'] - page = request.dbsession.query(models.Page).filter_by(name=pagename).first() + page = request.dbsession.scalars( + sa.select(models.Page).where(models.Page.name == pagename) + ).one_or_none() if page is None: raise HTTPNotFound return PageResource(page) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security.py b/docs/tutorials/wiki2/src/tests/tutorial/security.py index 18f0bd4c71..1f8fda0992 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/security.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/security.py @@ -22,7 +22,7 @@ def load_identity(self, request): return None userid = identity['userid'] - user = request.dbsession.query(models.User).get(userid) + user = request.dbsession.get(models.User, userid) return user def identity(self, request): diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py index 807ff34647..e97908b63c 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py @@ -8,6 +8,7 @@ forbidden_view_config, view_config, ) +import sqlalchemy as sa from .. import models @@ -22,11 +23,9 @@ def login(request): if request.method == 'POST': login = request.params['login'] password = request.params['password'] - user = ( - request.dbsession.query(models.User) - .filter_by(name=login) - .first() - ) + user = request.dbsession.scalars( + sa.select(models.User).where(models.User.name == login) + ).one_or_none() if user is not None and user.check_password(password): new_csrf_token(request) headers = remember(request, user.id) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py index 4a2a66c84a..50c34cbb5e 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -3,6 +3,7 @@ from pyramid.httpexceptions import HTTPSeeOther from pyramid.view import view_config import re +import sqlalchemy as sa from .. import models @@ -22,7 +23,9 @@ def view_page(request): def add_link(match): word = match.group(1) - exists = request.dbsession.query(models.Page).filter_by(name=word).all() + exists = request.dbsession.execute( + sa.select(sa.exists(models.Page)).where(models.Page.name == word) + ).scalar() if exists: view_url = request.route_url('view_page', pagename=word) return '%s' % (view_url, escape(word)) @@ -55,8 +58,7 @@ def add_page(request): pagename = request.context.pagename if request.method == 'POST': body = request.params['body'] - page = models.Page(name=pagename, data=body) - page.creator = request.identity + page = models.Page(name=pagename, data=body, creator=request.identity) request.dbsession.add(page) next_url = request.route_url('view_page', pagename=pagename) return HTTPSeeOther(location=next_url) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index dce14cf9ba..82bf0672f1 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -20,19 +20,28 @@ The test module would have the same name with the prefix ``test_``. The harness consists of the following setup: -- ``pytest.ini`` - controls basic ``pytest`` config including where to find the tests. +- ``[tool.pytest.ini_options]`` in ``pyproject.toml``. + + Controls basic ``pytest`` config including where to find the tests. We have configured ``pytest`` to search for tests in the application package and in the ``tests`` package. -- ``.coveragerc`` - controls coverage config. +- ``[tool.coverage.run]`` in ``pyproject.toml``. + In our setup, it works with the ``pytest-cov`` plugin that we use via the ``--cov`` options to the ``pytest`` command. -- ``testing.ini`` - a mirror of ``development.ini`` and ``production.ini`` that contains settings used for executing the test suite. +- ``testing.ini`` + + A mirror of ``development.ini`` and ``production.ini`` that contains settings used for executing the test suite. Most importantly, it contains the database connection information used by tests that require the database. -- ``tests_require`` in ``setup.py`` - controls the dependencies installed when testing. +- ``testing`` optional dependencies in ``[project.optional-dependencies]`` section of ``pyproject.toml``. + + Controls the dependencies installed when testing. When the list is changed, it's necessary to re-run ``$VENV/bin/pip install -e ".[testing]"`` to ensure the new dependencies are installed. -- ``tests/conftest.py`` - the core fixtures available throughout our tests. +- ``tests/conftest.py``. + + The core fixtures available throughout our tests. The fixtures are explained in more detail below. From 3069052018cc1f1dd6dc2ed702b829a1a7147212 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Feb 2024 17:16:02 -0700 Subject: [PATCH 11/15] update distributing chapter --- docs/tutorials/wiki2/distributing.rst | 41 +++++++++++++-------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/docs/tutorials/wiki2/distributing.rst b/docs/tutorials/wiki2/distributing.rst index 0eff634610..08f7d4d487 100644 --- a/docs/tutorials/wiki2/distributing.rst +++ b/docs/tutorials/wiki2/distributing.rst @@ -4,37 +4,36 @@ Distributing Your Application ============================= -Once your application works properly, you can create a "tarball" from it by -using the ``setup.py sdist`` command. The following commands assume your +.. note:: + + This is an optional step. + It is not required nor expected that every application is built to be distributed to a package index. + However, even when building personal projects, defining it as a distributable artifact can provide many advantages when it comes to optimizing your build for a Docker image or other "production" hardened environments that should not mirror your local development environment exactly. + +Once your application works properly, you can create a "sdist" or "wheel" from +it by using a PEP517-compliant client tool. The following commands assume your current working directory contains the ``tutorial`` package and the -``setup.py`` file. +``pyproject.toml`` file. On Unix: .. code-block:: bash - $VENV/bin/python setup.py sdist + $VENV/bin/pip install build + $VENV/bin/python -m build On Windows: .. code-block:: doscon - %VENV%\Scripts\python setup.py sdist - -The output of such a command will be something like: - -.. code-block:: text + %VENV%\Scripts\pip install build + %VENV%\Scripts\python -m build - running sdist - # more output - creating dist - Creating tar archive - removing 'tutorial-0.0' (and everything under it) +Upon successfull completion, a "sdist" and a "wheel" will be output to the ``dist`` subdirectory. +These artifacts are uploadable to `PyPI `_ using a tool like ``twine``. +You should be able to create a brand new virtualenv and ``pip install`` the sdist or wheel. +Note that the ``production.ini`` is not part of the distribution. +This file is considered to be defined by the "user" of your application, not part of the application itself. +If you'd like to help a user out, consider defining a new CLI script that can render a config file for them! -Note that this command creates a tarball in the ``dist`` subdirectory named -``tutorial-0.0.tar.gz``. You can send this file to your friends to show them -your cool new application. They should be able to install it by pointing the -``pip install`` command directly at it. Or you can upload it to `PyPI -`_ and share it with the rest of the world, where -it can be downloaded via ``pip install`` remotely like any other package people -download from PyPI. +Please learn more about distributing an application from the `Python Packaging User Guide `_. From 85e9b2af7d9fa19ec70a21e6ca27498389e19ba3 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Feb 2024 20:16:30 -0700 Subject: [PATCH 12/15] fix query in test_views --- docs/tutorials/wiki2/src/tests/tests/test_views.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/wiki2/src/tests/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tests/test_views.py index e93b04b3c7..dbf32799a1 100644 --- a/docs/tutorials/wiki2/src/tests/tests/test_views.py +++ b/docs/tutorials/wiki2/src/tests/tests/test_views.py @@ -1,4 +1,5 @@ from pyramid.testing import DummySecurityPolicy +import sqlalchemy as sa from tutorial import models @@ -93,11 +94,9 @@ def test_submit_works(self, dummy_config, dummy_request, dbsession): setUser(dummy_config, makeUser('foo', 'editor')) self._addRoutes(dummy_config) self._callFUT(dummy_request) - page = ( - dbsession.query(models.Page) - .filter_by(name='AnotherPage') - .one() - ) + page = dbsession.scalars( + sa.select(models.Page).where(models.Page.name == 'AnotherPage') + ).one() assert page.data == 'Hello yo!' class Test_edit_page: From a31db999eaecb393b1d0c10058b1d43be076a4f6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 7 Feb 2024 18:01:24 -0700 Subject: [PATCH 13/15] fix pytest / coverage references --- docs/tutorials/wiki2/installation.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 072625f803..238e00b2a2 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -372,7 +372,9 @@ Test and coverage cookiecutter defaults --------------------------------------- The Pyramid cookiecutter includes configuration defaults for ``pytest`` and test coverage. -These configuration files are ``pytest.ini`` and ``.coveragerc``, located at the root of your package. +The configuration for ``pytest`` is in the ``pyproject.toml`` file in the ``[tool.pytest.ini_options]`` section. +Coverage is checked using the ``pytest-cov`` plugin, a wrapper around the `Coverage `_ tool. +Options affecting coverage are defined in ``[tool.coverage.run]``. ``pytest`` follows :ref:`conventions for Python test discovery `. The configuration defaults from the cookiecutter tell ``pytest`` where to find the module on which we want to run tests and coverage. From b2c8840708079ad645cd7abea45634d4b8686eb8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 7 Feb 2024 18:11:30 -0700 Subject: [PATCH 14/15] improve the distributing.rst --- docs/tutorials/wiki2/distributing.rst | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/wiki2/distributing.rst b/docs/tutorials/wiki2/distributing.rst index 08f7d4d487..8840d5f7c6 100644 --- a/docs/tutorials/wiki2/distributing.rst +++ b/docs/tutorials/wiki2/distributing.rst @@ -29,11 +29,25 @@ On Windows: %VENV%\Scripts\pip install build %VENV%\Scripts\python -m build -Upon successfull completion, a "sdist" and a "wheel" will be output to the ``dist`` subdirectory. -These artifacts are uploadable to `PyPI `_ using a tool like ``twine``. -You should be able to create a brand new virtualenv and ``pip install`` the sdist or wheel. -Note that the ``production.ini`` is not part of the distribution. -This file is considered to be defined by the "user" of your application, not part of the application itself. -If you'd like to help a user out, consider defining a new CLI script that can render a config file for them! +The output of such a command will be something like: + +.. code-block:: text + + * Creating venv isolated environment... + * Installing packages in isolated environment... (setuptools) + * Getting build dependencies for sdist... + ... + removing build/bdist.linux-x86_64/wheel + Successfully built tutorial-0.0.tar.gz and tutorial-0.0-py3-none-any.whl + +This command creates a subdirectory named ``dist``. +Inside that is a tarball named ``tutorial-0.0.tar.gz`` (the source :term:`distribution` of your application), as well ass ``tutorial-0.0-py3-none-any.whl`` (the binary :term:`distribution`). +You can send these files to your friends to show them your cool new application. +They should be able to install the app by pointing the ``pip install`` command directly at one of them. +These artifacts are also uploadable to `PyPI `_, or another package index, using a tool like ``twine``. + +Note that the config files, such as ``production.ini`` are not part of the distribution. +These files are considered to be defined by the "user" of your application and not part of the application itself. +If you'd like to help a user out, consider defining a new CLI script similar to ``initialize_tutorial_db`` that can render a config file for them! Please learn more about distributing an application from the `Python Packaging User Guide `_. From 222386e96a1711b6215f64ea809a9f4a7a8c2202 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 7 Feb 2024 20:52:33 -0700 Subject: [PATCH 15/15] sync language from zodb tutorial for test sections --- docs/tutorials/wiki2/tests.rst | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index 82bf0672f1..62b4364019 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -20,30 +20,21 @@ The test module would have the same name with the prefix ``test_``. The harness consists of the following setup: -- ``[tool.pytest.ini_options]`` in ``pyproject.toml``. +- The ``project.optional-dependencies`` stanza of ``pyproject.toml`` - controls the dependencies installed when testing. + When the list is changed, it is necessary to re-run ``$VENV/bin/pip install -e ".[testing]"`` to ensure the new dependencies are installed. - Controls basic ``pytest`` config including where to find the tests. +- The ``tool.pytest.ini_options`` stanza of ``pyproject.toml`` controls basic ``pytest`` configuration, including where to find the tests. We have configured ``pytest`` to search for tests in the application package and in the ``tests`` package. -- ``[tool.coverage.run]`` in ``pyproject.toml``. - +- The ``tool.coverage.run`` stanza of ``pyproject.toml`` controls coverage config. In our setup, it works with the ``pytest-cov`` plugin that we use via the ``--cov`` options to the ``pytest`` command. -- ``testing.ini`` - - A mirror of ``development.ini`` and ``production.ini`` that contains settings used for executing the test suite. +- The ``testing.ini`` file is a mirror of ``development.ini`` and ``production.ini`` that contains settings used for executing the test suite. Most importantly, it contains the database connection information used by tests that require the database. -- ``testing`` optional dependencies in ``[project.optional-dependencies]`` section of ``pyproject.toml``. - - Controls the dependencies installed when testing. - When the list is changed, it's necessary to re-run ``$VENV/bin/pip install -e ".[testing]"`` to ensure the new dependencies are installed. - -- ``tests/conftest.py``. - - The core fixtures available throughout our tests. - The fixtures are explained in more detail below. - +- The ``tests/conftest.py`` file defines the core fixtures available throughout our tests. + The fixtures are explained in more detail in the following sections. + Open ``tests/conftest.py`` and follow along. Session-scoped test fixtures ----------------------------