Skip to content

Commit cfa10c0

Browse files
pcrespovmrnicegyu11
authored andcommitted
♻️🎨 web-server: enhances product domain (ITISFoundation#7294)
1 parent c8c1f1b commit cfa10c0

File tree

73 files changed

+1168
-810
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+1168
-810
lines changed

api/specs/web-server/_products.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import Annotated
99

1010
from fastapi import APIRouter, Depends
11-
from models_library.api_schemas_webserver.product import (
11+
from models_library.api_schemas_webserver.products import (
1212
CreditPriceGet,
1313
InvitationGenerate,
1414
InvitationGenerated,
@@ -17,7 +17,9 @@
1717
)
1818
from models_library.generics import Envelope
1919
from simcore_service_webserver._meta import API_VTAG
20-
from simcore_service_webserver.products._rest_schemas import ProductsRequestParams
20+
from simcore_service_webserver.products._controller.rest_schemas import (
21+
ProductsRequestParams,
22+
)
2123

2224
router = APIRouter(
2325
prefix=f"/{API_VTAG}",
@@ -31,8 +33,7 @@
3133
"/credits-price",
3234
response_model=Envelope[CreditPriceGet],
3335
)
34-
async def get_current_product_price():
35-
...
36+
async def get_current_product_price(): ...
3637

3738

3839
@router.get(
@@ -43,16 +44,14 @@ async def get_current_product_price():
4344
"po",
4445
],
4546
)
46-
async def get_product(_params: Annotated[ProductsRequestParams, Depends()]):
47-
...
47+
async def get_product(_params: Annotated[ProductsRequestParams, Depends()]): ...
4848

4949

5050
@router.get(
5151
"/products/current/ui",
5252
response_model=Envelope[ProductUIGet],
5353
)
54-
async def get_current_product_ui():
55-
...
54+
async def get_current_product_ui(): ...
5655

5756

5857
@router.post(
@@ -62,5 +61,4 @@ async def get_current_product_ui():
6261
"po",
6362
],
6463
)
65-
async def generate_invitation(_body: InvitationGenerate):
66-
...
64+
async def generate_invitation(_body: InvitationGenerate): ...

packages/models-library/src/models_library/api_schemas_webserver/product.py renamed to packages/models-library/src/models_library/api_schemas_webserver/products.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from datetime import datetime
2+
from decimal import Decimal
23
from typing import Annotated, Any, TypeAlias
34

