Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
5c2aa6e
asynciopg fixtures
pcrespov Aug 30, 2024
662a3d7
minimal repo
pcrespov Aug 30, 2024
d08dca3
converting tags to asynciopg
pcrespov Aug 30, 2024
44abdc2
merged
pcrespov Sep 4, 2024
8b28a15
renames aiopg_engine
pcrespov Sep 4, 2024
383b890
test utils tags
pcrespov Sep 4, 2024
a6722f2
mv base to tests
pcrespov Sep 5, 2024
19e8763
adapting tags db
pcrespov Sep 5, 2024
99f63d6
fixing transactions
pcrespov Sep 5, 2024
80a421f
warns to move to the new engine
pcrespov Sep 9, 2024
4e13ee9
this is fastapi only
pcrespov Sep 9, 2024
f881d29
splits aiohttp and fastapi parts
pcrespov Sep 9, 2024
136c644
apis
pcrespov Sep 9, 2024
b7a0d90
refactor handlers
pcrespov Sep 9, 2024
96c8447
service layer
pcrespov Sep 9, 2024
0d04391
bad import
pcrespov Sep 9, 2024
bb86208
add hostname
pcrespov Sep 9, 2024
8c542ba
adds testsing
pcrespov Sep 9, 2024
2c9fc68
minor
pcrespov Sep 9, 2024
a592efb
pylint
pcrespov Sep 9, 2024
97f5161
pylint
pcrespov Sep 9, 2024
3eb4cc6
tests
pcrespov Sep 9, 2024
a2ee168
adapting base tests
pcrespov Sep 9, 2024
2553550
extending test
pcrespov Sep 9, 2024
ad9ef52
minor
pcrespov Sep 10, 2024
4d57abd
fixes test_base_repo
pcrespov Sep 11, 2024
d066b74
cleanup makefile
pcrespov Sep 11, 2024
79facb5
fixes mypy in pg
pcrespov Sep 11, 2024
2c68e00
cleanup
pcrespov Sep 11, 2024
3c209aa
rename aiopg key
pcrespov Sep 11, 2024
871cf95
log
pcrespov Sep 11, 2024
02a8c2a
align logs of aiopg and asyncpg
pcrespov Sep 11, 2024
e8b543c
updates settings
pcrespov Sep 11, 2024
31dbca0
aiopg vs asyncpg
pcrespov Sep 11, 2024
c75fd8d
improves logs
pcrespov Sep 11, 2024
4e586eb
rm query
pcrespov Sep 11, 2024
032e454
improved test
pcrespov Sep 11, 2024
fb175ad
updates notifications
pcrespov Sep 13, 2024
15a3992
patches
pcrespov Sep 13, 2024
dff4f6f
fixes mypy
pcrespov Sep 13, 2024
a6d5d5c
fixes mypy
pcrespov Sep 13, 2024
bb72bea
add more assumptions
pcrespov Sep 13, 2024
4d2bd08
adds ro user sh
pcrespov Sep 13, 2024
f336cc7
new envs and avoids error upon recreationg
pcrespov Sep 13, 2024
041bec1
tabs
pcrespov Sep 13, 2024
2eb5eae
minor
pcrespov Sep 13, 2024
283dc39
vendor.landing_page not used anymore. retired
pcrespov Sep 13, 2024
1ac57b2
rename base name
pcrespov Sep 23, 2024
ae26ab2
adds script
pcrespov Sep 23, 2024
ecdea9b
revert readonly user chagnes
pcrespov Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/models-library/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,15 @@ erd-ServiceInput.svg: _erdantic
DOWNLOADED_TEST_DATA_DIR = "$(CURDIR)/tests/data/.downloaded-ignore"

.PHONY: _httpx
_httpx: _check_venv_active
_ensure_httpx: _check_venv_active
# ensures requirements installed
@python3 -c "import httpx" 2>/dev/null || uv pip install httpx

PHONY: pull_test_data
pull_test_data: $(DOT_ENV_FILE) _httpx ## downloads tests data from registry (this can take some time!)
# downloading all metadata files
PHONY: tests-data
tests-data: $(DOT_ENV_FILE) _ensure_httpx ## downloads tests data from registry defined in .env (time-intensive!)
# Downloading all metadata files ...
@set -o allexport; \
source $<; \
set +o allexport; \
python3 "$(PACKAGES_DIR)/pytest-simcore/src/pytest_simcore/helpers/docker_registry.py" $(DOWNLOADED_TEST_DATA_DIR)
@echo "Run now 'pytest -vv -m diagnostics tests'"
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<p>Dear Support team

