Merge pull request #3 from kprestel/dev

Updating with new functions
master
Kyle Prestel 2022-12-02 12:45:48 -05:00 zatwierdzone przez GitHub
commit c106dccfae
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
12 zmienionych plików z 627 dodań i 613 usunięć

34
.github/workflows/ci.yml vendored 100644
Wyświetl plik

@ -0,0 +1,34 @@
name: CI
on: push
jobs:
ci:
strategy:
fail-fast: false
matrix:
python-version: [3.8, 3.9]
poetry-version: [1.1.6]
os: [ubuntu-18.04]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@master
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Run image
uses: abatilo/actions-poetry@v2.0.0
with:
poetry-version: ${{ matrix.poetry-version }}
- name: build package
run: >-
poetry build
- name: Publish distribution 📦 to Test PyPI
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
- name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.PYPI_API_TOKEN }}

133
.gitignore vendored
Wyświetl plik

@ -1,4 +1,137 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Project specific
.idea
test/data
test/config
test/.pytest_cache
venv
.tox

Wyświetl plik

@ -1,3 +1,3 @@
FROM ghcr.io/linuxserver/nextcloud
COPY test/data /data
COPY test/config /config
COPY tests/data /data
COPY tests/config /config

Wyświetl plik

@ -1,5 +1,5 @@
.PHONY: clean-pyc test clean-build
TEST_PATH=tests
TEST_PATH=test
clean-pyc:
find . -name '*.pyc' -exec rm --force {} +
@ -20,12 +20,12 @@ isort:
lint:
flake8 --exclude=.tox
test: clean-pyc
test: clean-pyc build-test start-nextcloud-test-instance
tox
black:
black deck/
black test/
black tests/
create-volumes:
docker volume create nextcloud-data
@ -34,10 +34,18 @@ create-volumes:
build-test-image:
docker build -t nextcloud-test .
build-test: create-volumes build-test-image
build:
poetry build
deploy: build
poetry publish
publish: deploy
publish: deploy
start-nextcloud-test-instance:
./bin/start-nextcloud.sh
install-deck:
docker exec --user www-data nextcloud php occ app:install deck

Wyświetl plik

