Skip to content

Commit

Permalink
✨ Announcements entrypoint at the web-api (⚠️ devops) (#4487)
Browse files Browse the repository at this point in the history
Co-authored-by: Odei Maiz <[email protected]>
  • Loading branch information
pcrespov and odeimaiz authored Jul 12, 2023
1 parent eb5e00b commit 136ca89
Show file tree
Hide file tree
Showing 26 changed files with 673 additions and 100 deletions.
1 change: 1 addition & 0 deletions .env-devel
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ TRAEFIK_SIMCORE_ZONE=internal_simcore_stack
# NOTE: WEBSERVER_SESSION_SECRET_KEY = $(python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key())")
PROJECTS_MAX_COPY_SIZE_BYTES=30Gib
PROJECTS_MAX_NUM_RUNNING_DYNAMIC_NODES=5
WEBSERVER_ANNOUNCEMENTS=1
WEBSERVER_DEV_FEATURES_ENABLED=0
WEBSERVER_HOST=webserver
WEBSERVER_LOGIN_REGISTRATION_CONFIRMATION_REQUIRED=0
Expand Down
2 changes: 1 addition & 1 deletion .env-wb-db-event-listener
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Docs plugins config of services/web/server/src/simcore_service_webserver/application_settings.py
#


WEBSERVER_ANNOUNCEMENTS=0
WEBSERVER_ACTIVITY=null
WEBSERVER_CATALOG=null
WEBSERVER_NOTIFICATIONS=0
Expand Down
1 change: 1 addition & 0 deletions .env-wb-garbage-collector
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#


WEBSERVER_ANNOUNCEMENTS=0
WEBSERVER_ACTIVITY=null
WEBSERVER_CATALOG=null
WEBSERVER_NOTIFICATIONS=0
Expand Down
79 changes: 79 additions & 0 deletions api/specs/webserver/openapi-announcements.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
paths:
/announcements:
get:
tags:
- announcements
summary: List Announcements
operationId: list_announcements
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Envelope_list_simcore_service_webserver.announcements._models.Announcement__'
components:
schemas:
Announcement:
properties:
id:
type: string
title: Id
products:
items:
type: string
enum:
- osparc
- s4l
- s4llite
- tis
type: array
title: Products
start:
type: string
format: date-time
title: Start
end:
type: string
format: date-time
title: End
title:
type: string
title: Title
description:
type: string
title: Description
link:
type: string
title: Link
widgets:
items:
type: string
enum:
- login
- ribbon
- user-menu
type: array
title: Widgets
type: object
required:
- id
- products
- start
- end
- title
- description
- link
- widgets
title: Announcement
Envelope_list_simcore_service_webserver.announcements._models.Announcement__:
properties:
data:
items:
$ref: '#/components/schemas/Announcement'
type: array
title: Data
error:
title: Error
type: object
title: Envelope[list[simcore_service_webserver.announcements._models.Announcement]]
22 changes: 5 additions & 17 deletions api/specs/webserver/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: "osparc-simcore web API"
version: 0.24.0
version: 0.25.0
description: "API designed for the front-end app"
contact:
name: IT'IS Foundation
Expand All @@ -23,28 +23,16 @@ servers:
enum:
- v0
default: v0
tags:
- name: activity
- name: admin
- name: authentication
- name: catalog
- name: cluster
- name: configuration
- name: maintenance
- name: nih-sparc
- name: project
- name: publication
- name: repository
- name: storage
- name: tag
- name: tasks
- name: user

paths:
# ADMIN -------------
/email:test:
$ref: "./openapi-admin.yaml#/paths/~1email:test"

# ANNOUNCEMENTS ---------------------------------------------------------
/announcements:
$ref: "./openapi-announcements.yaml#/paths/~1announcements"

# DIAGNOSTICS ---------------------------------------------------------
/:
$ref: "./openapi-diagnostics.yaml#/paths/~1"
Expand Down
36 changes: 36 additions & 0 deletions api/specs/webserver/scripts/openapi_announcements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
""" Helper script to generate OAS automatically
"""

# pylint: disable=redefined-outer-name
# pylint: disable=unused-argument
# pylint: disable=unused-variable
# pylint: disable=too-many-arguments


from enum import Enum

from fastapi import FastAPI
from models_library.generics import Envelope
from simcore_service_webserver.announcements._handlers import Announcement

app = FastAPI(redoc_url=None)

TAGS: list[str | Enum] = [
"announcements",
]


@app.get(
"/announcements",
response_model=Envelope[list[Announcement]],
tags=TAGS,
operation_id="list_announcements",
)
async def list_announcements():
...


if __name__ == "__main__":
from _common import CURRENT_DIR, create_openapi_specs

create_openapi_specs(app, CURRENT_DIR.parent / "openapi-announcements.yaml")
2 changes: 1 addition & 1 deletion api/specs/webserver/scripts/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Extra reqs, besides webserver's

fastapi
fastapi<0.100
jsonref
1 change: 1 addition & 0 deletions packages/settings-library/src/settings_library/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class RedisDatabase(int, Enum):
VALIDATION_CODES = 2
SCHEDULED_MAINTENANCE = 3
USER_NOTIFICATIONS = 4
ANNOUNCEMENTS = 5


class RedisSettings(BaseCustomSettings):
Expand Down
2 changes: 1 addition & 1 deletion services/docker-compose-ops.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ services:
image: rediscommander/redis-commander:latest
init: true
environment:
- REDIS_HOSTS=resources:${REDIS_HOST}:${REDIS_PORT}:0,locks:${REDIS_HOST}:${REDIS_PORT}:1,validation_codes:${REDIS_HOST}:${REDIS_PORT}:2,scheduled_maintenance:${REDIS_HOST}:${REDIS_PORT}:3,user_notifications:${REDIS_HOST}:${REDIS_PORT}:4
- REDIS_HOSTS=resources:${REDIS_HOST}:${REDIS_PORT}:0,locks:${REDIS_HOST}:${REDIS_PORT}:1,validation_codes:${REDIS_HOST}:${REDIS_PORT}:2,scheduled_maintenance:${REDIS_HOST}:${REDIS_PORT}:3,user_notifications:${REDIS_HOST}:${REDIS_PORT}:4,announcements:${REDIS_HOST}:${REDIS_PORT}:5
# If you add/remove a db, do not forget to update the --databases entry in the docker-compose.yml
ports:
- "18081:8081"
Expand Down
2 changes: 1 addition & 1 deletion services/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ services:
"--loglevel",
"verbose",
"--databases",
"5",
"6",
"--appendonly",
"yes"
]
Expand Down
2 changes: 1 addition & 1 deletion services/web/server/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.24.0
0.25.0
2 changes: 1 addition & 1 deletion services/web/server/setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.24.0
current_version = 0.25.0
commit = True
message = services/webserver api version: {current_version} → {new_version}
tag = False
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
""" Service layer with announcement plugin business logic
"""
from aiohttp import web