45
from common_library.basic_types import DEFAULT_FACTORY
56
from pydantic import (
7+
BaseModel,
68
ConfigDict,
79
Field,
810
HttpUrl,
@@ -19,6 +21,28 @@
1921
from ._base import InputSchema, OutputSchema
2022

2123

24+
class CreditResultRpcGet(BaseModel):
25+
product_name: ProductName
26+
credit_amount: Decimal
27+
28+
@staticmethod
29+
def _update_json_schema_extra(schema: JsonDict) -> None:
30+
schema.update(
31+
{
32+
"examples": [
33+
{
34+
"product_name": "s4l",
35+
"credit_amount": Decimal("15.5"), # type: ignore[dict-item]
36+
},
37+
]
38+
}
39+
)
40+
41+
model_config = ConfigDict(
42+
json_schema_extra=_update_json_schema_extra,
43+
)
44+
45+
2246
class CreditPriceGet(OutputSchema):
2347
product_name: str
2448
usd_per_credit: Annotated[
Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,5 @@
1-
from decimal import Decimal
21
from typing import TypeAlias
32

4-
from pydantic import BaseModel, ConfigDict, Field
5-
63
ProductName: TypeAlias = str
74
StripePriceID: TypeAlias = str
85
StripeTaxRateID: TypeAlias = str
9-
10-
11-
class CreditResultGet(BaseModel):
12-
product_name: ProductName
13-
credit_amount: Decimal = Field(..., description="")
14-
15-
model_config = ConfigDict(
16-
json_schema_extra={
17-
"examples": [
18-
{"product_name": "s4l", "credit_amount": Decimal(15.5)}, # type: ignore[dict-item]
19-
]
20-
}
21-
)
22-
23-
24-
class ProductStripeInfoGet(BaseModel):
25-
stripe_price_id: StripePriceID
26-
stripe_tax_rate_id: StripeTaxRateID
27-
model_config = ConfigDict(
28-
json_schema_extra={
29-
"examples": [
30-
{
31-
"stripe_price_id": "stripe-price-id",
32-
"stripe_tax_rate_id": "stripe-tax-rate-id",
33-
},
34-
]
35-
}
36-
)

packages/postgres-database/src/simcore_postgres_database/errors.py renamed to packages/postgres-database/src/simcore_postgres_database/aiopg_errors.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
1-
""" aiopg errors
1+
"""aiopg errors
22
3-
StandardError
4-
|__ Warning
5-
|__ Error
6-
|__ InterfaceError
7-
|__ DatabaseError
8-
|__ DataError
9-
|__ OperationalError
10-
|__ IntegrityError
11-
|__ InternalError
12-
|__ ProgrammingError
13-
|__ NotSupportedError
3+
WARNING: these errors are not raised by asyncpg. Therefore all code using new sqlalchemy.ext.asyncio
4+
MUST use instead import sqlalchemy.exc exceptions!!!!
145
15-
- aiopg reuses DBAPI exceptions
16-
SEE https://aiopg.readthedocs.io/en/stable/core.html?highlight=Exception#exceptions
17-
SEE http://initd.org/psycopg/docs/module.html#dbapi-exceptions
18-
SEE https://www.postgresql.org/docs/current/errcodes-appendix.html
6+
StandardError
7+
|__ Warning
8+
|__ Error
9+
|__ InterfaceError
10+
|__ DatabaseError
11+
|__ DataError
12+
|__ OperationalError
13+
|__ IntegrityError
14+
|__ InternalError
15+
|__ ProgrammingError
16+
|__ NotSupportedError
17+
18+
- aiopg reuses DBAPI exceptions
19+
SEE https://aiopg.readthedocs.io/en/stable/core.html?highlight=Exception#exceptions
20+
SEE http://initd.org/psycopg/docs/module.html#dbapi-exceptions
21+
SEE https://www.postgresql.org/docs/current/errcodes-appendix.html
1922
"""
23+
2024
# NOTE: psycopg2.errors are created dynamically
2125
# pylint: disable=no-name-in-module
22-
from psycopg2 import DatabaseError, DataError
26+
from psycopg2 import (
27+
DatabaseError,
28+
DataError,
29+
)
2330
from psycopg2 import Error as DBAPIError
2431
from psycopg2 import (
2532
IntegrityError,

packages/postgres-database/src/simcore_postgres_database/utils_payments.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from aiopg.sa.connection import SAConnection
99
from aiopg.sa.result import ResultProxy, RowProxy
1010

11-
from . import errors
11+
from . import aiopg_errors
1212
from .models.payments_transactions import PaymentTransactionState, payments_transactions
1313

1414
_logger = logging.getLogger(__name__)
@@ -29,16 +29,13 @@ def __bool__(self):
2929
return False
3030

3131

32-
class PaymentAlreadyExists(PaymentFailure):
33-
...
32+
class PaymentAlreadyExists(PaymentFailure): ...
3433

3534

36-
class PaymentNotFound(PaymentFailure):
37-
...
35+
class PaymentNotFound(PaymentFailure): ...
3836

3937

40-
class PaymentAlreadyAcked(PaymentFailure):
41-
...
38+
class PaymentAlreadyAcked(PaymentFailure): ...
4239

4340

4441
async def insert_init_payment_transaction(
@@ -69,7 +66,7 @@ async def insert_init_payment_transaction(
6966
initiated_at=initiated_at,
7067
)
7168
)
72-
except errors.UniqueViolation:
69+
except aiopg_errors.UniqueViolation:
7370
return PaymentAlreadyExists(payment_id)
7471

7572
return payment_id
Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
""" Common functions to access products table
2-
3-
"""
4-
5-
import warnings
1+
"""Common functions to access products table"""
62

73
import sqlalchemy as sa
4+
from sqlalchemy.ext.asyncio import AsyncConnection
85

9-
from ._protocols import AiopgConnection, DBConnection
106
from .models.groups import GroupType, groups
117
from .models.products import products
128

139
# NOTE: outside this module, use instead packages/models-library/src/models_library/users.py
1410
_GroupID = int
1511

1612

17-
async def get_default_product_name(conn: DBConnection) -> str:
13+
class EmptyProductsError(ValueError): ...
14+
15+
16+
async def get_default_product_name(conn: AsyncConnection) -> str:
1817
"""The first row in the table is considered as the default product
1918
2019
:: raises ValueError if undefined
@@ -23,23 +22,25 @@ async def get_default_product_name(conn: DBConnection) -> str:
2322
sa.select(products.c.name).order_by(products.c.priority)
2423
)
2524
if not product_name:
26-
msg = "No product defined in database"
27-
raise ValueError(msg)
25+
msg = "No product was defined in database. Upon construction, at least one product is added but there are none."
26+
raise EmptyProductsError(msg)
2827

2928
assert isinstance(product_name, str) # nosec
3029
return product_name
3130

3231

33-
async def get_product_group_id(
34-
connection: DBConnection, product_name: str
32+
async def get_product_group_id_or_none(
33+
connection: AsyncConnection, product_name: str
3534
) -> _GroupID | None:
3635
group_id = await connection.scalar(
3736
sa.select(products.c.group_id).where(products.c.name == product_name)
3837
)
3938
return None if group_id is None else _GroupID(group_id)
4039

4140

42-
async def execute_get_or_create_product_group(conn, product_name: str) -> int:
41+
async def get_or_create_product_group(
42+
conn: AsyncConnection, product_name: str
43+
) -> _GroupID:
4344
#
4445
# NOTE: Separated so it can be used in asyncpg and aiopg environs while both
4546
# coexist
@@ -70,23 +71,3 @@ async def execute_get_or_create_product_group(conn, product_name: str) -> int:
7071
)
7172

7273
return group_id
73-
74-
75-
async def get_or_create_product_group(
76-
connection: AiopgConnection, product_name: str
77-
) -> _GroupID:
78-
"""
79-
Returns group_id of a product. Creates it if undefined
80-
"""
81-
warnings.warn(
82-
f"{__name__}.get_or_create_product_group uses aiopg which has been deprecated in this repo. Please use the asyncpg equivalent version instead"
83-
"See https://github.com/ITISFoundation/osparc-simcore/issues/4529",
84-
DeprecationWarning,
85-
stacklevel=1,
86-
)
87-
88-
async with connection.begin():
89-
group_id = await execute_get_or_create_product_group(
90-
connection, product_name=product_name
91-
)
92-
return _GroupID(group_id)

packages/postgres-database/src/simcore_postgres_database/utils_products_prices.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import NamedTuple, TypeAlias
33

44
import sqlalchemy as sa
5-
from aiopg.sa.connection import SAConnection
5+
from sqlalchemy.ext.asyncio import AsyncConnection
66

77
from .constants import QUANTIZE_EXP_ARG
88
from .models.products_prices import products_prices
@@ -17,9 +17,9 @@ class ProductPriceInfo(NamedTuple):
1717

1818

1919
async def get_product_latest_price_info_or_none(
20-
conn: SAConnection, product_name: str
20+
conn: AsyncConnection, product_name: str
2121
) -> ProductPriceInfo | None:
22-
"""None menans the product is not billable"""
22+
"""If the product is not billable, it returns None"""
2323
# newest price of a product
2424
result = await conn.execute(
2525
sa.select(
@@ -30,7 +30,7 @@ async def get_product_latest_price_info_or_none(
3030
.order_by(sa.desc(products_prices.c.valid_from))
3131
.limit(1)
3232
)
33-
row = await result.first()
33+
row = result.one_or_none()
3434

3535
if row and row.usd_per_credit is not None:
3636
assert row.min_payment_amount_usd is not None # nosec
@@ -43,27 +43,24 @@ async def get_product_latest_price_info_or_none(
4343
return None
4444

4545

46-
async def get_product_latest_stripe_info(
47-
conn: SAConnection, product_name: str
48-
) -> tuple[StripePriceID, StripeTaxRateID]:
46+
async def get_product_latest_stripe_info_or_none(
47+
conn: AsyncConnection, product_name: str
48+
) -> tuple[StripePriceID, StripeTaxRateID] | None:
4949
# Stripe info of a product for latest price
50-
row = await (
51-
await conn.execute(
52-
sa.select(
53-
products_prices.c.stripe_price_id,
54-
products_prices.c.stripe_tax_rate_id,
55-
)
56-
.where(products_prices.c.product_name == product_name)
57-
.order_by(sa.desc(products_prices.c.valid_from))
58-
.limit(1)
50+
result = await conn.execute(
51+
sa.select(
52+
products_prices.c.stripe_price_id,
53+
products_prices.c.stripe_tax_rate_id,
5954
)
60-
).fetchone()
61-
if row is None:
62-
msg = f"Required Stripe information missing from product {product_name=}"
63-
raise ValueError(msg)
64-
return (row.stripe_price_id, row.stripe_tax_rate_id)
55+
.where(products_prices.c.product_name == product_name)
56+
.order_by(sa.desc(products_prices.c.valid_from))
57+
.limit(1)
58+
)
59+
60+
row = result.one_or_none()
61+
return (row.stripe_price_id, row.stripe_tax_rate_id) if row else None
6562

6663

67-
async def is_payment_enabled(conn: SAConnection, product_name: str) -> bool:
64+
async def is_payment_enabled(conn: AsyncConnection, product_name: str) -> bool:
6865
p = await get_product_latest_price_info_or_none(conn, product_name=product_name)
6966
return bool(p) # zero or None is disabled

0 commit comments

Comments
 (0)