Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
liske committed Sep 29, 2019
0 parents commit 6ef260e
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
src/settings
src/settings.ex
*.pyc
*/*/__pycache__
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.venv/
.vscode/
src/settings/
*.pyc
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM alpine:3.10

COPY requirements.txt src/ /app/wekan-scheduler/

WORKDIR /app/wekan-scheduler

RUN apk add --no-cache ca-certificates git python3 && \
pip3 install -r requirements.txt && \
apk del git

CMD ["./scheduler.py" ]
119 changes: 119 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Wekan Scheduler

Pythonic card scheduler for [Wekan](https://wekan.github.io/).


## About

The purpose of *wekan-scheduler* is to create cards regulary for repeating
tasks. It uses [pycron](https://github.com/kipe/pycron) for schedule
configuration and uses the
[wekan-python-api-client](https://github.com/wekan/wekan-python-api-client) to
access the [Wekan REST API](https://wekan.github.io/api/v3.00/).


## Configuration

You should create a dedicated user in *Wekan* to be used to access the *Wekan
REST API* in the `PASSWORD` auth backend (`LDAP` seems not to work). Add the
user to the boards where it should create cards.

There are three configuration files required in the `settings` directory:

- **\_\_init\_\_.py**
should left empty, required to make the config a python package
- **config.py**
contains the base configuration to access the *Wekan REST API*:
```python
CONFIG = {
# URL for Wekan
'api_url': "https://localhost/kanban",

# REST API auth headers
'api_auth': {"username": "api", "password": "xxxxxxxx"},

# Verify X.509 certificate for api_url?
'api_verify': True,

# Sleep for N seconds between schedules (should be at least 60s)
'sleep': 300,
}
```
- **schedules.py**
contains your custom schedules:
```python
SCHEDULES = [
# daily work card
{
# cron-like schedule (pycron)
'schedule': '0 8 * * 1-5',

# target board and list
'board': '<board-id>',
'list': '<list-id>',

# required fields
'card': {
'title': 'check for donuts',
},

# more card details
'details': {
'description': 'first-come, first-served',
'color': 'pink',
'labelIds': ['<label-id>'],
'members': ['<user-id>'],
},
},
]
```

There is a `get-ids.py` helper script to dump the various IDs of your Wekan
instance. This helps to find the required various IDs for your schedules:

```
{'boards': {'4b5BmHE2CL8wt8brR': {'labels': {'2kRvzr': 'gray[crumbly]',
'jdiYNd': 'plum[goo]',
'xhAdgq': 'lime[tasty]'},
'lists': {'zzujS93kd8982k30y': 'Backlog',
'loxc9Dll,masd300a': 'Eating',
'qosLk34SDKsmsksd3': 'Done',
'lasd93lkdaskSAKsl': 'Problem'},
'title:': 'Donut Fighters'}},
'users': [{'_id': 'Ksd34pKsW23wFg9Sx', 'username': 'admin'},
{'_id': 'lS9lkasd3mAusdkSK', 'username': 'api'}]}
```


## Deploy

The recommended way to run *wekan-scheduler* is to use the provided auto-build
docker images from [Docker
Hub](https://cloud.docker.com/u/liske/repository/docker/liske/wekan-scheduler).
Run it with *docker-compose*:

```yaml
version: '3'

services:
scheduler:
image: liske/wekan-scheduler:0.1
restart: always
volumes:
- ./settings:/app/wekan-scheduler/settings:ro
```
If you are running *Wekan* on *Docker* you may add *wekan-scheduler* to your
existing `docker-compose.yml`:

```yaml
# ...
scheduler:
image: liske/wekan-scheduler:0.1
restart: always
volumes:
- ./scheduler/settings:/app/wekan-scheduler/settings:ro
depends_on:
- wekan
# ...
```
7 changes: 7 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
certifi==2019.9.11
chardet==3.0.4
idna==2.8
pycron==1.0.0
requests==2.22.0
urllib3==1.25.6
git+https://github.com/wekan/wekan-python-api-client.git#egg=wekanapi&subdirectory=src
37 changes: 37 additions & 0 deletions src/get-ids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env python3

from pprint import pprint
import wekan_scheduler
from settings.config import CONFIG


def get_ids():
api = wekan_scheduler.api()

ids = {
'users': api.api_call("/api/users"),
'boards': {},
}

for board in api.get_user_boards():
if board.title == 'Templates':
continue

ids['boards'][board.id] = {'title:': board.title, 'labels': {}, 'lists': {}}

data = api.api_call("/api/boards/{}".format(board.id))
for label in data['labels']:
ids['boards'][board.id]['labels'][label['_id']] = '{}[{}]'.format(label['color'], label.get('name', ''))

for cardlist in board.get_cardslists():
ids['boards'][board.id]['lists'][cardlist.id] = cardlist.title

return ids


if __name__ == "__main__":
if CONFIG['api_verify']:
pprint(get_ids())
else:
with wekan_scheduler.no_ssl_verification():
pprint(get_ids())
68 changes: 68 additions & 0 deletions src/scheduler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env python3

import time
import datetime
import sys
import pycron
import wekan_scheduler
from settings.config import CONFIG
from settings.schedules import SCHEDULES


from pprint import pprint

def create_card(_sched):
# create a work copy
sched = _sched.copy()

# Wekan API wrapper
api = wekan_scheduler.api()

# add authorId if missing
if not 'authorId' in sched['card']:
sched['card']['authorId'] = api.user_id

# add swimlaneId if missing
if not 'swimlaneId' in sched['card']:
sl = api.api_call("/api/boards/{}/swimlanes".format(sched['board']))
sched['card']['swimlaneId'] = sl[0]['_id']

# create card
res = api.api_call(
"/api/boards/{}/lists/{}/cards".format(sched['board'], sched['list']), sched['card'])

# add details
if '_id' in res:
api_response = api.session.put(
"{}{}".format(api.api_url, "/api/boards/{}/lists/{}/cards/{}".format(
sched['board'], sched['list'], res['_id'])),
data=sched['details'],
headers={"Authorization": "Bearer {}".format(api.token)},
proxies=api.proxies
)

sinces = {}
for idx, sched in enumerate(SCHEDULES):
sinces[idx] = datetime.datetime.now()

def main():
while True:
for idx, sched in enumerate(SCHEDULES):
ts = datetime.datetime.now()
if pycron.has_been(sched['schedule'], sinces[idx]):
try:
print("#{} RUNNING SCHEDULE".format(idx + 1))
create_card(sched)
sinces[idx] = ts
print("#{} => DONE".format(idx + 1))
except:
print("#{} => EXCEPTION: {}".format(idx + 1, sys.exc_info()))

time.sleep(CONFIG['sleep'])

if __name__ == "__main__":
if CONFIG['api_verify']:
main()
else:
with wekan_scheduler.no_ssl_verification():
main()
Empty file added src/settings.ex/__init__.py
Empty file.
13 changes: 13 additions & 0 deletions src/settings.ex/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CONFIG = {
# URL for wekan
'api_url': "https://localhost/kanban",

# REST API auth headers
'api_auth': {"username": "api", "password": "xxxxxxxx"},

# Verify X.509 certificate for api_url?
'api_verify': True,

# Sleep for N seconds between schedules (should be at least 60s)
'sleep': 300,
}
24 changes: 24 additions & 0 deletions src/settings.ex/schedules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
SCHEDULES = [
# daily work card
{
# cron-like schedule (pycron)
'schedule': '0 8 * * 1-5',

# target board and list
'board': '<board-id>',
'list': '<list-id>',

# required fields
'card': {
'title': 'check for donuts',
},

# more card details
'details': {
'description': 'first-come, first-served',
'color': 'pink',
'labelIds': ['<label-id>'],
'members': ['<user-id>'],
},
},
]
52 changes: 52 additions & 0 deletions src/wekan_scheduler/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import warnings
import contextlib
import requests
from urllib3.exceptions import InsecureRequestWarning
from wekanapi import WekanApi
from settings.config import CONFIG


old_merge_environment_settings = requests.Session.merge_environment_settings


@contextlib.contextmanager
def no_ssl_verification():
opened_adapters = set()

def merge_environment_settings(self, url, proxies, stream, verify, cert):
# Verification happens only once per connection so we need to close
# all the opened adapters once we're done. Otherwise, the effects of
# verify=False persist beyond the end of this context manager.
opened_adapters.add(self.get_adapter(url))

settings = old_merge_environment_settings(
self, url, proxies, stream, verify, cert)
settings['verify'] = False

return settings

requests.Session.merge_environment_settings = merge_environment_settings

try:
with warnings.catch_warnings():
warnings.simplefilter('ignore', InsecureRequestWarning)
yield
finally:
requests.Session.merge_environment_settings = old_merge_environment_settings

for adapter in opened_adapters:
try:
adapter.close()
except:
pass


_api = None


def api():
global _api
if _api is None:
_api = WekanApi(CONFIG['api_url'],
CONFIG['api_auth'], )
return _api

0 comments on commit 6ef260e

Please sign in to comment.