Skip to content
This repository has been archived by the owner on Dec 11, 2021. It is now read-only.

Migrate off topic channel name endpoint from the site API #26

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .github/workflows/lint-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
PRE_COMMIT_HOME: ${{ github.workspace }}/.cache/pre-commit-cache

AUTH_TOKEN: ci-token
DATABASE_URL: postgres://pysite:pysite@postgres:5432/pysite
TESTING: true

# Via https://github.com/actions/example-services/blob/main/.github/workflows/postgres-service.yml
# services:
Expand Down Expand Up @@ -101,7 +101,3 @@ jobs:

- name: Run pytest
run: pytest
env:
POSTGRES_HOST: localhost
# Get the published port.
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
12 changes: 11 additions & 1 deletion api/core/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@
This package contains the ORM models, migrations, and
other functionality related to database interop.
"""
from unittest.mock import Mock

from sqlalchemy.orm import declarative_base
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker

from api.core.settings import settings

Base = declarative_base()
metadata = Base.metadata

if settings.TESTING:
session_factory = Mock()
else:
engine = create_engine(settings.database_url)
session_factory = sessionmaker(bind=engine)
2 changes: 1 addition & 1 deletion api/core/database/models/api/bot/reminder.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Reminder(Base):

# Whether this reminder is still active.
# If not, it has been sent out to the user.
active = Column(Boolean, nullable=False)
active = Column(Boolean, nullable=False, default=True)
# The channel ID that this message was
# sent in, taken from Discord.
channel_id = Column(BigInteger, nullable=False)
Expand Down
10 changes: 7 additions & 3 deletions api/core/database/models/api/bot/user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import NoReturn, Union
from unittest.mock import Mock

from sqlalchemy import (
ARRAY,
Expand All @@ -16,8 +17,11 @@
from api.core.settings import settings
from .role import Role

engine = create_engine(settings.database_url)
SessionLocal = sessionmaker(bind=engine)
if not settings.TESTING:
engine = create_engine(settings.database_url)
SessionLocal = sessionmaker(bind=engine)
else:
SessionLocal = Mock()


class User(Base):
Expand All @@ -39,7 +43,7 @@ class User(Base):
in_guild = Column(Boolean, nullable=False, default=True)

# IDs of roles the user has on the server
roles = Column(ARRAY(BigInteger()), nullable=False)
roles = Column(ARRAY(BigInteger()), nullable=False, default=[])

@validates("id")
def validate_user_id(self, _key: str, user_id: int) -> Union[int, NoReturn]:
Expand Down
5 changes: 3 additions & 2 deletions api/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import the name `settings` from `api.core` directly.
"""

from pydantic import BaseSettings, PostgresDsn
from pydantic import BaseSettings, Field, PostgresDsn


class Settings(BaseSettings):
Expand All @@ -27,10 +27,11 @@ class Settings(BaseSettings):
`pydantic.error_wrappers.ValidationError` exception.
"""

database_url: PostgresDsn
database_url: PostgresDsn = Field(None)
auth_token: str
commit_sha: str = "development"
DEBUG: bool = False
TESTING: bool = False

class Config:
"""Configure Settings to load a `.env` file if present."""
Expand Down
9 changes: 9 additions & 0 deletions api/endpoints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@
There are currently no plan to use a strictly versioned API design, as this API
is currently tightly coupled with a single client application.
"""
from fastapi import APIRouter

from .off_topic_channel_names.endpoints import otn
from .reminder.reminder_endpoints import reminder

bot_router = APIRouter(prefix="/bot")

bot_router.include_router(reminder)
bot_router.include_router(otn)
Empty file.
10 changes: 10 additions & 0 deletions api/endpoints/dependencies/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from api.core.database import session_factory


def create_database_session() -> None:
"""A FastAPI dependency that creates an SQLAlchemy session."""
db = session_factory()
try:
yield db
finally:
db.close()
Empty file.
142 changes: 142 additions & 0 deletions api/endpoints/off_topic_channel_names/endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from typing import Optional, Union

from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Query, Session

from api.core.database.models.api.bot import OffTopicChannelName
from api.core.schemas import ErrorMessage
from api.endpoints.dependencies.database import create_database_session

otn = APIRouter(prefix="/off-topic-channel-names")