<p>
We have received the following request form for an account in {{ product.display_name }} from {{ host }}
We have received the following request form for an account in {{ product.display_name }} from <b>{{ host }}</b>
</p>

<pre>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Dear Support team,

We have received the following request form for an account in {{ product.display_name }} from {{ host }}:
We have received the following request form for an account in {{ product.display_name }} from **{{ host }}**:

{{ dumps(request_form) }}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import logging
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine

_logger = logging.getLogger(__name__)


@asynccontextmanager
async def get_or_create_connection(
engine: AsyncEngine, connection: AsyncConnection | None = None
) -> AsyncIterator[AsyncConnection]:
# NOTE: When connection is passed, the engine is actually not needed
# NOTE: Creator is responsible of closing connection
is_connection_created = connection is None
if is_connection_created:
connection = await engine.connect()
try:
assert connection # nosec
yield connection
finally:
assert connection # nosec
assert not connection.closed # nosec
if is_connection_created and connection:
await connection.close()


@asynccontextmanager
async def transaction_context(
engine: AsyncEngine, connection: AsyncConnection | None = None
):
async with get_or_create_connection(engine, connection) as conn:
if conn.in_transaction():
async with conn.begin_nested(): # inner transaction (savepoint)
yield conn
else:
try:
async with conn.begin(): # outer transaction (savepoint)
yield conn
finally:
assert not conn.closed # nosec
assert not conn.in_transaction() # nosec
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ class Vendor(TypedDict, total=False):
invitation_url: str # How to request a trial invitation? (if applies)
invitation_form: bool # If True, it takes precendence over invitation_url and asks the FE to show the form (if defined)

has_landing_page: bool # Is Landing page enabled

release_notes_url_template: str # a template url where `{vtag}` will be replaced, eg: "http://example.com/{vtag}.md"


Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
""" Repository pattern, errors and data structures for models.tags
"""

import itertools
from dataclasses import dataclass
from typing import TypedDict

from aiopg.sa.connection import SAConnection
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine

