Skip to content

Commit

Permalink
Release 1.1.2
Browse files Browse the repository at this point in the history
add cache-to and cache-from options for docker buildx build
Add LMS
  • Loading branch information
depocoder authored Oct 17, 2024
2 parents 0542c90 + 46cbae4 commit 1895e44
Show file tree
Hide file tree
Showing 14 changed files with 421 additions and 49 deletions.
14 changes: 12 additions & 2 deletions .github/workflows/build-and-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ env:
SSH_USER: ${{ secrets.SSH_USER }}
SSH_PORT: ${{ secrets.SSH_PORT }}

S3_ENDPOINT_URL: ${{ secrets.S3_ENDPOINT_URL }}
S3_REGION: ${{ secrets.S3_REGION }}
BUCKET_NAME: ${{ secrets.BUCKET_NAME }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ACCESS_KEY: ${{ secrets.ACCESS_KEY }}


jobs:
build:
Expand All @@ -38,13 +44,17 @@ jobs:
run: |
docker buildx build --platform linux/amd64,linux/arm64 --push ./backend \
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:latest \
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:$IMAGE_TAG
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:$IMAGE_TAG \
--cache-to type=s3,endpoint_url=$S3_ENDPOINT_URL,region=$S3_REGION,bucket=$BUCKET_NAME,name=calendar_backend,access_key_id=$ACCESS_KEY,secret_access_key=$SECRET_KEY \
--cache-from type=s3,endpoint_url=$S3_ENDPOINT_URL,region=$S3_REGION,bucket=$BUCKET_NAME,name=calendar_backend,access_key_id=$ACCESS_KEY,secret_access_key=$SECRET_KEY
- name: Build & Publish frontend to Github Container registry
run: |
docker buildx build --platform linux/amd64,linux/arm64 --push ./frontend \
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:latest \
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:$IMAGE_TAG
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:$IMAGE_TAG \
--cache-to type=s3,endpoint_url=$S3_ENDPOINT_URL,region=$S3_REGION,bucket=$BUCKET_NAME,name=calendar_frontend,access_key_id=$ACCESS_KEY,secret_access_key=$SECRET_KEY \
--cache-from type=s3,endpoint_url=$S3_ENDPOINT_URL,region=$S3_REGION,bucket=$BUCKET_NAME,name=calendar_frontend,access_key_id=$ACCESS_KEY,secret_access_key=$SECRET_KEY
deploy:
runs-on: ubuntu-latest
Expand Down
16 changes: 12 additions & 4 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ env:
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
IMAGE_TAG: ${{ github.sha }}

S3_ENDPOINT_URL: ${{ secrets.S3_ENDPOINT_URL }}
S3_REGION: ${{ secrets.S3_REGION }}
BUCKET_NAME: ${{ secrets.BUCKET_NAME }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ACCESS_KEY: ${{ secrets.ACCESS_KEY }}

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -33,11 +39,13 @@ jobs:
- name: Build & Publish backend to Github Container registry
run: |
docker buildx build --platform linux/amd64,linux/arm64 --push ./backend \
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:latest \
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:$IMAGE_TAG
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_backend:$IMAGE_TAG \
--cache-to type=s3,endpoint_url=$S3_ENDPOINT_URL,region=$S3_REGION,bucket=$BUCKET_NAME,name=calendar_backend,access_key_id=$ACCESS_KEY,secret_access_key=$SECRET_KEY \
--cache-from type=s3,endpoint_url=$S3_ENDPOINT_URL,region=$S3_REGION,bucket=$BUCKET_NAME,name=calendar_backend,access_key_id=$ACCESS_KEY,secret_access_key=$SECRET_KEY
- name: Build & Publish frontend to Github Container registry
run: |
docker buildx build --platform linux/amd64,linux/arm64 --push ./frontend \
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:latest \
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:$IMAGE_TAG
--tag $REGISTRY_URL/$REGISTRY_USERNAME/yet_another_calendar_frontend:$IMAGE_TAG \
--cache-to type=s3,endpoint_url=$S3_ENDPOINT_URL,region=$S3_REGION,bucket=$BUCKET_NAME,name=calendar_frontend,access_key_id=$ACCESS_KEY,secret_access_key=$SECRET_KEY \
--cache-from type=s3,endpoint_url=$S3_ENDPOINT_URL,region=$S3_REGION,bucket=$BUCKET_NAME,name=calendar_frontend,access_key_id=$ACCESS_KEY,secret_access_key=$SECRET_KEY
8 changes: 8 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
This project was created to replace Modeus/Netology calendars


## Features

* Export to .ics calendar format
* Your timezone support (default Moscow)
* Modeus + Netology integration
* LMS support (not required to use)
* Redis cache

## Getting started

1. Install [poetry](https://python-poetry.org/docs/#installing-with-the-official-installer)
Expand Down
47 changes: 31 additions & 16 deletions backend/yet_another_calendar/web/api/bulk/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

from yet_another_calendar.settings import settings
from ..netology import views as netology_views
from ..lms import views as lms_views
from ..modeus import views as modeus_views
from ..modeus import schema as modeus_schema
from ..lms import schema as lms_schema
from ..netology import schema as netology_schema
from . import schema

Expand All @@ -22,17 +24,16 @@

def create_ics_event(title: str, starts_at: datetime.datetime, ends_at: datetime.datetime,
lesson_id: Any, description: Optional[str] = None,
webinar_url: Optional[str] = None) -> icalendar.Event:
url: Optional[str] = None) -> icalendar.Event:
event = icalendar.Event()
dt_now = datetime.datetime.now()
event.add('summary', title)
event.add('location', webinar_url)
event.add('location', url if url else 'unknown location')
event.add('dtstart', starts_at)
event.add('dtend', ends_at)
event.add('dtstamp', dt_now)
event.add('uid', lesson_id)
if description:
event.add('description', description)
event.add('DESCRIPTION', description)
return event


Expand All @@ -44,30 +45,36 @@ def export_to_ics(calendar: schema.CalendarResponse) -> Iterable[bytes]:
for netology_lesson in calendar.netology.webinars:
if not netology_lesson.starts_at or not netology_lesson.ends_at:
continue
event = create_ics_event(title=f"Netology: {netology_lesson.title}", starts_at=netology_lesson.starts_at,
event = create_ics_event(title=f"Netology: {netology_lesson.block_title}", starts_at=netology_lesson.starts_at,
ends_at=netology_lesson.ends_at, lesson_id=netology_lesson.id,
webinar_url=netology_lesson.webinar_url)
description=netology_lesson.title,
url=netology_lesson.webinar_url)
ics_calendar.add_component(event)
for modeus_lesson in calendar.modeus:
event = create_ics_event(title=f"Modeus: {modeus_lesson.name}",
starts_at=modeus_lesson.start_time, ends_at=modeus_lesson.end_time,
lesson_id=modeus_lesson.id,
description=modeus_lesson.description)
for modeus_lesson in calendar.utmn.modeus_events:
event = create_ics_event(title=f"Modeus: {modeus_lesson.course_name}", starts_at=modeus_lesson.start_time,
ends_at=modeus_lesson.end_time, lesson_id=modeus_lesson.id,
description=modeus_lesson.name)
ics_calendar.add_component(event)
for lms_event in calendar.utmn.lms_events:
dt_start = lms_event.dt_end - datetime.timedelta(hours=2)
event = create_ics_event(title=f"LMS: {lms_event.course_name}", starts_at=dt_start, ends_at=lms_event.dt_end,
lesson_id=lms_event.id, description=lms_event.name, url=lms_event.url)
ics_calendar.add_component(event)
yield ics_calendar.to_ical()


async def refresh_events(
body: modeus_schema.ModeusEventsBody,
lms_user: lms_schema.User,
jwt_token: str,
calendar_id: int,
cookies: netology_schema.NetologyCookies,
timezone: str,
) -> schema.RefreshedCalendarResponse:
"""Clear events cache."""
cached_json = await get_cached_calendar(body, jwt_token, calendar_id, cookies, timezone)
cached_json = await get_cached_calendar(body, lms_user, jwt_token, calendar_id, cookies, timezone)
cached_calendar = schema.CalendarResponse.model_validate(cached_json)
calendar = await get_calendar(body, jwt_token, calendar_id, cookies, timezone)
calendar = await get_calendar(body, lms_user, jwt_token, calendar_id, cookies, timezone)
changed = cached_calendar.get_hash() != calendar.get_hash()
try:
cache_key = default_key_builder(get_cached_calendar, args=(body, jwt_token, calendar_id, cookies), kwargs={})
Expand All @@ -87,6 +94,7 @@ async def refresh_events(

async def get_calendar(
body: modeus_schema.ModeusEventsBody,
lms_user: lms_schema.User,
jwt_token: str,
calendar_id: int,
cookies: netology_schema.NetologyCookies,
Expand All @@ -96,21 +104,28 @@ async def get_calendar(
tz = pytz.timezone(timezone)
except pytz.exceptions.UnknownTimeZoneError:
raise HTTPException(detail="Wrong timezone", status_code=status.HTTP_400_BAD_REQUEST) from None

lms_response = None
async with asyncio.TaskGroup() as tg:
netology_response = tg.create_task(netology_views.get_calendar(body, calendar_id, cookies))
modeus_response = tg.create_task(modeus_views.get_calendar(body, jwt_token))
if lms_user.is_enabled:
lms_response = tg.create_task(lms_views.get_events(lms_user, body))
lms_events = lms_response.result() if lms_response else []
return schema.CalendarResponse.model_validate(
{"netology": netology_response.result(), "modeus": modeus_response.result()},
{"netology": netology_response.result(), "utmn": {
"modeus_events": modeus_response.result(),
"lms_events": lms_events,
}},
).change_timezone(tz)


@cache(expire=settings.redis_events_time_live)
async def get_cached_calendar(
body: modeus_schema.ModeusEventsBody,
lms_user: lms_schema.User,
jwt_token: str,
calendar_id: int,
cookies: netology_schema.NetologyCookies,
timezone: str,
) -> schema.CalendarResponse:
return await get_calendar(body, jwt_token, calendar_id, cookies, timezone)
return await get_calendar(body, lms_user, jwt_token, calendar_id, cookies, timezone)
23 changes: 18 additions & 5 deletions backend/yet_another_calendar/web/api/bulk/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@
from pydantic import BaseModel, Field

from ..modeus import schema as modeus_schema
from ..lms import schema as lms_schema
from ..netology import schema as netology_schema

def now_dt_utc() -> datetime.datetime:
return datetime.datetime.now(tz=datetime.timezone.utc)

class UtmnResponse(BaseModel):
modeus_events: list[modeus_schema.FullEvent]
lms_events: list[lms_schema.ModuleResponse]


class BulkResponse(BaseModel):
netology: netology_schema.SerializedEvents
modeus: list[modeus_schema.FullEvent]
utmn: UtmnResponse

def change_timezone(self, timezone: datetime.tzinfo) -> Self:
for homework in self.netology.homework:
Expand All @@ -20,13 +28,18 @@ def change_timezone(self, timezone: datetime.tzinfo) -> Self:
webinar.starts_at = webinar.validate_starts_at(webinar.starts_at, timezone)
webinar.ends_at = webinar.validate_ends_at(webinar.ends_at, timezone)

for event in self.modeus:
event.start_time = event.validate_starts_at(event.start_time, timezone)
event.end_time = event.validate_end_time(event.end_time, timezone)
for modeus_event in self.utmn.modeus_events:
modeus_event.start_time = modeus_event.validate_starts_at(modeus_event.start_time, timezone)
modeus_event.end_time = modeus_event.validate_end_time(modeus_event.end_time, timezone)

for lms_event in self.utmn.lms_events:
lms_event.dt_start = lms_event.dt_start.astimezone(timezone)
lms_event.dt_end = lms_event.dt_end.astimezone(timezone)
return self


class CalendarResponse(BulkResponse):
cached_at: datetime.datetime = Field(default_factory=datetime.datetime.now, alias="cached_at")
cached_at: datetime.datetime = Field(default_factory=now_dt_utc, alias="cached_at")

def get_hash(self) -> str:
dump = BulkResponse(**self.model_dump(by_alias=True)).model_dump_json(by_alias=True)
Expand Down
10 changes: 7 additions & 3 deletions backend/yet_another_calendar/web/api/bulk/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from yet_another_calendar.settings import settings
from ..modeus import schema as modeus_schema
from ..lms import schema as lms_schema
from ..netology import schema as netology_schema
from . import integration, schema

Expand All @@ -18,6 +19,7 @@
@router.post("/events/")
async def get_calendar(
body: modeus_schema.ModeusEventsBody,
lms_user: lms_schema.User,
cookies: Annotated[netology_schema.NetologyCookies, Depends(netology_schema.get_cookies_from_headers)],
jwt_token: Annotated[str, Depends(modeus_schema.get_cookies_from_headers)],
calendar_id: int = settings.netology_default_course_id,
Expand All @@ -27,13 +29,14 @@ async def get_calendar(
Get events from Netology and Modeus, cached.
"""

cached_calendar = await integration.get_cached_calendar(body, jwt_token, calendar_id, cookies, time_zone)
cached_calendar = await integration.get_cached_calendar(body, lms_user, jwt_token, calendar_id, cookies, time_zone)
return schema.CalendarResponse.model_validate(cached_calendar)


@router.post("/refresh_events/")
async def refresh_calendar(
body: modeus_schema.ModeusEventsBody,
lms_user: lms_schema.User,
cookies: Annotated[netology_schema.NetologyCookies, Depends(netology_schema.get_cookies_from_headers)],
jwt_token: Annotated[str, Depends(modeus_schema.get_cookies_from_headers)],
calendar_id: int = settings.netology_default_course_id,
Expand All @@ -43,12 +46,13 @@ async def refresh_calendar(
Refresh events in redis.
"""

return await integration.refresh_events(body, jwt_token, calendar_id, cookies, time_zone)
return await integration.refresh_events(body, lms_user, jwt_token, calendar_id, cookies, time_zone)


@router.post("/export_ics/")
async def export_ics(
body: modeus_schema.ModeusEventsBody,
lms_user: lms_schema.User,
cookies: Annotated[netology_schema.NetologyCookies, Depends(netology_schema.get_cookies_from_headers)],
jwt_token: Annotated[str, Depends(modeus_schema.get_cookies_from_headers)],
calendar_id: int = settings.netology_default_course_id,
Expand All @@ -57,5 +61,5 @@ async def export_ics(
"""
Export into .ics format
"""
calendar = await integration.get_calendar(body, jwt_token, calendar_id, cookies, time_zone)
calendar = await integration.get_calendar(body, lms_user, jwt_token, calendar_id, cookies, time_zone)
return StreamingResponse(integration.export_to_ics(calendar))
3 changes: 3 additions & 0 deletions backend/yet_another_calendar/web/api/lms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .views import router

__all__ = ["router"]
Loading

0 comments on commit 1895e44

Please sign in to comment.