Skip to content

Commit 76d1fcd

Browse files
committed
feat: update project with changes from bettermarks licencing
1 parent 5b0e735 commit 76d1fcd

59 files changed

Lines changed: 6920 additions & 290 deletions

Some content is hidden

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

.github/workflows/pytest.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: "Python tests"
2+
3+
on:
4+
pull_request:
5+
branches: ["main"]
6+
7+
env:
8+
SEGMENT: loc00
9+
DB_HOST: localhost
10+
DB_PORT: 5432
11+
DB_USER: postgres
12+
DB_PASSWORD: postgres
13+
DB_NAME: test_licensing
14+
15+
jobs:
16+
pytest:
17+
runs-on: ubuntu-latest
18+
19+
services:
20+
postgres:
21+
image: postgres:17
22+
env:
23+
POSTGRES_USER: postgres
24+
POSTGRES_PASSWORD: postgres
25+
POSTGRES_DB: test_licensing
26+
ports: ["5432:5432"]
27+
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
28+
29+
steps:
30+
- uses: actions/checkout@v4
31+
with:
32+
fetch-depth: 0
33+
34+
- name: Install uv
35+
uses: astral-sh/setup-uv@v5
36+
with:
37+
# Install a specific version of uv.
38+
version: "0.6.3"
39+
40+
- name: Set up Python
41+
uses: actions/setup-python@v5
42+
with:
43+
python-version-file: ".python-version"
44+
45+
- run: uv sync --dev --frozen
46+
- run: uv run pytest --cov bm.ucm
47+
48+
- uses: actions/upload-artifact@v4
49+
if: always()
50+
with:
51+
name: coverage
52+
path: htmlcov

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,6 @@ target/
8080
profile_default/
8181
ipython_config.py
8282

83-
# pyenv
84-
.python-version
85-
8683
# pipenv
8784
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
8885
# However, in case of collaboration, if having platform-specific dependencies or dependencies

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12.9

k8s/Dockerfile.dev

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ RUN uv sync --extra export --frozen --no-editable --no-dev
3030
# INFO: final image only with virtual env
3131
FROM python:${python_version}-slim-bookworm
3232
ENV PYTHONUNBUFFERED 1
33+
ENV PATH="/venv/bin:$PATH"
3334
COPY --from=build /venv /venv
35+
COPY ./alembic.ini .
36+
COPY ./uvicorn_disable_logging.json .
37+
COPY ./src/services/licensing/data/sqlalchemy/migrations ./src/services/licensing/data/sqlalchemy/migrations
3438