@ -1,3 +1,5 @@
# Python Nextcloud Deck API
Simple python based wrapper around the Nextcloud deck API
Simple python based wrapper around the Nextcloud deck API
Before using this library you should get familiar with the [Offical REST API](https://deck.readthedocs.io/en/latest/API/)

3
bin/start-nextcloud.sh 100644 → 100755
Wyświetl plik

@ -4,6 +4,9 @@ docker run -d \
-e PUID=1000 \
-e PGID=1001 \
-e TZ=Europe/London \
-e NEXTCLOUD_ADMIN_USER=Admin \
-e NEXTCLOUD_ADMIN_PASSWORD=admin \
-e SQLITE_DATABASE=nextcloud-deck-test \
-p 443:443 \
-v nextcloud-config:/config \
-v nextcloud-data:/data \

Wyświetl plik

@ -8,8 +8,11 @@ from deck.models import Board, Card, Label, Stack, deserialize
logger = logging.getLogger(__name__)
IdType = typing.Union[int, str]
class NextCloudDeckAPI:
"""docstring for NextCloudDeck."""
"""Wrapper around the NextCloud Deck API"""
def __init__(
self,
@ -38,6 +41,14 @@ class NextCloudDeckAPI:
)
return response.json()
@deserialize(Board)
def create_board(self, title, color="ff0000") -> Board:
logger.info(f"Creating board with title: {title} and color: {color}")
response = self.session.post(
f"{self.url}", json={"title": title, "color": color}
)
return response.json()
@deserialize(Board)
def get_board(self, board_id) -> Board:
response = self.session.get(
@ -45,6 +56,72 @@ class NextCloudDeckAPI:
)
return response.json()
def update_board(
self, board_id: IdType, title: str, color: str, archived: bool
) -> typing.Dict[str, typing.Any]:
response = self.session.put(
f"{self.url}/boards/{board_id}",
json={"title": title, "color": color, "archived": archived},
)
return response.json()
def delete_board(self, board_id):
logger.info(f"Deleting board with ID: {board_id}")
response = self.session.delete(f"{self.url}/{board_id}")
logger.debug(response)
return response.json()
def undo_delete_board(self, board_id: IdType) -> typing.Dict[str, typing.Any]:
response = self.session.post(f"{self.url}/boards/{board_id}/undo_delete")
return response.json()
def add_board_acl_rule(
self,
board_id: IdType,
type: int,
participant: str,
perm_edit: bool,
perm_share: bool,
perm_manage: bool,
):
response = self.session.post(
f"{self.url}/boards/{board_id}/acl",
json={
"type": type,
"participant": participant,
"permissionEdit": perm_edit,
"permissionShare": perm_share,
"permissionManage": perm_manage,
},
)
return response.json() # TODO: Deserialization to model for ACL rule
def update_board_acl_rule(
self,
board_id: IdType,
acl_id: IdType,
perm_edit: bool,
perm_share: bool,
perm_manage: bool,
) -> typing.Dict[str, typing.Any]:
response = self.session.put(
f"{self.url}/boards/{board_id}/acl/{acl_id}",
json={
"permissionEdit": perm_edit,
"permissionShare": perm_share,
"permissionManage": perm_manage,
},
)
return response.json()
def delete_board_acl_rule(
self, board_id: IdType, acl_id: IdType
) -> typing.Dict[str, typing.Any]:
response = self.session.delete(f"{self.url}/boards/{board_id}/acl/{acl_id}")
return response.json()
# Stacks
@deserialize(typing.List[Stack])
def get_stacks(self, board_id):
response = self.session.get(
@ -52,6 +129,36 @@ class NextCloudDeckAPI:
)
return response.json()
@deserialize(typing.List[Stack])
def get_archived_stacks(self, board_id: IdType) -> typing.Dict[str, typing.Any]:
response = self.session.get(f"{self.url}/{board_id}/stacks/archived")
return response.json()
@deserialize(Stack)
def get_stack(self, board_id: IdType, stack_id: IdType):
response = self.session.get(f"{self.url}/{board_id}/stacks/{stack_id}")
return response.json()
@deserialize(Stack)
def create_stack(self, board_id, title, order=999):
response = self.session.post(
f"{self.url}/{board_id}/stacks",
json={"title": title, "order": order},
)
return response.json()
def update_stack(self, board_id: IdType, stack_id: IdType, title: str, order: int):
response = self.session.put(
f"{self.url}/{board_id}/stacks/{stack_id}",
json={"title": title, "order": order},
)
return response.json()
@deserialize(Stack)
def delete_stack(self, board_id, stack_id):
response = self.session.delete(f"{self.url}/{board_id}/stacks/{stack_id}")
return response.json()
@deserialize(typing.List[Card])
def get_cards_from_stack(self, board_id, stack_id):
response = self.session.get(
@ -61,6 +168,8 @@ class NextCloudDeckAPI:
return response.json()["cards"]
return []
# Cards
@deserialize(Card)
def get_card(self, board_id, stack_id, card_id):
response = self.session.get(
@ -68,27 +177,6 @@ class NextCloudDeckAPI:
)
return response.json()
@deserialize(Label)
def create_label(self, board_id, title, color="ff0000"):
response = self.session.post(
f"{self.url}/{board_id}/labels",
json={"title": title, "color": color},
)
return response.json()
@deserialize(Stack)
def create_stack(self, board_id, title, order=999):
response = self.session.post(
f"{self.url}/{board_id}/stacks",
json={"title": title, "order": order},
)
return response.json()
@deserialize(Stack)
def delete_stack(self, board_id, stack_id):
response = self.session.delete(f"{self.url}/{board_id}/stacks/{stack_id}")
return response.json()
@deserialize(Card)
def create_card(
self,
@ -141,13 +229,8 @@ class NextCloudDeckAPI:
response = self.session.delete(
f"{self.url}/{board_id}/stacks/{stack_id}/cards/{card_id}"
)
return response.json()
def get_labels(self, board_id):
boards = self.get_board(board_id)
return boards.labels
def remove_label_from_card(self, board_id, stack_id, card_id, label_id):
logger.info(
f"removing label: board_id: {board_id}, stack_id: {stack_id}, card_id: {card_id}, label_id: {label_id}"
@ -158,12 +241,8 @@ class NextCloudDeckAPI:
)
return response.json()
def delete_label(self, board_id, label_id):
response = self.session.delete(f"{self.url}/{board_id}/labels/{label_id}")
return response.json()
def assign_label_to_card(
self, label_id, card_id, board_id, stack_id
self, board_id, stack_id, card_id, label_id
) -> typing.Dict[str, typing.Any]:
response = self.session.put(
f"{self.url}/{board_id}/stacks/{stack_id}/cards/{card_id}/assignLabel",
@ -171,16 +250,65 @@ class NextCloudDeckAPI:
)
return response.json()
@deserialize(Board)
def create_board(self, title, color="ff0000") -> Board:
logger.info(f"Creating board with title: {title} and color: {color}")
response = self.session.post(
f"{self.url}", json={"title": title, "color": color}
def assign_user_to_card(
self, board_id, stack_id, card_id, user_id
) -> typing.Dict[str, typing.Any]:
response = self.session.put(
f"{self.url}/{board_id}/stacks/{stack_id}/cards/{card_id}/assignUser",
json={"userId": user_id},
)
return response.json()
def delete_board(self, board_id):
logger.info(f"Deleting board with ID: {board_id}")
response = self.session.delete(f"{self.url}/{board_id}")
logger.debug(response)
def unassign_user_from_card(
self, board_id, stack_id, card_id, user_id
) -> typing.Dict[str, typing.Any]:
response = self.session.put(
f"{self.url}/{board_id}/stacks/{stack_id}/cards/{card_id}/unassignUser",
json={"userId": user_id},
)
return response.json()
def reorder_card(
self, board_id, stack_id, card_id, order, stack_target
) -> typing.Dict[str, typing.Any]:
response = self.session.put(
f"{self.url}/{board_id}/stacks/{stack_id}/cards/{card_id}/reorder",
json={"order": order, "stackId": stack_target},
)
return response.json()
# Labels
@deserialize(Label)
def get_label(
self, board_id: IdType, label_id: IdType
) -> typing.Dict[str, typing.Any]:
response = self.session.get(f"{self.url}/{board_id}/labels/{label_id}")
return response.json()
@deserialize(Label)
def create_label(self, board_id, title, color="ff0000"):
response = self.session.post(
f"{self.url}/{board_id}/labels",
json={"title": title, "color": color},
)
return response.json()
def update_label(
self, board_id: IdType, label_id: IdType, title: str, color: str
) -> typing.Dict[str, typing.Any]:
response = self.session.put(
f"{self.url}/{board_id}/labels/{label_id}",
json={"title": title, "color": color},
)
return response.json()
def delete_label(
self, board_id: IdType, label_id: IdType
) -> typing.Dict[str, typing.Any]:
response = self.session.delete(f"{self.url}/{board_id}/labels/{label_id}")
return response.json()
def get_board_labels(self, board_id: IdType):
boards = self.get_board(board_id)
return boards.labels

Wyświetl plik

@ -5,7 +5,6 @@ import typing
import attr
import cattr
import dateutil
from cattr.preconf.json import make_converter
from dateutil.parser import parse
@ -17,7 +16,7 @@ T = typing.TypeVar("T")
def to_snake(s):
return re.sub("([A-Z]\w+$)", "_\\1", s).lower()
return re.sub(r"([A-Z]\w+$)", "_\\1", s).lower()
def json_to_snake(d):
@ -30,10 +29,10 @@ def json_to_snake(d):
}
def deserialize(model: typing.Generic[T]) -> T:
def deserialize(model: typing.Generic[T]) -> typing.Callable:
def outer(fn):
@functools.wraps(fn)
def inner(*args, **kwargs):
def inner(*args, **kwargs) -> T:
json_ = fn(*args, **kwargs)
json_ = json_to_snake(json_)
return cattr.structure(json_, model)
@ -71,8 +70,8 @@ class Card:
archived: bool
notified: bool = False
deleted_at: int = 0
duedate: str = None
description: str = None
duedate: typing.Optional[str] = None
description: typing.Optional[str] = None
type: str = "plain"
labels: typing.Optional[typing.List[Label]] = attr.Factory(list)

782
poetry.lock wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -2,6 +2,9 @@
name = "nextcloud-deck"
version = "0.1.0"
description = "Python wrapper around Nextcloud Deck API"
readme = "README.md"
repository = "https://github.com/kprestel/nextcloud-deck"
keywords = ["nextcloud"]
authors = ["Kyle Prestel <kprestel@gmail.com>"]
license = "MIT"
packages = [
@ -9,7 +12,7 @@ packages = [
]
[tool.poetry.dependencies]
python = "^3.9"
python = "^3.8"
requests = "^2.25.1"
attrs = "^21.2.0"
cattrs = "^1.7.1"
@ -17,11 +20,12 @@ dateutils = "^0.6.12"
[tool.poetry.dev-dependencies]
pytest = "^6.2.4"
black = "^21.6b0"
black = { version = "*", allow-prereleases = true }
mypy = "^0.902"
isort = "^5.8.0"
tox = "^3.23.1"
tox-poetry = "^0.4.0"
flake8-black = "^0.2.1"
[build-system]
requires = ["poetry>=0.12"]

Wyświetl plik

@ -1,4 +1,3 @@
import cattr
import pytest
from requests.auth import HTTPBasicAuth
@ -9,7 +8,7 @@ from deck.models import Board, Card, Stack
@pytest.fixture(scope="session")
def nc() -> NextCloudDeckAPI:
return NextCloudDeckAPI(
"https://localhost:443", HTTPBasicAuth("kp", "asdf"), ssl_verify=False
"https://localhost:443", HTTPBasicAuth("Admin", "admin"), ssl_verify=False
)
@ -27,6 +26,13 @@ def stack(nc, board: Board):
nc.delete_stack(board_id=board.id, stack_id=s.id)
@pytest.fixture()
def stack2(nc, board: Board):
s = nc.create_stack(board.id, "test stack 2")
yield s
nc.delete_stack(board_id=board.id, stack_id=s.id)
@pytest.fixture()
def card(nc, board, stack):
c = nc.create_card(board_id=board.id, stack_id=stack.id, title="test card fixture")
@ -82,7 +88,7 @@ def test_create_card(nc: NextCloudDeckAPI, board, stack):
def test_create_label(board: Board, nc: NextCloudDeckAPI, stack: Stack, card: Card):
label = nc.create_label(board_id=board.id, title="Test label")
labels = nc.get_labels(board_id=board.id)
labels = nc.get_board_labels(board_id=board.id)
for l in labels:
if l.id == label.id:
assert l == label
@ -113,6 +119,18 @@ def test_create_label(board: Board, nc: NextCloudDeckAPI, stack: Stack, card: Ca
def test_get_cards_from_stack(board, stack, card, nc: NextCloudDeckAPI):
cards = nc.get_cards_from_stack(board_id=board.id, stack_id=stack.id)
assert len(cards) == 1
assert cards[0] == card
def test_reorder_card(board, stack, stack2, card, nc: NextCloudDeckAPI):
assert card.stack_id == stack.id
nc.reorder_card(board.id, stack.id, card.id, card.order - 1, stack2.id)
cards1 = nc.get_cards_from_stack(board.id, stack.id)
cards2 = nc.get_cards_from_stack(board.id, stack2.id)
assert len(cards1) == 0
assert len(cards2) == 1
card_moved = nc.get_card(board.id, stack2.id, card.id)
assert card.title == card_moved.title
assert card.stack_id != card_moved.stack_id
assert card_moved.stack_id == stack2.id

Wyświetl plik

@ -1,6 +1,7 @@
# For more information about tox, see https://tox.readthedocs.io/en/latest/
[tox]
envlist = py37,py38,py39,flake8
envlist = py38,py39,flake8
isolated_build = True
[testenv]
;changedir=tests
@ -16,7 +17,7 @@ deps = flake8
commands = flake8
[flake8]
ignore = F401
ignore = F401,E501,W605
exclude =
.git,
__pycache__,
@ -28,4 +29,6 @@ exclude =
.pytest_cache,
.tox,
*.pyc,
*.egg
*.egg,
venv,
test