simple-module-test 0.0.35__tar.gz → 0.0.37__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/.gitignore +1 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/PKG-INFO +5 -4
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/README.md +1 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/pyproject.toml +4 -4
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/simple_module_test/__init__.py +5 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/simple_module_test/database.py +14 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/simple_module_test/plugin.py +74 -0
- simple_module_test-0.0.37/simple_module_test/setup_wizard.py +37 -0
- simple_module_test-0.0.37/simple_module_test/tenant_client.py +125 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/LICENSE +0 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/simple_module_test/_schema.py +0 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/simple_module_test/app_factory.py +0 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/simple_module_test/fake_events.py +0 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/simple_module_test/fixtures.py +0 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/simple_module_test/redis_server.py +0 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/simple_module_test/routes.py +0 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/simple_module_test/session_cookie.py +0 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/tests/test_app_factory.py +0 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/tests/test_fake_events.py +0 -0
- {simple_module_test-0.0.35 → simple_module_test-0.0.37}/tests/test_plugin_fixtures.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_test
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.37
|
|
4
4
|
Summary: Shared pytest fixtures (app, client, db_session, authenticated_client) for writing simple_module module tests
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -25,9 +25,9 @@ Requires-Dist: fastapi>=0.115
|
|
|
25
25
|
Requires-Dist: httpx>=0.27
|
|
26
26
|
Requires-Dist: pytest-asyncio>=0.24
|
|
27
27
|
Requires-Dist: pytest>=8.0
|
|
28
|
-
Requires-Dist: simple-module-core==0.0.
|
|
29
|
-
Requires-Dist: simple-module-db==0.0.
|
|
30
|
-
Requires-Dist: simple-module-hosting==0.0.
|
|
28
|
+
Requires-Dist: simple-module-core==0.0.37
|
|
29
|
+
Requires-Dist: simple-module-db==0.0.37
|
|
30
|
+
Requires-Dist: simple-module-hosting==0.0.37
|
|
31
31
|
Requires-Dist: sqlalchemy>=2.0
|
|
32
32
|
Description-Content-Type: text/markdown
|
|
33
33
|
|
|
@@ -56,6 +56,7 @@ All fixtures below are auto-registered via the `pytest11` entry point — instal
|
|
|
56
56
|
- `app` — a `create_app(settings)` instance with `lifespan` started and stopped.
|
|
57
57
|
- `client` — an `httpx.AsyncClient` bound to the test app.
|
|
58
58
|
- `authenticated_client` — same but with an admin user seeded and a forged session cookie attached. **Requires the `users` module** to be installed (it seeds the admin via `users.bootstrap`); apps scaffolded by `smpy` include it.
|
|
59
|
+
- `tenant_client` — factory: `async with tenant_client("member") as (client, tenant_id, user_id)` signs in a fresh user who is an `owner` / `admin` / `member` (default `owner`) of a new tenant — or of `tenant_id=` when given — with that tenant active. **Requires the `users` and `tenants` modules.**
|
|
59
60
|
|
|
60
61
|
## Usage
|
|
61
62
|
|
|
@@ -23,6 +23,7 @@ All fixtures below are auto-registered via the `pytest11` entry point — instal
|
|
|
23
23
|
- `app` — a `create_app(settings)` instance with `lifespan` started and stopped.
|
|
24
24
|
- `client` — an `httpx.AsyncClient` bound to the test app.
|
|
25
25
|
- `authenticated_client` — same but with an admin user seeded and a forged session cookie attached. **Requires the `users` module** to be installed (it seeds the admin via `users.bootstrap`); apps scaffolded by `smpy` include it.
|
|
26
|
+
- `tenant_client` — factory: `async with tenant_client("member") as (client, tenant_id, user_id)` signs in a fresh user who is an `owner` / `admin` / `member` (default `owner`) of a new tenant — or of `tenant_id=` when given — with that tenant active. **Requires the `users` and `tenants` modules.**
|
|
26
27
|
|
|
27
28
|
## Usage
|
|
28
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_test"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.37"
|
|
4
4
|
description = "Shared pytest fixtures (app, client, db_session, authenticated_client) for writing simple_module module tests"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -25,9 +25,9 @@ dependencies = [
|
|
|
25
25
|
"httpx>=0.27",
|
|
26
26
|
"pytest>=8.0",
|
|
27
27
|
"pytest-asyncio>=0.24",
|
|
28
|
-
"simple_module_core==0.0.
|
|
29
|
-
"simple_module_db==0.0.
|
|
30
|
-
"simple_module_hosting==0.0.
|
|
28
|
+
"simple_module_core==0.0.37",
|
|
29
|
+
"simple_module_db==0.0.37",
|
|
30
|
+
"simple_module_hosting==0.0.37",
|
|
31
31
|
"sqlalchemy>=2.0",
|
|
32
32
|
]
|
|
33
33
|
|
|
@@ -20,16 +20,21 @@ real broker (cross-worker cache invalidation) and skips when the binary is absen
|
|
|
20
20
|
See ``simple_module_test.plugin`` (and ``simple_module_test.fixtures``) for
|
|
21
21
|
their definitions. ``authenticated_client`` additionally requires the ``users``
|
|
22
22
|
module to be installed — it seeds an admin via ``users.bootstrap``.
|
|
23
|
+
``tenant_client`` (``users`` + ``tenants``) is a factory yielding a client signed
|
|
24
|
+
in as an ``owner``/``admin``/``member`` of a real tenant — see
|
|
25
|
+
``simple_module_test.tenant_client``.
|
|
23
26
|
"""
|
|
24
27
|
|
|
25
28
|
from simple_module_test.app_factory import build_test_app
|
|
26
29
|
from simple_module_test.fake_events import FakeEventBus, RecordedEvent
|
|
27
30
|
from simple_module_test.routes import effective_route_paths
|
|
28
31
|
from simple_module_test.session_cookie import forge_session_cookie
|
|
32
|
+
from simple_module_test.tenant_client import TenantClient
|
|
29
33
|
|
|
30
34
|
__all__ = [
|
|
31
35
|
"FakeEventBus",
|
|
32
36
|
"RecordedEvent",
|
|
37
|
+
"TenantClient",
|
|
33
38
|
"build_test_app",
|
|
34
39
|
"effective_route_paths",
|
|
35
40
|
"forge_session_cookie",
|
|
@@ -26,6 +26,10 @@ def database_url_for_tests() -> str:
|
|
|
26
26
|
return os.environ.get(TEST_DATABASE_ENV) or SQLITE_MEMORY
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def database_is_postgres() -> bool:
|
|
30
|
+
return database_url_for_tests().startswith("postgresql")
|
|
31
|
+
|
|
32
|
+
|
|
29
33
|
def is_sqlite(url: str) -> bool:
|
|
30
34
|
return url.startswith("sqlite")
|
|
31
35
|
|
|
@@ -41,6 +45,15 @@ async def reset_schema(engine: Any) -> None:
|
|
|
41
45
|
async with engine.begin() as conn:
|
|
42
46
|
await conn.execute(text("DROP SCHEMA IF EXISTS public CASCADE"))
|
|
43
47
|
await conn.execute(text("CREATE SCHEMA public"))
|
|
48
|
+
# The drop took the installed extensions with it; modules' models need them.
|
|
49
|
+
await conn.run_sync(_create_declared_extensions)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _create_declared_extensions(sync_conn: Any) -> None:
|
|
53
|
+
from simple_module_core.discovery import discover_modules
|
|
54
|
+
from simple_module_db.extensions import ensure_db_extensions
|
|
55
|
+
|
|
56
|
+
ensure_db_extensions(sync_conn, discover_modules())
|
|
44
57
|
|
|
45
58
|
|
|
46
59
|
# The test the schema was last emptied for. On SQLite every engine is its own
|
|
@@ -70,6 +83,7 @@ __all__ = [
|
|
|
70
83
|
"SQLITE_MEMORY",
|
|
71
84
|
"TEST_DATABASE_ENV",
|
|
72
85
|
"begin_test",
|
|
86
|
+
"database_is_postgres",
|
|
73
87
|
"database_url_for_tests",
|
|
74
88
|
"init_db_kwargs",
|
|
75
89
|
"is_sqlite",
|
|
@@ -34,6 +34,37 @@ from simple_module_test.fixtures import ( # noqa: F401
|
|
|
34
34
|
setup_pending_app,
|
|
35
35
|
)
|
|
36
36
|
from simple_module_test.redis_server import redis_server # noqa: F401
|
|
37
|
+
from simple_module_test.tenant_client import tenant_client # noqa: F401
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def pytest_configure(config: pytest.Config) -> None:
|
|
41
|
+
config.addinivalue_line(
|
|
42
|
+
"markers",
|
|
43
|
+
"postgres: needs a PostgreSQL test database (SM_TEST_DATABASE_URL=postgresql+...)",
|
|
44
|
+
)
|
|
45
|
+
from simple_module_test.database import database_is_postgres
|
|
46
|
+
|
|
47
|
+
# Every Postgres test drops and recreates ``public`` in the one database, so
|
|
48
|
+
# xdist workers would wipe each other's schema mid-test.
|
|
49
|
+
if database_is_postgres() and getattr(config.option, "numprocesses", None):
|
|
50
|
+
raise pytest.UsageError(
|
|
51
|
+
"SM_TEST_DATABASE_URL is Postgres, whose schema every test drops: run "
|
|
52
|
+
"serially with `make test-py-pg` or `-n 0` (`PYTEST_WORKERS=0 make test-py`)"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
|
|
57
|
+
"""Skip ``@pytest.mark.postgres`` tests unless the test database is Postgres."""
|
|
58
|
+
from simple_module_test.database import database_is_postgres
|
|
59
|
+
|
|
60
|
+
if database_is_postgres():
|
|
61
|
+
return
|
|
62
|
+
skip = pytest.mark.skip(
|
|
63
|
+
reason="needs Postgres: set SM_TEST_DATABASE_URL=postgresql+asyncpg://..."
|
|
64
|
+
)
|
|
65
|
+
for item in items:
|
|
66
|
+
if "postgres" in item.keywords:
|
|
67
|
+
item.add_marker(skip)
|
|
37
68
|
|
|
38
69
|
|
|
39
70
|
def _bootstrap_eager_celery() -> None:
|
|
@@ -64,6 +95,49 @@ def _bootstrap_eager_celery() -> None:
|
|
|
64
95
|
_bootstrap_eager_celery()
|
|
65
96
|
|
|
66
97
|
|
|
98
|
+
def _cheap_password_hashing() -> None:
|
|
99
|
+
"""Make ``PasswordHelper()`` hash with minimal Argon2/bcrypt cost.
|
|
100
|
+
|
|
101
|
+
Every ``app`` fixture seeds an admin, and production-cost Argon2 is ~90ms
|
|
102
|
+
of CPU per hash — over a tenth of each app's setup, paid thousands of times
|
|
103
|
+
a run. The hashes stay real ``$argon2``/``$2b`` strings that verify and
|
|
104
|
+
upgrade through the same code paths; only their work factor drops, which is
|
|
105
|
+
a property of deployment rather than anything a test asserts.
|
|
106
|
+
|
|
107
|
+
Patched at import, not in a fixture: test modules build ``PasswordHelper()``
|
|
108
|
+
at module scope during collection, before any fixture runs. An explicit
|
|
109
|
+
``password_hash=`` still wins. Skipped when ``fastapi_users`` is absent, or
|
|
110
|
+
when ``SM_TEST_REAL_PASSWORD_HASHING`` is set — the opt-out for a consumer
|
|
111
|
+
suite whose tests depend on production hash parameters.
|
|
112
|
+
"""
|
|
113
|
+
if os.environ.get("SM_TEST_REAL_PASSWORD_HASHING"):
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
from fastapi_users.password import PasswordHelper
|
|
117
|
+
from pwdlib import PasswordHash
|
|
118
|
+
from pwdlib.hashers.argon2 import Argon2Hasher
|
|
119
|
+
from pwdlib.hashers.bcrypt import BcryptHasher
|
|
120
|
+
except ImportError:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
original_init = PasswordHelper.__init__
|
|
124
|
+
|
|
125
|
+
def _init(self, password_hash: PasswordHash | None = None) -> None:
|
|
126
|
+
if password_hash is None:
|
|
127
|
+
password_hash = PasswordHash(
|
|
128
|
+
(
|
|
129
|
+
Argon2Hasher(time_cost=1, memory_cost=8, parallelism=1),
|
|
130
|
+
BcryptHasher(rounds=4),
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
original_init(self, password_hash)
|
|
134
|
+
|
|
135
|
+
PasswordHelper.__init__ = _init # type: ignore[method-assign]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
_cheap_password_hashing()
|
|
139
|
+
|
|
140
|
+
|
|
67
141
|
@pytest.fixture
|
|
68
142
|
def fake_event_bus() -> FakeEventBus:
|
|
69
143
|
"""Fresh recording EventBus for each test."""
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Helpers for driving the ``/setup`` wizard from tests.
|
|
2
|
+
|
|
3
|
+
Every wizard mutation carries the session-bound CSRF token, so a test has to
|
|
4
|
+
do what the page does: load the wizard (which mints the token into the
|
|
5
|
+
session cookie and hands it out as the ``csrfToken`` prop) and echo it back
|
|
6
|
+
as ``X-CSRF-Token``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from simple_module_hosting.csrf import CSRF_HEADER
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def wizard_client(app) -> httpx.AsyncClient:
|
|
16
|
+
"""An anonymous client for *app*; it keeps the session cookie between calls."""
|
|
17
|
+
return httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://testserver")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def wizard_headers(client: httpx.AsyncClient) -> dict[str, str]:
|
|
21
|
+
"""Load the wizard as Inertia would and return the headers its forms send.
|
|
22
|
+
|
|
23
|
+
Raises ``AssertionError`` when the wizard is closed (404) — a test that
|
|
24
|
+
expected to post into it would otherwise fail with a confusing 403.
|
|
25
|
+
"""
|
|
26
|
+
resp = await client.get("/setup", headers={"X-Inertia": "true"})
|
|
27
|
+
assert resp.status_code == 200, f"wizard not open: {resp.status_code}"
|
|
28
|
+
token = resp.json()["props"]["csrfToken"]
|
|
29
|
+
return {CSRF_HEADER: token, "Accept": "application/json"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def post_step(
|
|
33
|
+
client: httpx.AsyncClient, step_id: str, data: dict | None = None
|
|
34
|
+
) -> httpx.Response:
|
|
35
|
+
"""Submit *step_id*'s wizard form, CSRF token included."""
|
|
36
|
+
headers = await wizard_headers(client)
|
|
37
|
+
return await client.post(f"/setup/steps/{step_id}", json=data or {}, headers=headers)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""``tenant_client``: a signed-in member of a real tenant, with a chosen role.
|
|
2
|
+
|
|
3
|
+
For a module adopting ``MultiTenantMixin``: its tests need requests that act
|
|
4
|
+
for one tenant, as an ``owner``, ``admin`` or ``member`` there — the role that
|
|
5
|
+
reaches the principal as ``tenant:<role>``::
|
|
6
|
+
|
|
7
|
+
async def test_members_can_read(tenant_client):
|
|
8
|
+
async with tenant_client("member") as (client, tenant_id, user_id):
|
|
9
|
+
resp = await client.get("/api/things")
|
|
10
|
+
|
|
11
|
+
async def test_isolation(tenant_client):
|
|
12
|
+
async with tenant_client() as a, tenant_client() as b:
|
|
13
|
+
... # two tenants, two owners
|
|
14
|
+
|
|
15
|
+
async with tenant_client("owner") as owner, tenant_client(
|
|
16
|
+
"member", tenant_id=owner.tenant_id
|
|
17
|
+
) as member:
|
|
18
|
+
... # two users of the same tenant
|
|
19
|
+
|
|
20
|
+
Requires the ``users`` and ``tenants`` modules. Both are imported inside the
|
|
21
|
+
fixture body, never at module scope, so this plugin still imports cleanly in
|
|
22
|
+
an app that has neither — the same rule ``authenticated_client`` follows.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import uuid
|
|
28
|
+
from collections.abc import AsyncGenerator, Callable
|
|
29
|
+
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
|
30
|
+
from typing import Any, NamedTuple
|
|
31
|
+
|
|
32
|
+
import httpx
|
|
33
|
+
import pytest
|
|
34
|
+
from simple_module_core.tenancy import TenantRole
|
|
35
|
+
|
|
36
|
+
from simple_module_test.session_cookie import forge_session_cookie
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TenantClient(NamedTuple):
|
|
40
|
+
client: httpx.AsyncClient
|
|
41
|
+
tenant_id: str
|
|
42
|
+
user_id: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def create_user(app: Any, email: str) -> str:
|
|
46
|
+
"""A real, active, non-superuser ``users`` row with the ``user`` role; its id."""
|
|
47
|
+
from users.bootstrap import ensure_user_role
|
|
48
|
+
from users.models import User, UserRole
|
|
49
|
+
|
|
50
|
+
async with app.state.sm.db.session_factory() as session:
|
|
51
|
+
user = User(
|
|
52
|
+
id=uuid.uuid4(),
|
|
53
|
+
email=email,
|
|
54
|
+
hashed_password="x",
|
|
55
|
+
is_active=True,
|
|
56
|
+
is_superuser=False,
|
|
57
|
+
is_verified=True,
|
|
58
|
+
)
|
|
59
|
+
session.add(user)
|
|
60
|
+
await session.flush()
|
|
61
|
+
# Seeded lazily: test schemas come from ``create_all`` with no seed rows,
|
|
62
|
+
# and a missing role must not silently leave the user role-less.
|
|
63
|
+
role = await ensure_user_role(session)
|
|
64
|
+
session.add(UserRole(user_id=user.id, role_id=role.id))
|
|
65
|
+
await session.commit()
|
|
66
|
+
return str(user.id)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _join(app: Any, user_id: str, email: str, role: TenantRole, tenant_id: str | None) -> str:
|
|
70
|
+
from tenants.models import Membership, Tenant
|
|
71
|
+
from tenants.resolver import forget
|
|
72
|
+
|
|
73
|
+
async with app.state.sm.db.session_factory() as session:
|
|
74
|
+
if tenant_id is None:
|
|
75
|
+
tenant = Tenant(slug=f"t-{uuid.uuid4().hex[:12]}", name="Test Org")
|
|
76
|
+
session.add(tenant)
|
|
77
|
+
await session.flush()
|
|
78
|
+
tenant_id = tenant.id
|
|
79
|
+
session.add(Membership(tenant_id=tenant_id, user_id=user_id, role=role, email=email))
|
|
80
|
+
await session.commit()
|
|
81
|
+
forget(user_id) # the resolver caches memberships per user
|
|
82
|
+
return tenant_id
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@asynccontextmanager
|
|
86
|
+
async def session_client(app: Any, data: dict[str, Any]) -> AsyncGenerator[httpx.AsyncClient]:
|
|
87
|
+
"""An ``httpx`` client for ``app`` whose signed session cookie holds ``data``."""
|
|
88
|
+
cookie = forge_session_cookie(app.state.sm.settings.secret_key, data)
|
|
89
|
+
async with httpx.AsyncClient(
|
|
90
|
+
transport=httpx.ASGITransport(app=app),
|
|
91
|
+
base_url="http://testserver",
|
|
92
|
+
cookies={"session": cookie},
|
|
93
|
+
) as client:
|
|
94
|
+
yield client
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.fixture
|
|
98
|
+
def tenant_client(app: Any) -> Callable[..., AbstractAsyncContextManager[TenantClient]]:
|
|
99
|
+
"""Factory: ``tenant_client(role="owner", *, tenant_id=None, email=None)``.
|
|
100
|
+
|
|
101
|
+
Creates a user and a membership with ``role`` — in a new tenant, or in
|
|
102
|
+
``tenant_id`` when given — and yields a :class:`TenantClient` whose session
|
|
103
|
+
has that tenant active.
|
|
104
|
+
"""
|
|
105
|
+
from tenants.constants import SESSION_ACTIVE_TENANT
|
|
106
|
+
|
|
107
|
+
@asynccontextmanager
|
|
108
|
+
async def factory(
|
|
109
|
+
role: str = TenantRole.OWNER,
|
|
110
|
+
*,
|
|
111
|
+
tenant_id: str | None = None,
|
|
112
|
+
email: str | None = None,
|
|
113
|
+
) -> AsyncGenerator[TenantClient]:
|
|
114
|
+
member_role = TenantRole(role)
|
|
115
|
+
email = email or f"{member_role}-{uuid.uuid4().hex[:8]}@example.com"
|
|
116
|
+
user_id = await create_user(app, email)
|
|
117
|
+
tenant_id = await _join(app, user_id, email, member_role, tenant_id)
|
|
118
|
+
data = {"user_id": user_id, SESSION_ACTIVE_TENANT: tenant_id}
|
|
119
|
+
async with session_client(app, data) as client:
|
|
120
|
+
yield TenantClient(client, tenant_id, user_id)
|
|
121
|
+
|
|
122
|
+
return factory
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = ["TenantClient", "create_user", "session_client", "tenant_client"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_test-0.0.35 → simple_module_test-0.0.37}/simple_module_test/session_cookie.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|