3539
CMD ["uvicorn", "services.licensing.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--log-config", "uvicorn_disable_logging.json", "--timeout-keep-alive", "0"]

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ dependencies = [
3232

3333
[project.optional-dependencies]
3434
export = ["motor==3.6.0", "paramiko==3.5.0", "sshtunnel==0.4.0"]
35-
tests = [
35+
36+
[dependency-groups]
37+
dev = [
3638
"black==25.1.0",
3739
"bump2version==1.0.1",
3840
"commitizen==4.1.0",
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import functools
2+
import os
3+
4+
from fastapi import APIRouter
5+
from fastapi import status as http_status
6+
7+
from services.licensing import settings
8+
from services.licensing.business.service import LicensingService
9+
from services.licensing.exceptions import HTTPException
10+
from services.licensing.logging import LogLevel
11+
from services.licensing import __version__
12+
from services.licensing.settings import transaction_manager, repository
13+
14+
router = APIRouter()
15+
16+
17+
@router.get("/livez", status_code=http_status.HTTP_200_OK)
18+
async def get_livez() -> dict:
19+
return {
20+
"status": "OK",
21+
}
22+
23+
24+
@router.get("/status", status_code=http_status.HTTP_200_OK)
25+
async def get_status() -> dict:
26+
async with transaction_manager() as tm:
27+
if await LicensingService(repository(tm.session)).is_db_alive():
28+
return {"status": "OK"}
29+
raise HTTPException(
30+
status_code=http_status.HTTP_500_INTERNAL_SERVER_ERROR,
31+
message="Database not reachable",
32+
)
33+
34+
35+
@router.get("/version", status_code=http_status.HTTP_200_OK)
36+
async def get_version() -> dict:
37+
return {
38+
"debug": True if settings.log_level == LogLevel.DEBUG else False,
39+
"version": __version__,
40+
"git:sha": get_version_sha(),
41+
"segment": settings.segment,
42+
}
43+
44+
45+
@functools.cache
46+
def get_version_sha():
47+
if os.path.exists("./SHA.txt"):
48+
with open("./SHA.txt", "r") as f:
49+
sha = f.read().strip("\n")
50+
else:
51+
sha = "NO_GIT_SHA_FILE"
52+
return sha
Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
from fastapi import APIRouter
22

3-
from services.licensing.api.v1.endpoints import (
4-
admin,
5-
hierarchy,
6-
member,
7-
status,
8-
order,
9-
shop,
10-
)
3+
from services.licensing.api.v1.endpoints import admin, hierarchy, member, order
114

125
api_router = APIRouter()
13-
146
api_router.include_router(admin.router, prefix="/admin", tags=["Admin"])
15-
api_router.include_router(status.router, prefix="/status", tags=["Status"])
167
api_router.include_router(member.router, prefix="/member", tags=["Member"])
178
api_router.include_router(hierarchy.router, prefix="/hierarchy", tags=["Hierarchy"])
189
api_router.include_router(order.router, prefix="/order", tags=["Order"])

src/services/licensing/api/v1/endpoints/admin.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ async def create_license(
104104
message in case of an error
105105
"""
106106
async with transaction_manager() as tm:
107-
license_ = await LicensingService(repository(tm.session)).create_license(
107+
license_dict = await LicensingService(repository(tm.session)).create_license(
108108
hierarchy_provider_uri=data.hierarchy_provider_uri,
109109
manager_eid=data.manager_eid,
110110
product_eid=data.product_eid,
@@ -123,10 +123,10 @@ async def create_license(
123123
logger.info(
124124
"Successfully created license",
125125
is_trial=False,
126-
uuid=license_["uuid"],
127-
product=license_["product_eid"],
128-
owner_type=license_["owner_type"],
129-
nof_seats=license_["nof_seats"],
126+
uuid=license_dict["uuid"],
127+
product=license_dict["product_eid"],
128+
owner_type=license_dict["owner_type"],
129+
nof_seats=license_dict["nof_seats"],
130130
)
131131
except DuplicateEntryException:
132132
raise HTTPException(
@@ -137,7 +137,7 @@ async def create_license(
137137
"already exists"
138138
),
139139
)
140-
return LicenseCreatedSchema.parse_obj(license_) if license_ else None
140+
return LicenseCreatedSchema.parse_obj(license_dict)
141141

142142

143143
@router.get("/licenses/{license_uuid}", status_code=http_status.HTTP_200_OK)
@@ -180,3 +180,13 @@ async def update_license(
180180
payload["filter_restrictions"],
181181
**license_update.dict(exclude_unset=True)
182182
)
183+
184+
185+
@router.delete("/licenses/{license_uuid}", status_code=http_status.HTTP_200_OK)
186+
async def delete_license(
187+
license_uuid: str,
188+
token_data: Tuple[str, Dict[str, Any]] = Depends(authorize_with_admin_token),
189+
) -> None:
190+
async with transaction_manager() as tm:
191+
await LicensingService(repository(tm.session)).delete_license(license_uuid)
192+
await tm.commit()

src/services/licensing/api/v1/endpoints/hierarchy.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,53 @@ async def get_licenses_for_entity(
165165
payload["iss"], data.entity_type, data.entity_eid, hierarchies
166166
)
167167
]
168+
169+
170+
@router.post("/licenses/{license_id}", status_code=http_status.HTTP_200_OK)
171+
async def get_managed_license_by_uuid(
172+
license_id: str,
173+
_: HierarchiesSchema = Body(default_factory=HierarchiesSchema),
174+
token_data: Tuple[str, Dict[str, Any]] = Depends(authorize_with_hierarchies_token),
175+
) -> LicenseManagedSchema | None:
176+
"""
177+
Route for getting all the licenses in the hierarchy of a given user that they have
178+
created.
179+
180+
### Example Bearer Token structure
181+
```
182+
{
183+
"iss": "https://acc.bettermarks.com/ucm",
184+
"exp": 1701789570.99798,
185+
"sub": "12@EN_test",
186+
"iat": 1701788970.99799,
187+
"jti": "2b47ca46-28b9-4b8d-bd1c-a893acb9de29",
188+
"hashes": {
189+
"memberships": {
190+
"alg": "SHA256",
191+
"hash": "86f8b8313c183b3f9ee74b9c042ee440b894f690e9f6308de3da63fd4a6b8"
192+
}
193+
}
194+
}
195+
```
196+
\f
197+
:param license_id: License ID
198+
:param _: a list of hierarchies of a user (is not used right now)
199+
:param token_data: info gotten from hierarchies token
200+
:return: a JSON object
201+
"""
202+
_, payload = token_data
203+
204+
async with transaction_manager() as tm:
205+
license_item = await LicensingService(
206+
repository(tm.session)
207+
).get_managed_licenses_by_id(
208+
license_id,
209+
payload["iss"],
210+
payload["sub"],
211+
)
212+
item = (
213+
LicenseManagedSchema.parse_obj(asdict(license_item))
214+
if license_item
215+
else None
216+
)
217+
return item

src/services/licensing/api/v1/endpoints/member.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,25 @@ async def get_accessible_products(
8686
)
8787

8888

89-
@router.post("/licenses/trial", status_code=http_status.HTTP_201_CREATED)
89+
@router.post(
90+
"/licenses/trial",
91+
status_code=http_status.HTTP_201_CREATED,
92+
responses={
93+
409: {
94+
"description": "Duplicate Error",
95+
"content": {
96+
"application/json": {
97+
"example": {
98+
"detail": (
99+
"Trial license creation failed:"
100+
" A trial license for this entity already exists"
101+
),
102+
},
103+
},
104+
},
105+
},
106+
},
107+
)
90108
async def create_trial_license(
91109
data: LicenseTrialSchema,
92110
token_data: Tuple[str, Dict[str, Any]] = Depends(authorize_with_memberships_token),
@@ -112,6 +130,11 @@ async def create_trial_license(
112130

113131
# Do the actual check, if the owner EID is part of the memberships.
114132
if Entity(type_=data.owner_type, eid=data.owner_eid) not in memberships:
133+
logger.warn(
134+
"license owner does not match any users membership",
135+
owner_type=data.owner_type,
136+
owner_eid=data.owner_eid,
137+
)
115138
raise HTTPException(
116139
status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY,
117140
message=(
@@ -140,7 +163,7 @@ async def create_trial_license(
140163
"A trial license for this entity already exists"
141164
),
142165
)
143-
license_ = await LicensingService(repository(tm.session)).create_license(
166+
license_dict = await LicensingService(repository(tm.session)).create_license(
144167
hierarchy_provider_uri=payload["iss"],
145168
manager_eid=payload["sub"],
146169
product_eid=data.product_eid,
@@ -158,10 +181,10 @@ async def create_trial_license(
158181
logger.info(
159182
"Successfully created license",
160183
is_trial=True,
161-
uuid=license_["uuid"],
162-
product=license_["product_eid"],
163-
owner_type=license_["owner_type"],
164-
nof_seats=license_["nof_seats"],
184+
uuid=license_dict["uuid"],
185+
product=license_dict["product_eid"],
186+
owner_type=license_dict["owner_type"],
187+
nof_seats=license_dict["nof_seats"],
165188
)
166189
except DuplicateEntryException:
167190
raise HTTPException(
@@ -171,7 +194,7 @@ async def create_trial_license(
171194
"A license with these properties already exists"
172195
),
173196
)
174-
return license_
197+
return LicenseCreatedSchema.parse_obj(license_dict)
175198

176199

177200
@router.post("/licenses", status_code=http_status.HTTP_200_OK)
@@ -187,9 +210,9 @@ async def get_available_licenses(
187210
### Example Bearer Token structure
188211
```
189212
{
190-
"iss": "https://your-domain.com/ucm",
213+
"iss": "https://acc.bettermarks.com/ucm",
191214
"exp": 1701799583.393268,
192-
"sub": "3@DE_tesyt",
215+
"sub": "3@DE_bettermarks",
193216
"iat": 1701798983.393283,
194217
"jti": "77ab5b01-83a0-44f3-a086-d25162aae84e",
195218
"hashes": {

0 commit comments

Comments
 (0)