[doc] Add docstrings for core

This commit is contained in:
Cathy Hu 2020-07-11 12:54:07 +02:00 committed by acipm
parent a00a6b8497
commit 051dd062ac
10 changed files with 263 additions and 27 deletions

View file

@ -4,6 +4,19 @@
#
# SPDX-License-Identifier: 0BSD
""" Configuration file and command line argument parser.
Gives a dictionary named `config` with configuration parsed either from
`/etc/kibicara.conf` or from a file given by command line argument `-f`.
If no configuration was found at all, the defaults are used.
Example:
```
from kibicara.config import config
print(config)
```
"""
from argparse import ArgumentParser
from pytoml import load
from sys import argv
@ -14,6 +27,10 @@ config = {
'frontend_path': None,
'root_url': 'http://localhost:8000/',
}
""" Default configuration.
The default configuration gets overwritten by a configuration file if one exists.
"""
if argv[0].endswith('kibicara'):
parser = ArgumentParser()

View file

@ -3,6 +3,8 @@
#
# SPDX-License-Identifier: 0BSD
""" E-Mail handling. """
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from logging import getLogger
@ -14,6 +16,22 @@ logger = getLogger(__name__)
def send_email(to, subject, sender='kibicara', body=''):
""" E-Mail sender.
Sends an E-Mail to a specified recipient with a body
Example:
```
from kibicara import email
email.send_email('abc@de.fg', 'My Email subject', body='Hi this is a mail body.')
```
Args:
to (str): Recipients' e-mail address
subject (str): The subject of the e-mail
sender (str): optional, Sender of the e-mail
body (str): The body of the e-mail
"""
msg = MIMEMultipart()
msg['From'] = 'Kibicara <%s@%s>' % (sender, getfqdn())
msg['To'] = to

View file

@ -3,6 +3,8 @@
#
# SPDX-License-Identifier: 0BSD
""" Entrypoint of Kibicara. """
from asyncio import run as asyncio_run
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
@ -19,17 +21,23 @@ logger = getLogger(__name__)
class Main:
def __init__(self):
asyncio_run(self.run())
""" Entrypoint for Kibicara.
async def run(self):
Initializes the platform bots and starts the hypercorn webserver serving the
Kibicara application and the specified frontend on port 8000.
"""
def __init__(self):
asyncio_run(self.__run())
async def __run(self):
basicConfig(level=DEBUG, format="%(asctime)s %(name)s %(message)s")
getLogger('aiosqlite').setLevel(WARNING)
Mapping.create_all()
await Spawner.init_all()
await self._start_webserver()
await self.__start_webserver()
async def _start_webserver(self):
async def __start_webserver(self):
class SinglePageApplication(StaticFiles):
async def get_response(self, path, scope):
response = await super().get_response(path, scope)

View file

@ -3,6 +3,8 @@
#
# SPDX-License-Identifier: 0BSD
""" ORM Models for core. """
from databases import Database
from kibicara.config import config
from ormantic import Integer, ForeignKey, Model, Text

View file