from . import _redis
from ._models import Announcement


async def list_announcements(
app: web.Application, *, product_name: str
) -> list[Announcement]:
return await _redis.list_announcements(
app, include_product=product_name, exclude_expired=True
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
""" Controler layer to expose to the web rest API
"""
from aiohttp import web

from .._meta import api_version_prefix
from ..products.plugin import get_product_name
from ..utils_aiohttp import envelope_json_response
from . import _api
from ._models import Announcement

routes = web.RouteTableDef()


@routes.get(f"/{api_version_prefix}/announcements", name="list_announcements")
async def list_announcements(request: web.Request) -> web.Response:
"""Returns non-expired announcements for current product"""
product_name = get_product_name(request)
announcements: list[Announcement] = await _api.list_announcements(
request.app, product_name=product_name
)

return envelope_json_response(announcements)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from datetime import datetime
from typing import Any, ClassVar, Literal

import arrow
from pydantic import BaseModel, validator


# NOTE: this model is used for BOTH
# - parse+validate from redis
# - schema in the response
class Announcement(BaseModel):
id: str # noqa: A003
products: list[str]
start: datetime
end: datetime
title: str
description: str
link: str
widgets: list[Literal["login", "ribbon", "user-menu"]]

@validator("end")
@classmethod
def check_start_before_end(cls, v, values):
if start := values.get("start"):
end = v
if end <= start:
msg = f"end={end!r} is not before start={start!r}"
raise ValueError(msg)
return v

def expired(self) -> bool:
return self.end <= arrow.utcnow().datetime

class Config:
schema_extra: ClassVar[dict[str, Any]] = {
"examples": [
{
"id": "Student_Competition_2023",
"products": ["s4llite"],
"start": "2023-06-22T15:00:00.000Z",
"end": "2023-11-01T02:00:00.000Z",
"title": "Student Competition 2023",
"description": "For more information click <a href='https://zmt.swiss/news-and-events/news/sim4life/s4llite-student-competition-2023/' style='color: white' target='_blank'>here</a>",
"link": "https://zmt.swiss/news-and-events/news/sim4life/s4llite-student-competition-2023/",
"widgets": ["login", "ribbon"],
},
{
"id": "TIP_v2",
"products": ["tis"],
"start": "2023-07-22T15:00:00.000Z",
"end": "2023-08-01T02:00:00.000Z",
"title": "TIP v2",
"description": "For more information click <a href='https://itis.swiss/tools-and-systems/ti-planning/' style='color: white' target='_blank'>here</a>",
"link": "https://itis.swiss/tools-and-systems/ti-planning/",
"widgets": ["login", "ribbon", "user-menu"],
},
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
""" Repository layer using redis
"""

import logging

import redis.asyncio as aioredis
from aiohttp import web
from pydantic import ValidationError

from ..redis import get_redis_announcements_client
from ._models import Announcement

_logger = logging.getLogger(__name__)

_PUBLIC_ANNOUNCEMENTS_REDIS_KEY = "public"
#
# At this moment `announcements` are manually stored in redis db 6 w/o guarantees
# Here we validate them and log a big-fat error if there is something wrong
# Invalid announcements are not passed to the front-end
#
_MSG_REDIS_ERROR = f"Invalid announcements[{_PUBLIC_ANNOUNCEMENTS_REDIS_KEY}] in redis. Please check values introduced *by hand*. Skipping"


async def list_announcements(
app: web.Application, *, include_product: str, exclude_expired: bool
) -> list[Announcement]:
# get-all
redis_client: aioredis.Redis = get_redis_announcements_client(app)
items: list[str] = await redis_client.lrange(_PUBLIC_ANNOUNCEMENTS_REDIS_KEY, 0, -1)

# validate
announcements = []
for i, item in enumerate(items):
try:
model = Announcement.parse_raw(item)
# filters
if include_product not in model.products:
continue
if exclude_expired and model.expired():
continue
# OK
announcements.append(model)
except ValidationError: # noqa: PERF203
_logger.exception(
"%s. Check item[%d]=%s",
_MSG_REDIS_ERROR,
i,
item,
)

return announcements
Loading

0 comments on commit 136ca89

Please sign in to comment.