def get_all_otn(db_session: Session) -> Query:
"""Get a partial query object with .all()."""
return db_session.query(OffTopicChannelName).all()
Comment on lines +14 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move this to a separate file, into something likeotn_dependency?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't really a dependency and is only used in the current file, I don't think it deserves to go in a separate dependency file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If your code depends on this function, then it is a dependency, the reason I ask is because in the future new dependencies could be added, and I believe it would make the project structure more friendly.



@otn.get(
"/",
status_code=200,
response_model=list[str],
D0rs4n marked this conversation as resolved.
Show resolved Hide resolved
responses={404: {"model": ErrorMessage}},
)
def get_off_topic_channel_names(
random_items: Optional[int] = None,
db_session: Session = Depends(create_database_session),
) -> Union[JSONResponse, list[str]]:
"""
### GET /bot/off-topic-channel-names.

Return all known off-topic channel names from the database.

If the `random_items` query parameter is given, for example using...
$ curl 127.0.0.1:8000/api/bot/off-topic-channel-names?random_items=5
... then the API will return `5` random items from the database
that is not used in current rotation.

When running out of names, API will mark all names to not used and start new rotation.

#### Response format
Return a list of off-topic-channel names:
>>> [
... "lemons-lemonade-stand",
... "bbq-with-bisk"
... ]

#### Status codes
- 200: returned on success
- 400: returned when `random_items` is not a positive integer

## Authentication
Requires a API token.
"""
if not random_items:
queryset = get_all_otn(db_session)
return [offtopic_name.name for offtopic_name in queryset]
Shivansh-007 marked this conversation as resolved.
Show resolved Hide resolved

if random_items <= 0:
return JSONResponse(
status_code=404,
Copy link
Member

@D0rs4n D0rs4n Nov 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the API "docs":
- 400: returned when `random_items` is not a positive integer
So, this has to be 400.

content={"error": ["'random_items' must be a positive integer."]},
)

queryset = get_all_otn(db_session).order_by("used", "?")[:random_items]

# When any name is used in our listing then this means we reached end of round
# and we need to reset all other names `used` to False
if any(offtopic_name.used for offtopic_name in queryset):
# These names that we just got have to be excluded from updating used to False
get_all_otn(db_session).update(
{
OffTopicChannelName.used: OffTopicChannelName.name
in (offtopic_name.name for offtopic_name in queryset)
}
)
else:
# Otherwise mark selected names `used` to True
get_all_otn(db_session).filter_by(
name__in=(offtopic_name.name for offtopic_name in queryset)
).update(used=True)

return [offtopic_name.name for offtopic_name in queryset]


@otn.post(
"/",
status_code=201,
responses={400: {"model": ErrorMessage}},
)
def create_off_topic_channel_names(
name: str,
db_session: Session = Depends(create_database_session),
) -> None:
"""
### POST /bot/off-topic-channel-names.

Create a new off-topic-channel name in the database.
The name must be given as a query parameter, for example:
$ curl 127.0.0.1:8000/api/bot/off-topic-channel-names?name=lemons-lemonade-shop

#### Status codes
- 201: returned on success
- 400: if the request body has invalid fields, see the response for details

## Authentication
Requires a API token.
"""
new_off_topic_channel_name = OffTopicChannelName(name=name)
db_session.add(new_off_topic_channel_name)
db_session.commit()


@otn.delete("/", status_code=204, responses={404: {"model": ErrorMessage}})
async def delete_off_topic_channel_names(
name: str, db_session: Session = Depends(create_database_session)
) -> Optional[JSONResponse]:
"""
### DELETE /bot/off-topic-channel-names/<name:str>.

Delete the off-topic-channel name with the given `name`.

#### Status codes
- 204: returned on success
- 404: returned when the given `name` was not found

## Authentication
Requires a API token.
"""
if not (
otn_to_delete := db_session.query(OffTopicChannelName)
.filter_by(name=name)
.first()
):
return JSONResponse(
status_code=404,
content={
"error": "There is no off topic channel name with that `name` in the database"
},
)
db_session.delete(otn_to_delete)
db_session.commit()
1 change: 1 addition & 0 deletions api/endpoints/reminder/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .reminder_endpoints import reminder
8 changes: 8 additions & 0 deletions api/endpoints/reminder/reminder_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from fastapi import Depends

from .reminder_schemas import ReminderFilter


async def filter_values(reminder_filter: ReminderFilter = Depends()) -> dict:
"""Returns a dictionary exported from a ReminderFilter model from the Path, with None values excluded."""
return reminder_filter.dict(exclude_none=True)
Loading