Skip to content

Commit

Permalink
Updated configs, docs and some code for Asphalt 5
Browse files Browse the repository at this point in the history
  • Loading branch information
agronholm committed Dec 30, 2024
1 parent a9910d5 commit c8b0275
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 98 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ jobs:
- name: Start external services
run: docker compose up -d
- name: Install dependencies
run: |
pip install git+https://github.com/asphalt-framework/[email protected]
pip install -e .[test] coverage
run: pip install -e .[test]
- name: Test with pytest
run: coverage run -m pytest -v
- name: Generate coverage report
Expand Down
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx_autodoc_typehints",
"sphinx_rtd_theme",
"sphinx_tabs.tabs",
]

Expand All @@ -26,6 +27,7 @@
exclude_patterns = ["_build"]
pygments_style = "sphinx"
autodoc_default_options = {"members": True, "show-inheritance": True}
autodoc_inherit_docstrings = False
highlight_language = "python3"
todo_include_todos = False

Expand Down
34 changes: 8 additions & 26 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ Configuration

A typical SQLAlchemy configuration consists of a single database.
At minimum, you only need a connection URL (see the
`SQLAlchemy documentation`_ for how to construct one). Such a configuration would look
something like this::
:doc:`SQLAlchemy documentation <sqlalchemy:tutorial/engine>` on how to construct one).
Such a configuration would look something like this::

components:
sqlalchemy:
url: postgresql+asyncpg://user:[email protected]/mydatabase
url: postgresql+psycopg://user:[email protected]/mydatabase

This will add two static resources and one resource factory, as follows:

* engine (type: :class:`sqlalchemy.future.engine.Engine` or
* engine (type: :class:`sqlalchemy.engine.Engine` or
:class:`sqlalchemy.ext.asyncio.AsyncEngine`)
* sessionmaker (type: :class:`sqlalchemy.orm.sessionmaker`)
* session factory (generates :class:`~sqlalchemy.orm.Session` or
Expand All @@ -26,23 +26,22 @@ sharing features)::
components:
sqlalchemy:
url:
drivername: postgresql+asyncpg
drivername: postgresql+psycopg
username: user
password: password
host: 10.0.0.8
database: mydatabase

.. seealso::
* :class:`sqlalchemy.engine.URL`
* :class:`asphalt.sqlalchemy.component.SQLAlchemyComponent`

.. _SQLAlchemy documentation: https://docs.sqlalchemy.org/en/14/core/engines.html
* :class:`asphalt.sqlalchemy.SQLAlchemyComponent`

Setting engine or session options
---------------------------------

If you need to adjust the options used for creating new sessions, or pass extra
arguments to the engine, you can do so by specifying them in the ``session`` option::
arguments to the engine, you can do so by specifying them in the ``session_args`` and
``engine_args``, respectively::

components:
sqlalchemy:
Expand All @@ -52,20 +51,3 @@ arguments to the engine, you can do so by specifying them in the ``session`` opt
session_args:
info:
hello: world

Multiple databases
------------------

If you need to work with multiple databases, you will need to use multiple instances
of the ``sqlalchemy`` component::

components:
sqlalchemy:
resource_name: db1
url: postgresql+asyncpg:///mydatabase
sqlalchemy/db2:
resource_name: db2
url: sqlite+aiosqlite:///mydb.sqlite

This will make the appropriate resources available using their respective namespaces
(``db1`` or ``db2`` instead of ``default``).
32 changes: 16 additions & 16 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ classifiers = [
]
requires-python = ">=3.9"
dependencies = [
"asphalt ~= 4.11",
"asphalt @ git+https://github.com/asphalt-framework/asphalt",
"SQLAlchemy[asyncio] >= 2.0",
]
dynamic = ["version"]
Expand All @@ -40,6 +40,7 @@ test = [
"aiosqlite",
"anyio[trio] >= 4.2",
"asyncmy; platform_python_implementation == 'CPython'",
"coverage >= 7",
"pymysql",
"psycopg >= 3.1; platform_python_implementation == 'CPython'",
"pytest >= 7.4",
Expand Down Expand Up @@ -73,9 +74,10 @@ select = [

[tool.ruff.lint.isort]
known-first-party = ["asphalt.sqlalchemy"]
known-third-party = ["asphalt.core"]

[tool.pytest.ini_options]
addopts = "-rsx --tb=short"
addopts = ["-rsfE", "--tb=short"]
testpaths = ["tests"]

[tool.mypy]
Expand All @@ -92,20 +94,18 @@ branch = true
show_missing = true

[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py39, py310, py311, py312, py313, pypy3
env_list = ["py39", "py310", "py311", "py312", "py313"]
skip_missing_interpreters = true
minversion = 4.0

[testenv]
extras = test
commands = python -m pytest {posargs}
setenv =
MYSQL_URL = mysql+pymysql://root@localhost:33060/asphalttest
POSTGRESQL_URL = postgresql+psycopg://postgres:secret@localhost:54320/asphalttest
[testenv:docs]
extras = doc
commands = sphinx-build -W docs build/sphinx
[tool.tox.env_run_base]
commands = [["python", "-m", "pytest", { replace = "posargs", extend = true }]]
package = "editable"
extras = ["test"]
setenv = """
MYSQL_URL = mysql+pymysql://root@localhost:33060/asphalttest
POSTGRESQL_URL = postgresql+psycopg://postgres:secret@localhost:54320/asphalttest
"""

[tool.tox.env.docs]
commands = [["sphinx-build", "-W", "-n", "docs", "build/sphinx", { replace = "posargs", extend = true }]]
extras = ["doc"]
29 changes: 5 additions & 24 deletions src/asphalt/sqlalchemy/_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import Pool

logger = logging.getLogger(__name__)


class SQLAlchemyComponent(Component):
"""
Expand Down Expand Up @@ -59,15 +57,15 @@ class SQLAlchemyComponent(Component):
* ``expire_on_commit``: ``False``
:param url: the connection url passed to
:func:`~sqlalchemy.engine.create_engine`
:func:`~sqlalchemy.create_engine`
(can also be a dictionary of :class:`~sqlalchemy.engine.url.URL` keyword
arguments)
:param bind: a connection or engine to use instead of creating a new engine
:param prefer_async: if ``True``, try to create an async engine rather than a
synchronous one, in cases like ``psycopg`` where the driver supports both
:param engine_args: extra keyword arguments passed to
:func:`sqlalchemy.engine.create_engine` or
:func:`sqlalchemy.ext.asyncio.create_engine`
:func:`sqlalchemy.create_engine` or
:func:`sqlalchemy.ext.asyncio.create_async_engine`
:param session_args: extra keyword arguments passed to
:class:`~sqlalchemy.orm.session.Session` or
:class:`~sqlalchemy.ext.asyncio.AsyncSession`
Expand All @@ -76,9 +74,8 @@ class SQLAlchemyComponent(Component):
:param ready_callback: a callable that is called right before the resources are
added to the context (can be a coroutine function too)
:param poolclass: the SQLAlchemy pool class (or a textual reference to one) to use;
passed to :func:`sqlalchemy.engine.create_engine` or
:func:`sqlalchemy.ext.asyncio.create_engine`
:param resource_name: name space for the database resources
passed to :func:`sqlalchemy.create_engine` or
:func:`sqlalchemy.ext.asyncio.create_async_engine`
"""

_engine: Engine | AsyncEngine
Expand All @@ -96,9 +93,7 @@ def __init__(
commit_executor_workers: int = 50,
ready_callback: Callable[[Engine, sessionmaker[Any]], Any] | str | None = None,
poolclass: str | type[Pool] | None = None,
resource_name: str = "default",
):
self.resource_name = resource_name
self.commit_thread_limiter = CapacityLimiter(commit_executor_workers)
self.ready_callback = resolve_reference(ready_callback)
engine_args = engine_args or {}
Expand Down Expand Up @@ -220,23 +215,19 @@ async def start(self) -> None:

add_resource(
self._engine,
self.resource_name,
description="SQLAlchemy engine (asynchronous)",
teardown_callback=teardown_callback,
)
add_resource(
self._sessionmaker,
self.resource_name,
description="SQLAlchemy session factory (synchronous)",
)
add_resource(
self._async_sessionmaker,
self.resource_name,
description="SQLAlchemy session factory (asynchronous)",
)
add_resource_factory(
self.create_async_session,
self.resource_name,
description="SQLAlchemy session (asynchronous)",
)
else:
Expand All @@ -253,24 +244,14 @@ async def start(self) -> None:

add_resource(
self._engine,
self.resource_name,
description="SQLAlchemy engine (synchronous)",
teardown_callback=teardown_callback,
)
add_resource(
self._sessionmaker,
self.resource_name,
description="SQLAlchemy session factory (synchronous)",
)
add_resource_factory(
self.create_session,
self.resource_name,
description="SQLAlchemy session (synchronous)",
)

logger.info(
"Configured SQLAlchemy resources (%s; dialect=%s, driver=%s)",
self.resource_name,
bind.dialect.name,
bind.dialect.driver,
)
40 changes: 11 additions & 29 deletions tests/test_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,47 +34,29 @@
pytestmark = pytest.mark.anyio


@pytest.mark.parametrize(
"component_opts, args",
[
pytest.param({}, ()),
pytest.param({"resource_name": "alternate"}, ("alternate",)),
],
)
async def test_component_start_sync(
component_opts: dict[str, Any], args: tuple[Any]
) -> None:
async def test_component_start_sync() -> None:
"""Test that the component creates all the expected (synchronous) resources."""
url = URL.create("sqlite", database=":memory:")
component = SQLAlchemyComponent(url=url, **component_opts)
component = SQLAlchemyComponent(url=url)
async with Context():
await component.start()

get_resource_nowait(Engine, *args)
get_resource_nowait(sessionmaker, *args)
get_resource_nowait(Session, *args)
get_resource_nowait(Engine)
get_resource_nowait(sessionmaker)
get_resource_nowait(Session)


@pytest.mark.parametrize(
"component_opts, args",
[
pytest.param({}, ()),
pytest.param({"resource_name": "alternate"}, ("alternate",)),
],
)
async def test_component_start_async(
component_opts: dict[str, Any], args: tuple[Any]
) -> None:
async def test_component_start_async() -> None:
"""Test that the component creates all the expected (asynchronous) resources."""
url = URL.create("sqlite+aiosqlite", database=":memory:")
component = SQLAlchemyComponent(url=url, **component_opts)
component = SQLAlchemyComponent(url=url)
async with Context():
await component.start()

get_resource_nowait(AsyncEngine, *args)
async_session_class = get_resource_nowait(async_sessionmaker, *args)
get_resource_nowait(AsyncSession, *args)
sync_session_class = get_resource_nowait(sessionmaker, *args)
get_resource_nowait(AsyncEngine)
async_session_class = get_resource_nowait(async_sessionmaker)
get_resource_nowait(AsyncSession)
sync_session_class = get_resource_nowait(sessionmaker)
assert async_session_class.kw["sync_session_class"] is sync_session_class


Expand Down

0 comments on commit c8b0275

Please sign in to comment.