from .base_repo import get_or_create_connection, transaction_context
from .utils_tags_sql import (
count_users_with_access_rights_stmt,
create_tag_stmt,
Expand Down Expand Up @@ -49,15 +48,16 @@ class TagDict(TypedDict, total=True):
delete: bool


@dataclass(frozen=True)
class TagsRepo:
user_id: int # Determines access-rights
def __init__(self, engine: AsyncEngine):
self.engine = engine

async def access_count(
self,
conn: SAConnection,
tag_id: int,
connection: AsyncConnection | None = None,
*,
user_id: int,
tag_id: int,
read: bool | None = None,
write: bool | None = None,
delete: bool | None = None,
Expand All @@ -66,20 +66,22 @@ async def access_count(
Returns 0 if tag does not match access
Returns >0 if it does and represents the number of groups granting this access to the user
"""
count_stmt = count_users_with_access_rights_stmt(
user_id=self.user_id, tag_id=tag_id, read=read, write=write, delete=delete
)
permissions_count: int | None = await conn.scalar(count_stmt)
return permissions_count if permissions_count else 0
async with get_or_create_connection(self.engine, connection) as conn:
count_stmt = count_users_with_access_rights_stmt(
user_id=user_id, tag_id=tag_id, read=read, write=write, delete=delete
)
permissions_count: int | None = await conn.scalar(count_stmt)
return permissions_count if permissions_count else 0

#
# CRUD operations
#

async def create(
self,
conn: SAConnection,
connection: AsyncConnection | None = None,
*,
user_id: int,
name: str,
color: str,
description: str | None = None, # =nullable
Expand All @@ -94,69 +96,127 @@ async def create(
if description:
values["description"] = description

async with conn.begin():
async with transaction_context(self.engine, connection) as conn:
# insert new tag
insert_stmt = create_tag_stmt(**values)
result = await conn.execute(insert_stmt)
tag = await result.first()
tag = result.first()
assert tag # nosec

# take tag ownership
access_stmt = set_tag_access_rights_stmt(
tag_id=tag.id,
user_id=self.user_id,
user_id=user_id,
read=read,
write=write,
delete=delete,
)
result = await conn.execute(access_stmt)
access = await result.first()
assert access

return TagDict(itertools.chain(tag.items(), access.items())) # type: ignore

async def list_all(self, conn: SAConnection) -> list[TagDict]:
stmt_list = list_tags_stmt(user_id=self.user_id)
return [TagDict(row.items()) async for row in conn.execute(stmt_list)] # type: ignore
access = result.first()
assert access # nosec

return TagDict(
id=tag.id,
name=tag.name,
description=tag.description,
color=tag.color,
read=access.read,
write=access.write,
delete=access.delete,
)

async def get(self, conn: SAConnection, tag_id: int) -> TagDict:
stmt_get = get_tag_stmt(user_id=self.user_id, tag_id=tag_id)
result = await conn.execute(stmt_get)
row = await result.first()
if not row:
msg = f"{tag_id=} not found: either no access or does not exists"
raise TagNotFoundError(msg)
return TagDict(row.items()) # type: ignore
async def list_all(
self,
connection: AsyncConnection | None = None,
*,
user_id: int,
) -> list[TagDict]:
async with get_or_create_connection(self.engine, connection) as conn:
stmt_list = list_tags_stmt(user_id=user_id)
result = await conn.stream(stmt_list)
return [
TagDict(
id=row.id,
name=row.name,
description=row.description,
color=row.color,
read=row.read,
write=row.write,
delete=row.delete,
)
async for row in result
]

async def get(
self,
connection: AsyncConnection | None = None,
*,
user_id: int,
tag_id: int,
) -> TagDict:
stmt_get = get_tag_stmt(user_id=user_id, tag_id=tag_id)
async with get_or_create_connection(self.engine, connection) as conn:
result = await conn.execute(stmt_get)
row = result.first()
if not row:
msg = f"{tag_id=} not found: either no access or does not exists"
raise TagNotFoundError(msg)
return TagDict(
id=row.id,
name=row.name,
description=row.description,
color=row.color,
read=row.read,
write=row.write,
delete=row.delete,
)

async def update(
self,
conn: SAConnection,
connection: AsyncConnection | None = None,
*,
user_id: int,
tag_id: int,
**fields,
) -> TagDict:
updates = {
name: value
for name, value in fields.items()
if name in {"name", "color", "description"}
}

if not updates:
# no updates == get
return await self.get(conn, tag_id=tag_id)

update_stmt = update_tag_stmt(user_id=self.user_id, tag_id=tag_id, **updates)
result = await conn.execute(update_stmt)
row = await result.first()
if not row:
msg = f"{tag_id=} not updated: either no access or not found"
raise TagOperationNotAllowedError(msg)

return TagDict(row.items()) # type: ignore

async def delete(self, conn: SAConnection, tag_id: int) -> None:
stmt_delete = delete_tag_stmt(user_id=self.user_id, tag_id=tag_id)
async with transaction_context(self.engine, connection) as conn:
updates = {
name: value
for name, value in fields.items()
if name in {"name", "color", "description"}
}

if not updates:
# no updates == get
return await self.get(conn, user_id=user_id, tag_id=tag_id)

update_stmt = update_tag_stmt(user_id=user_id, tag_id=tag_id, **updates)
result = await conn.execute(update_stmt)
row = result.first()
if not row:
msg = f"{tag_id=} not updated: either no access or not found"
raise TagOperationNotAllowedError(msg)

return TagDict(
id=row.id,
name=row.name,
description=row.description,
color=row.color,
read=row.read,
write=row.write,
delete=row.delete,
)

deleted = await conn.scalar(stmt_delete)
if not deleted:
msg = f"Could not delete {tag_id=}. Not found or insuficient access."
raise TagOperationNotAllowedError(msg)
async def delete(
self,
connection: AsyncConnection | None = None,
*,
user_id: int,
tag_id: int,
) -> None:
stmt_delete = delete_tag_stmt(user_id=user_id, tag_id=tag_id)
async with transaction_context(self.engine, connection) as conn:
deleted = await conn.scalar(stmt_delete)
if not deleted:
msg = f"Could not delete {tag_id=}. Not found or insuficient access."
raise TagOperationNotAllowedError(msg)
Loading