-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from rcthomas/tests
Tests
- Loading branch information
Showing
12 changed files
with
370 additions
and
147 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import datetime | ||
import json | ||
|
||
|
||
class _JSONEncoder(json.JSONEncoder): | ||
def default(self, obj): | ||
if isinstance(obj, datetime.datetime): | ||
return obj.isoformat() | ||
return json.JSONEncoder.default(self, obj) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import datetime | ||
import json | ||
|
||
import aiofiles | ||
from traitlets import Float, List, Unicode | ||
from traitlets.config import LoggingConfigurable | ||
|
||
from jupyterhub_announcement.encoder import _JSONEncoder | ||
|
||
|
||
def _datetime_hook(json_dict): | ||
for (key, value) in json_dict.items(): | ||
try: | ||
json_dict[key] = datetime.datetime.fromisoformat(value) | ||
except Exception: | ||
pass | ||
return json_dict | ||
|
||
|
||
class AnnouncementQueue(LoggingConfigurable): | ||
|
||
announcements = List() | ||
|
||
persist_path = Unicode( | ||
"", | ||
help="""File path where announcements persist as JSON. | ||
For a persistent announcement queue, this parameter must be set to | ||
a non-empty value and correspond to a read+write-accessible path. | ||
The announcement queue is stored as a list of JSON objects. If this | ||
parameter is set to a non-empty value: | ||
* The persistence file is used to initialize the announcement queue | ||
at start-up. This is the only time the persistence file is read. | ||
* If the persistence file does not exist at start-up, it is | ||
created when an announcement is added to the queue. | ||
* The persistence file is over-written with the contents of the | ||
announcement queue each time a new announcement is added. | ||
If this parameter is set to an empty value (the default) then the | ||
queue is just empty at initialization and the queue is ephemeral; | ||
announcements will not be persisted on updates to the queue.""", | ||
).tag(config=True) | ||
|
||
lifetime_days = Float( | ||
7.0, | ||
help="""Number of days to retain announcements. | ||
Announcements that have been in the queue for this many days are | ||
purged from the queue.""", | ||
).tag(config=True) | ||
|
||
def __init__(self, **kwargs): | ||
super().__init__(**kwargs) | ||
|
||
if self.persist_path: | ||
self.log.info(f"restoring queue from {self.persist_path}") | ||
self._handle_restore() | ||
else: | ||
self.log.info("ephemeral queue, persist_path not set") | ||
self.log.info(f"queue has {len(self.announcements)} announcements") | ||
|
||
def __len__(self): | ||
return len(self.announcements) | ||
|
||
def _handle_restore(self): | ||
try: | ||
self._restore() | ||
except FileNotFoundError: | ||
self.log.info(f"persist_path not found ({self.persist_path})") | ||
except Exception as err: | ||
self.log.error(f"failed to restore queue ({err})") | ||
|
||
def _restore(self): | ||
with open(self.persist_path) as stream: | ||
self.announcements = json.load(stream, object_hook=_datetime_hook) | ||
|
||
async def update(self, user, announcement=""): | ||
self.announcements.append( | ||
dict( | ||
user=user, announcement=announcement, timestamp=datetime.datetime.now() | ||
) | ||
) | ||
if self.persist_path: | ||
self.log.info(f"persisting queue to {self.persist_path}") | ||
await self._handle_persist() | ||
|
||
async def _handle_persist(self): | ||
try: | ||
await self._persist() | ||
except Exception as err: | ||
self.log.error(f"failed to persist queue ({err})") | ||
|
||
async def _persist(self): | ||
async with aiofiles.open(self.persist_path, "w") as stream: | ||
await stream.write( | ||
json.dumps(self.announcements, cls=_JSONEncoder, indent=2) | ||
) | ||
|
||
async def purge(self): | ||
max_age = datetime.timedelta(days=self.lifetime_days) | ||
now = datetime.datetime.now() | ||
old_count = len(self.announcements) | ||
self.announcements = [ | ||
a for a in self.announcements if now - a["timestamp"] < max_age | ||
] | ||
if self.persist_path and len(self.announcements) < old_count: | ||
self.log.info(f"persisting queue to {self.persist_path}") | ||
await self._handle_persist() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import os | ||
|
||
from jupyterhub.utils import make_ssl_context | ||
from traitlets import Unicode | ||
from traitlets.config import Configurable | ||
|
||
|
||
class SSLContext(Configurable): | ||
|
||
keyfile = Unicode( | ||
os.getenv("JUPYTERHUB_SSL_KEYFILE", ""), help="SSL key, use with certfile" | ||
).tag(config=True) | ||
|
||
certfile = Unicode( | ||
os.getenv("JUPYTERHUB_SSL_CERTFILE", ""), help="SSL cert, use with keyfile" | ||
).tag(config=True) | ||
|
||
cafile = Unicode( | ||
os.getenv("JUPYTERHUB_SSL_CLIENT_CA", ""), | ||
help="SSL CA, use with keyfile and certfile", | ||
).tag(config=True) | ||
|
||
def ssl_context(self): | ||
if self.keyfile and self.certfile and self.cafile: | ||
return make_ssl_context( | ||
self.keyfile, self.certfile, cafile=self.cafile, check_hostname=False | ||
) | ||
else: | ||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
pre-commit | ||
flake8 | ||
pytest | ||
pytest-asyncio | ||
pytest-cov |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
aiofiles | ||
html-sanitizer | ||
jupyterhub |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,11 +5,7 @@ | |
author_email="[email protected]", | ||
data_files=[("share/jupyterhub/announcement/templates", ["templates/index.html"])], | ||
description="JupyterHub Announcement Service", | ||
install_requires=[ | ||
"aiofiles", | ||
"html-sanitizer", | ||
"jupyterhub", | ||
], | ||
install_requires=open("requirements.txt").read().splitlines(), | ||
name="jupyterhub-announcement", | ||
packages=["jupyterhub_announcement"], | ||
version="0.8.0.dev", | ||
|
Empty file.
Oops, something went wrong.