Skip to content

Commit 6f7a4e1

Browse files
authored
✨ Is91/adds short name for Alphanumeric Sender ID (⚠️ devops) (ITISFoundation#3292)
1 parent 37d7e09 commit 6f7a4e1

File tree

9 files changed

+140
-9
lines changed

9 files changed

+140
-9
lines changed

packages/models-library/src/models_library/basic_regex.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,11 @@
4343
# Datcore file ID
4444
DATCORE_FILE_ID_RE = rf"^N:package:{UUID_RE_BASE}$"
4545
DATCORE_DATASET_NAME_RE = rf"^N:dataset:{UUID_RE_BASE}$"
46+
47+
48+
TWILIO_ALPHANUMERIC_SENDER_ID_RE = r"(?!^\d+$)^[a-zA-Z0-9\s]{2,11}$"
49+
# Alphanumeric Sender IDs may be up to 11 characters long.
50+
# Accepted characters include both upper- and lower-case Ascii letters,
51+
# the digits 0 through 9, and the space character.
52+
# They may not be only numerals.
53+
# SEE # https://www.twilio.com/docs/glossary/what-alphanumeric-sender-id

packages/models-library/tests/test_basic_regex.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from models_library.basic_regex import (
1313
DATE_RE,
1414
PUBLIC_VARIABLE_NAME_RE,
15+
TWILIO_ALPHANUMERIC_SENDER_ID_RE,
1516
UUID_RE,
1617
VERSION_RE,
1718
)
@@ -148,3 +149,21 @@ def test_variable_names_regex(string_under_test, expected_match):
148149
assert variable_re.match(string_under_test)
149150
else:
150151
assert not variable_re.match(string_under_test)
152+
153+
154+
@pytest.mark.parametrize(
155+
"sample, expected",
156+
[
157+
("0123456789a", VALID),
158+
("A12b4567 9a", VALID),
159+
("01234567890", INVALID), # they may NOT be only numerals.
160+
("0123456789a1", INVALID), # may be up to 11 characters long
161+
("0-23456789a", INVALID), # '-' is invalid
162+
],
163+
)
164+
def test_TWILIO_ALPHANUMERIC_SENDER_ID_RE(sample, expected):
165+
# Alphanumeric Sender IDs may be up to 11 characters long.
166+
# Accepted characters include both upper- and lower-case Ascii letters,
167+
# the digits 0 through 9, and the space character.
168+
169+
assert_match_and_get_capture(TWILIO_ALPHANUMERIC_SENDER_ID_RE, sample, expected)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Adds short_name colm to products
2+
3+
Revision ID: 9d477e20d06e
4+
Revises: e5f3167d7277
5+
Create Date: 2022-08-29 17:53:57.958145+00:00
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "9d477e20d06e"
13+
down_revision = "e5f3167d7277"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.add_column(
21+
"products",
22+
sa.Column("short_name", sa.String(), server_default="osparc", nullable=False),
23+
)
24+
# ### end Alembic commands ###
25+
26+
27+
def downgrade():
28+
# ### commands auto generated by Alembic - please adjust! ###
29+
op.drop_column("products", "short_name")
30+
# ### end Alembic commands ###

packages/postgres-database/src/simcore_postgres_database/models/products.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727
server_default="o²S²PARC",
2828
doc="Human readable name of the product or display name",
2929
),
30+
sa.Column(
31+
"short_name",
32+
sa.String,
33+
nullable=False,
34+
server_default="osparc",
35+
doc="Alphanumeric name up to 11 characters long with characters "
36+
"include both upper- and lower-case Ascii letters, the digits 0 through 9, "
37+
"and the space character. They may not be only numerals.",
38+
),
3039
sa.Column(
3140
"host_regex",
3241
sa.String,

packages/postgres-database/tests/test_models_products.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,12 @@ async def test_jinja2_templates_table(
100100
{
101101
"name": "s4l",
102102
"host_regex": r"(^s4l[\.-])|(^sim4life\.)",
103+
"short_name": "s4l web",
103104
"registration_email_template": registration_email_template,
104105
},
105106
{
106107
"name": "tis",
108+
"short_name": "TIP",
107109
"host_regex": r"(^ti.[\.-])|(^ti-solution\.)",
108110
},
109111
]:
@@ -114,13 +116,15 @@ async def test_jinja2_templates_table(
114116

115117
# prints those products having customized templates
116118
j = products.join(jinja2_templates)
117-
stmt = sa.select([products.c.name, jinja2_templates.c.name]).select_from(j)
119+
stmt = sa.select(
120+
[products.c.name, jinja2_templates.c.name, products.c.short_name]
121+
).select_from(j)
118122

119123
result: ResultProxy = await conn.execute(stmt)
120124
assert result.rowcount == 2
121125
assert await result.fetchall() == [
122-
("osparc", "registration_email.jinja2"),
123-
("s4l", "registration_email.jinja2"),
126+
("s4l", "registration_email.jinja2", "s4l web"),
127+
("osparc", "registration_email.jinja2", "osparc"),
124128
]
125129

126130
assert (

services/web/server/src/simcore_service_webserver/login/_2fa.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,21 +79,21 @@ async def send_sms_code(
7979
code: str,
8080
twilo_auth: TwilioSettings,
8181
twilio_messaging_sid: str,
82-
product_display_name: str,
82+
twilio_alpha_numeric_sender: str,
8383
user_name: str = "user",
8484
):
8585
def sender():
8686
log.info(
8787
"Sending sms code to %s from product %s",
8888
f"{phone_number=}",
89-
product_display_name,
89+
twilio_alpha_numeric_sender,
9090
)
9191
# SEE https://www.twilio.com/docs/sms/quickstart/python
9292
#
9393
client = Client(twilo_auth.TWILIO_ACCOUNT_SID, twilo_auth.TWILIO_AUTH_TOKEN)
9494
message = client.messages.create(
9595
messaging_service_sid=twilio_messaging_sid,
96-
from_=product_display_name,
96+
from_=twilio_alpha_numeric_sender,
9797
to=phone_number,
9898
body=f"Dear {user_name[:20].capitalize().strip()}, your verification code is {code}",
9999
)

services/web/server/src/simcore_service_webserver/login/handlers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ async def register_phone(request: web.Request):
179179
code=code,
180180
twilo_auth=settings.LOGIN_TWILIO,
181181
twilio_messaging_sid=product.twilio_messaging_sid,
182-
product_display_name=product.display_name,
182+
twilio_alpha_numeric_sender=product.twilio_alpha_numeric_sender_id,
183183
user_name=_get_user_name(email),
184184
)
185185

@@ -311,7 +311,7 @@ async def login(request: web.Request):
311311
code=code,
312312
twilo_auth=settings.LOGIN_TWILIO,
313313
twilio_messaging_sid=product.twilio_messaging_sid,
314-
product_display_name=product.display_name,
314+
twilio_alpha_numeric_sender=product.twilio_alpha_numeric_sender_id,
315315
user_name=user["name"],
316316
)
317317

services/web/server/src/simcore_service_webserver/products_db.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import logging
2+
import string
23
from typing import Any, AsyncIterator, Optional, Pattern
34

45
import sqlalchemy as sa
56
from aiopg.sa.engine import Engine
67
from aiopg.sa.result import ResultProxy, RowProxy
7-
from models_library.basic_regex import PUBLIC_VARIABLE_NAME_RE
8+
from models_library.basic_regex import (
9+
PUBLIC_VARIABLE_NAME_RE,
10+
TWILIO_ALPHANUMERIC_SENDER_ID_RE,
11+
)
812
from models_library.utils.change_case import snake_to_camel
913
from pydantic import BaseModel, Field, HttpUrl, validator
1014
from simcore_postgres_database.models.products import jinja2_templates
@@ -15,6 +19,7 @@
1519

1620
log = logging.getLogger(__name__)
1721

22+
1823
#
1924
# MODEL
2025
#
@@ -27,6 +32,9 @@ class Product(BaseModel):
2732

2833
name: str = Field(regex=PUBLIC_VARIABLE_NAME_RE)
2934
display_name: str
35+
short_name: Optional[str] = Field(
36+
None, regex=TWILIO_ALPHANUMERIC_SENDER_ID_RE, min_length=2, max_length=11
37+
)
3038
host_regex: Pattern
3139

3240
# EMAILS/PHONE
@@ -53,6 +61,23 @@ class Product(BaseModel):
5361

5462
class Config:
5563
orm_mode = True
64+
schema_extra = {
65+
"examples": [
66+
{
67+
# fake mandatory
68+
"name": "osparc",
69+
"host_regex": r"([\.-]{0,1}osparc[\.-])",
70+
"twilio_messaging_sid": "1" * 34,
71+
"registration_email_template": "osparc_registration_email",
72+
# defaults from sqlalchemy table
73+
**{
74+
c.name: c.server_default.arg
75+
for c in products.columns
76+
if c.server_default and isinstance(c.server_default.arg, str)
77+
},
78+
},
79+
]
80+
}
5681

5782
@validator("name", pre=True, always=True)
5883
@classmethod
@@ -63,6 +88,10 @@ def validate_name(cls, v):
6388
)
6489
return v
6590

