commit 7654ac95850dddfaf6b2fc62bb340199bae1d4b5 Author: Rick Rongen Date: Tue Feb 5 23:04:30 2019 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e452ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,107 @@ +# Created by .ignore support plugin (hsz.mobi) +### Python template +# 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/ +*.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/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# 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/ +.idea/ diff --git a/Crx/__init__.py b/Crx/__init__.py new file mode 100644 index 0000000..62774b4 --- /dev/null +++ b/Crx/__init__.py @@ -0,0 +1,7 @@ +from .connection import Connection +from .simplenode import SimpleNode +from .util import get_simple_con + +__version__ = (1, 0, 0, 0) + +__all__ = ["Connection", "SimpleNode", "get_simple_con"] diff --git a/Crx/connection.py b/Crx/connection.py new file mode 100644 index 0000000..f555196 --- /dev/null +++ b/Crx/connection.py @@ -0,0 +1,77 @@ +from typing import Union + +import requests +from urllib.parse import urljoin + +from .simplenode import SimpleNode + + +# TODO validation + +""" +http://localhost:4502/crx/de/init.jsp?_dc=1549392939742 +http://localhost:4502/crx/de/nodetypes.jsp?_dc=1549392939958 +http://localhost:4502/crx/server/crx.default/jcr%3aroot/libs.1.json?_dc=1549392123434&node=xnode-265 +http://localhost:4502/crx/de/query.jsp?_dc=1549392245191&_charset_=utf-8&type=xpath&stmt=%2Fjcr%3Aroot%2Fbin%2F%2F*%5Bjcr%3Acontains(.%2C%20%27asdf%27)%5D%20order%20by%20%40jcr%3Ascore&showResults=true +""" + +CRX_SERVER_ROOT = '/crx/server/crx.default/jcr:root/' +CRX_QUERY = '/crx/de/query.jsp' + +JSON_DATA_EXTENSION = '.1.json' + + +class CrxException(ValueError): + pass + + +class Connection: + def __init__(self, + host: str = 'localhost', + port: int = 4502, + protocol: str = 'http', + root: str = CRX_SERVER_ROOT, + query: str = CRX_QUERY): + + self.host = f'{protocol}://{host}:{port}' + self.data_root = self.host + root + self.query_path = self.host + query + + self.session = requests.session() + + def login_basic(self, username: str, password: str): + self.session.auth = (username, password) + + def get_node_raw(self, path: str): + url = urljoin(self.data_root, '.' + path + JSON_DATA_EXTENSION) + try: + response = self.session.get(url) + except requests.exceptions.RequestException as exception: + raise CrxException() # todo more specific exceptions + + try: + data = response.json() + except ValueError: + raise # todo + + return data + + def get_simple_node(self, path: str) -> SimpleNode: + return SimpleNode(path, self.get_node_raw(path), self) + + def rename_node(self, old_path: str, new_path: str): + diff = f'>{old_path} : {new_path}' + resp = self.session.post(self.data_root, data={':diff': diff}) + resp.raise_for_status() + + def apply_diff(self, diff: Union[str, bytes]): + files = { + ':diff': ( + None, + diff, + 'text/plain; charset=utf-8' + ) + } + # todo check for exception + resp = self.session.post(self.data_root, files=files) + resp.raise_for_status() diff --git a/Crx/node.py b/Crx/node.py new file mode 100644 index 0000000..a86da35 --- /dev/null +++ b/Crx/node.py @@ -0,0 +1,95 @@ +from copy import copy, deepcopy +import json +from datetime import datetime + + +def parse_iso_date(date: str) -> datetime: + return datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.000%z') + + +def fmt_iso_date(date: datetime) -> str: + return date.strftime('%Y-%m-%dT%H:%M:%S.000%z') + + +PROPERTY_DEFAULT = { + int: 'LONG', + str: 'STRING', + float: 'DOUBLE', +} + +""" +type: [external, simple, complex] +content-type[complex]: +""" + +PROPERTY_TYPES = { + 'BINARY': { + 'type': 'complex', + 'content-type': 'jcr-value/binary' + }, + 'BOOLEAN': { + 'type': 'simple', + 'serialize': json.dumps + }, + 'DATE': { + 'type': 'complex', + 'content-type': 'jcr-value/date', + 'serialize': fmt_iso_date, + 'deserialize': parse_iso_date + }, + 'DECIMAL': { + 'type': 'complex', + 'content-type': 'jcr-value/decimal', + 'serialize': str, + }, + 'DOUBLE': { + 'type': 'simple', + 'serialize': str, + }, + 'LONG': { + 'type': 'simple', + 'serialize': str, + }, + 'NAME': {}, + 'PATH': {}, + 'REFERENCE': {}, + 'STRING': { + 'type': 'simple', + 'serialize': json.dumps, + 'deserialize': json.loads + }, + 'UNDEFINED': {}, + 'URI': {}, + 'WEAKREFERENCE': {}, +} + + +class Property: + pass + + +class Node: + def __init__(self, path: str, primary_type: str, is_new: bool = True, data: dict = None): + # TODO validate path and type + self.path = path + self.data = data or {} + self.data['jcr:primaryType'] = primary_type + self.original_path = copy(path) + self.original_data = deepcopy(data) + self.is_new = is_new + + def _build_diff(self) -> str: + if self.is_new: + return self._build_diff_new() + else: + return self._build_diff_change() + + def _build_diff_new(self) -> str: + data = [] + ptype_dict = {'jcr:primaryType': self.data['jcr:primaryType']} + data.append(f'+{self.path} : {json.dumps(ptype_dict)}') + for key, value in self.data: + if key == 'jcr:primaryType': + continue # primaryType is handled differently + data.append(f'^{self.path}/{key} : {json.dumps(value)}') + return '\n'.join(data) + '\n' diff --git a/Crx/simplenode.py b/Crx/simplenode.py new file mode 100644 index 0000000..8dbcc9e --- /dev/null +++ b/Crx/simplenode.py @@ -0,0 +1,30 @@ +if False: + from .connection import Connection + + +class SimpleNode: + def __init__(self, path: str, data: dict, connection: 'Connection'): + self.path = path + self._data = data + self._connection = connection + + def __getitem__(self, item): + return getattr(self, item) + + def __getattr__(self, item: str): + try: + return super(SimpleNode, self).__getattr__(item) + except AttributeError: + pass + + try: + value = self._data.get(item) + except KeyError: + raise AttributeError() + + if isinstance(value, dict): + return self._connection.get_simple_node(self.path + '/' + item) + return value + + def __dir__(self): + return super(SimpleNode, self).__dir__() + list(self._data.keys()) diff --git a/Crx/util.py b/Crx/util.py new file mode 100644 index 0000000..78e4d5f --- /dev/null +++ b/Crx/util.py @@ -0,0 +1,7 @@ +from .connection import Connection + + +def get_simple_con(): + con = Connection() + con.login_basic('admin', 'admin') + return con diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..898cd84 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests >= 2.21.0 \ No newline at end of file