@ -3,6 +3,8 @@
#
# SPDX-License-Identifier: 0BSD
""" API classes for implementing bots for platforms. """
from asyncio import create_task, Queue
from kibicara.model import BadWord, Trigger
from logging import getLogger
@ -13,47 +15,116 @@ logger = getLogger(__name__)
class Message:
"""The Message object that is send through the censor.
Examples:
```
message = Message('Message sent by a user from platform xyz', xyz_message_id=123)
```
Args:
text (str): The message text
**kwargs (object, optional): Other platform-specific data.
Attributes:
text (str): The message text
**kwargs (object, optional): Other platform-specific data.
"""
def __init__(self, text, **kwargs):
self.text = text
self.__dict__.update(kwargs)
class Censor:
instances = {}
""" The superclass for a platform bot.
The censor is the superclass for every platform bot. It distributes a message to all
other bots from the same hood if it passes the message filter. It provides methods
to start and stop the bot and an overwritable stub for a starting routine.
Examples:
```
class XYZPlatform(Censor):
def __init__(self, xyz_model):
super().__init__(xyz_model.hood)
...
async def run(self):
await gather(self.poll(), self.push())
...
async def poll(self):
while True:
# XXX get text message from platform xyz
await self.publish(Message(text))
...
async def push(self):
while True:
message = await self.receive()
# XXX send message.text to platform xyz
```
Args:
hood (Hood): A Hood Model object
Attributes:
hood (Hood): A Hood Model object
"""
__instances = {}
def __init__(self, hood):
self.hood = hood
self.inbox = Queue()
self.task = None
self.hood_censors = self.instances.setdefault(hood.id, [])
self.hood_censors.append(self)
self._inbox = Queue()
self.__task = None
self.__hood_censors = self.__instances.setdefault(hood.id, [])
self.__hood_censors.append(self)
def start(self):
if self.task is None:
self.task = create_task(self.__run())
""" Start the bot.
Note: This will be called by a spawner, a platform bot should not call this.
"""
if self.__task is None:
self.__task = create_task(self.__run())
def stop(self):
if self.task is not None:
self.task.cancel()
self.task = None
""" Stop the bot.
Note: This will be called by a spawner, a platform bot should not call this.
"""
if self.__task is not None:
self.__task.cancel()
self.__task = None
async def __run(self):
await self.hood.load()
self.task.set_name('%s %s' % (self.__class__.__name__, self.hood.name))
self.__task.set_name('%s %s' % (self.__class__.__name__, self.hood.name))
await self.run()
# override this in derived class
async def run(self):
""" Entry point for a bot.
Note: Override this in the derived bot class.
"""
pass
async def publish(self, message):
""" Distribute a message to the bots in a hood.
Args:
message (Message): Message to distribute
"""
if not await self.__is_appropriate(message):
return
for censor in self.hood_censors:
await censor.inbox.put(message)
for censor in self.__hood_censors:
await censor._inbox.put(message)
async def receive(self):
return await self.inbox.get()
""" Receive a message.
Returns (Message): Received message
"""
return await self._inbox.get()
async def __is_appropriate(self, message):
for badword in await BadWord.objects.filter(hood=self.hood).all():
@ -69,17 +140,40 @@ class Censor:
class Spawner:
instances = []
""" Spawns a bot with a specific bot model.
Examples:
```
class XYZPlatform(Censor):
# bot class
class XYZ(Model):
# bot model
spawner = Spawner(XYZ, XYZPlatform)
```
Args:
ORMClass (ORM Model subclass): A Bot Model object
BotClass (Censor subclass): A Bot Class object
Attributes:
ORMClass (ORM Model subclass): A Hood Model object
BotClass (Censor subclass): A Bot Class object
"""
__instances = []
def __init__(self, ORMClass, BotClass):
self.ORMClass = ORMClass
self.BotClass = BotClass
self.bots = {}
self.instances.append(self)
self.__bots = {}
self.__instances.append(self)
@classmethod
async def init_all(cls):
for spawner in cls.instances:
""" Instantiate and start a bot for every row in the corresponding ORM model. """
for spawner in cls.__instances:
await spawner._init()
async def _init(self):
@ -87,13 +181,34 @@ class Spawner:
self.start(item)
def start(self, item):
bot = self.bots.setdefault(item.pk, self.BotClass(item))
""" Instantiate and start a bot with the provided ORM object.
Example:
```
xyz = await XYZ.objects.create(hood=hood, **values.__dict__)
spawner.start(xyz)
```
Args:
item (ORM Model object): Argument to the bot constructor
"""
bot = self.__bots.setdefault(item.pk, self.BotClass(item))
bot.start()
def stop(self, item):
bot = self.bots.pop(item.pk, None)
""" Stop and delete a bot.
Args:
item (ORM Model object): ORM object corresponding to bot.
"""
bot = self.__bots.pop(item.pk, None)
if bot is not None:
bot.stop()
def get(self, item):
return self.bots.get(item.pk)
""" Get a running bot.
Args:
item (ORM Model object): ORM object corresponding to bot.
"""
return self.__bots.get(item.pk)