91+
@property
92+
def twilio_alpha_numeric_sender_id(self) -> str:
93+
return self.short_name or self.display_name.replace(string.punctuation, "")[:11]
94+
6695
def to_statics(self) -> dict[str, Any]:
6796
"""
6897
Selects **public** fields from product's info
@@ -89,6 +118,7 @@ def to_statics(self) -> dict[str, Any]:
89118
}
90119

91120
def get_template_name_for(self, filename: str) -> Optional[str]:
121+
"""Checks for field marked with 'x_template_name' that fits the argument"""
92122
template_name = filename.removesuffix(".jinja2")
93123
for field in self.__fields__.values():
94124
if field.field_info.extra.get("x_template_name") == template_name:
@@ -100,6 +130,7 @@ def get_template_name_for(self, filename: str) -> Optional[str]:
100130
# REPOSITORY
101131
#
102132

133+
# NOTE: This also asserts that all model fields are in sync with sqlalchemy columns
103134
_include_cols = [products.columns[f] for f in Product.__fields__]
104135

105136

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# pylint: disable=redefined-outer-name
2+
# pylint: disable=unused-argument
3+
# pylint: disable=unused-variable
4+
5+
6+
from typing import Any
7+
8+
import pytest
9+
from pydantic import BaseModel
10+
from servicelib.json_serialization import json_dumps
11+
from simcore_service_webserver.products_db import Product
12+
13+
14+
@pytest.mark.parametrize(
15+
"model_cls",
16+
(Product,),
17+
)
18+
def test_product_examples(
19+
model_cls: type[BaseModel], model_cls_examples: dict[str, dict[str, Any]]
20+
):
21+
for name, example in model_cls_examples.items():
22+
print(name, ":", json_dumps(example, indent=1))
23+
model_instance = model_cls(**example)
24+
assert model_instance, f"Failed with {name}"
25+
26+
if isinstance(model_cls, Product):
27+
assert model_instance.to_statics()
28+
29+
if "registration_email_template" in example:
30+
assert model_instance.get_template_name_for("registration_email.jinja2")

0 commit comments

Comments
 (0)