View file

@ -3,6 +3,12 @@
#
# SPDX-License-Identifier: 0BSD
""" Routing definitions for the REST API.
A platform bot shall add its API router in this `__init__.py`
file to get included into the main application.
"""
from fastapi import APIRouter
from kibicara.platforms.test.webapi import router as test_router
from kibicara.platforms.telegram.webapi import router as telegram_router

View file

@ -3,6 +3,8 @@
#
# SPDX-License-Identifier: 0BSD
""" REST API endpoints for hood admins. """
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from kibicara import email
@ -71,8 +73,14 @@ router = APIRouter()
@router.post('/register/', status_code=status.HTTP_202_ACCEPTED)
async def admin_register(values: BodyAdmin):
""" Sends an email with a confirmation link.
- **email**: E-Mail Address of new hood admin
- **password**: Password of new hood admin
"""
register_token = to_token(**values.__dict__)
logger.debug(f'register_token={register_token}')
# TODO implement check to see if email already is in database
try:
email.send_email(
to=values.email,
@ -88,6 +96,10 @@ async def admin_register(values: BodyAdmin):
@router.post('/confirm/{register_token}')
async def admin_confirm(register_token: str):
""" Registration confirmation and account creation.
- **register_token**: Registration token received in email from /register
"""
try:
values = from_token(register_token)
passhash = argon2.hash(values['password'])
@ -99,6 +111,11 @@ async def admin_confirm(register_token: str):
@router.post('/login/')
async def admin_login(form_data: OAuth2PasswordRequestForm = Depends()):
""" Get an access token.
- **username**: Email of a registered hood admin
- **password**: Password of a registered hood admin
"""
try:
await get_auth(form_data.username, form_data.password)
except ValueError:
@ -112,6 +129,7 @@ async def admin_login(form_data: OAuth2PasswordRequestForm = Depends()):
@router.get('/hoods/')
async def admin_hood_read_all(admin=Depends(get_admin)):
""" Get a list of all hoods of a given admin. """
return (
await AdminHoodRelation.objects.select_related('hood').filter(admin=admin).all()
)

View file

@ -3,6 +3,8 @@
#
# SPDX-License-Identifier: 0BSD
""" REST API Endpoints for managing hoods. """
from fastapi import APIRouter, Depends, HTTPException, Response, status
from kibicara.model import AdminHoodRelation, Hood
from kibicara.webapi.admin import get_admin
@ -38,11 +40,17 @@ router = APIRouter()
@router.get('/')
async def hood_read_all():
""" Get all existing hoods. """
return await Hood.objects.all()
@router.post('/', status_code=status.HTTP_201_CREATED)
async def hood_create(values: BodyHood, response: Response, admin=Depends(get_admin)):
""" Creates a hood.
- **name**: Name of the hood
- **landingpage**: Markdown formatted description of the hood
"""
try:
hood = await Hood.objects.create(**values.__dict__)
await AdminHoodRelation.objects.create(admin=admin.id, hood=hood.id)
@ -54,16 +62,23 @@ async def hood_create(values: BodyHood, response: Response, admin=Depends(get_ad
@router.get('/{hood_id}')
async def hood_read(hood=Depends(get_hood)):
""" Get hood with id **hood_id**. """
return hood
@router.put('/{hood_id}', status_code=status.HTTP_204_NO_CONTENT)
async def hood_update(values: BodyHood, hood=Depends(get_hood)):
""" Updates hood with id **hood_id**.
- **name**: New name of the hood
- **landingpage**: New Markdown formatted description of the hood
"""
await hood.update(**values.__dict__)
@router.delete('/{hood_id}', status_code=status.HTTP_204_NO_CONTENT)
async def hood_delete(hood=Depends(get_hood)):
""" Deletes hood with id **hood_id**. """
for relation in await AdminHoodRelation.objects.filter(hood=hood).all():
await relation.delete()
await hood.delete()

View file

@ -3,6 +3,13 @@
#
# SPDX-License-Identifier: 0BSD
""" REST API endpoints for managing badwords.
Provides API endpoints for adding, removing and reading regular expressions that block a
received message to be dropped by a censor. This provides a message filter customizable
by the hood admins.
"""
from fastapi import APIRouter, Depends, HTTPException, Response, status
from kibicara.model import BadWord
from kibicara.webapi.hoods import get_hood
@ -28,6 +35,7 @@ router = APIRouter()
@router.get('/')
async def badword_read_all(hood=Depends(get_hood)):
""" Get all badwords of hood with id **hood_id**. """
return await BadWord.objects.filter(hood=hood).all()
@ -35,6 +43,10 @@ async def badword_read_all(hood=Depends(get_hood)):
async def badword_create(
values: BodyBadWord, response: Response, hood=Depends(get_hood)
):
""" Creates a new badword for hood with id **hood_id**.
- **pattern**: Regular expression which is used to match a badword.
"""
try:
regex_compile(values.pattern)
badword = await BadWord.objects.create(hood=hood, **values.__dict__)
@ -48,14 +60,20 @@ async def badword_create(
@router.get('/{badword_id}')
async def badword_read(badword=Depends(get_badword)):
""" Reads badword with id **badword_id** for hood with id **hood_id**. """
return badword
@router.put('/{badword_id}', status_code=status.HTTP_204_NO_CONTENT)
async def badword_update(values: BodyBadWord, badword=Depends(get_badword)):
""" Updates badword with id **badword_id** for hood with id **hood_id**.
- **pattern**: Regular expression which is used to match a badword
"""
await badword.update(**values.__dict__)
@router.delete('/{badword_id}', status_code=status.HTTP_204_NO_CONTENT)
async def badword_delete(badword=Depends(get_badword)):
""" Deletes badword with id **badword_id** for hood with id **hood_id**. """
await badword.delete()

View file

@ -3,6 +3,14 @@
#
# SPDX-License-Identifier: 0BSD
""" REST API endpoints for managing triggers.
Provides API endpoints for adding, removing and reading regular expressions that allow a
message to be passed through by a censor. A published message must match one of these
regular expressions otherwise it gets dropped by the censor. This provides a message
filter customizable by the hood admins.
"""
from fastapi import APIRouter, Depends, HTTPException, Response, status
from kibicara.model import Trigger
from kibicara.webapi.hoods import get_hood
@ -28,6 +36,7 @@ router = APIRouter()
@router.get('/')
async def trigger_read_all(hood=Depends(get_hood)):
""" Get all triggers of hood with id **hood_id**. """
return await Trigger.objects.filter(hood=hood).all()
@ -35,6 +44,10 @@ async def trigger_read_all(hood=Depends(get_hood)):
async def trigger_create(
values: BodyTrigger, response: Response, hood=Depends(get_hood)
):
""" Creates a new trigger for hood with id **hood_id**.
- **pattern**: Regular expression which is used to match a trigger.
"""
try:
regex_compile(values.pattern)
trigger = await Trigger.objects.create(hood=hood, **values.__dict__)
@ -48,14 +61,20 @@ async def trigger_create(
@router.get('/{trigger_id}')
async def trigger_read(trigger=Depends(get_trigger)):
""" Reads trigger with id **trigger_id** for hood with id **hood_id**. """
return trigger
@router.put('/{trigger_id}', status_code=status.HTTP_204_NO_CONTENT)
async def trigger_update(values: BodyTrigger, trigger=Depends(get_trigger)):
""" Updates trigger with id **trigger_id** for hood with id **hood_id**.
- **pattern**: Regular expression which is used to match a trigger
"""
await trigger.update(**values.__dict__)
@router.delete('/{trigger_id}', status_code=status.HTTP_204_NO_CONTENT)
async def trigger_delete(trigger=Depends(get_trigger)):
""" Deletes trigger with id **trigger_id** for hood with id **hood_id**. """
await trigger.delete()