From 6e7ee891482956209745993d9e61c0fc24732b1d Mon Sep 17 00:00:00 2001 From: Rick Rongen Date: Thu, 14 Feb 2019 23:19:09 +0100 Subject: [PATCH] Added some comments and basic write support --- Crx/connection.py | 101 +++++++++++++++++++++++++++++++++++--------- Crx/patchbuilder.py | 64 ++++++++++++++++++++++++++++ Crx/simplenode.py | 28 +++++++++++- example.py | 14 ++++++ requirements.txt | 6 ++- 5 files changed, 191 insertions(+), 22 deletions(-) create mode 100644 Crx/patchbuilder.py create mode 100644 example.py diff --git a/Crx/connection.py b/Crx/connection.py index 4c8fcde..65a3c08 100644 --- a/Crx/connection.py +++ b/Crx/connection.py @@ -1,10 +1,10 @@ -from typing import Union, List +from typing import Union, List, Optional import requests from urllib.parse import urljoin from .simplenode import SimpleNode - +from .patchbuilder import PatchBuilder # TODO validation @@ -47,18 +47,38 @@ class Connection: query: str = CRX_QUERY, image_references: str = WCM_REFERENCES): - self.host = f'{protocol}://{host}:{port}' - self.data_root = self.host + root - self.query_path = self.host + query - self.image_references = self.host + image_references + self._host = f'{protocol}://{host}:{port}' + self._data_root = self._host + root + self._query_path = self._host + query + self._image_references = self._host + image_references - self.session = requests.session() + self._session = requests.session() + + self._patch_builder: Optional[PatchBuilder] = None def login_basic(self, username: str, password: str): - self.session.auth = (username, password) + """ + Set the credentials to use for this connection. + + Args: + username: The username to use + password: The password to use + """ + self._session.auth = (username, password) def query(self, query: str, query_type: str = 'SQL2') -> List[str]: - response = self.session.get(self.query_path, params={ + """ + Perform an query and return the matching paths. + Query may be an XPATH, SQL or SQL2 Query + + Args: + query: The query to perform + query_type: The type of the query (defaults to SQL2) + + Returns: + The matching paths of the query + """ + response = self._session.get(self._query_path, params={ '_charset': 'utf-8', 'type': QUERY_TYPES.get(query_type, query_type), 'stmt': query, @@ -71,15 +91,35 @@ class Connection: return list(map(lambda node: node['path'], data['results'])) def get_image_references(self, path: str): - response = self.session.get(self.image_references, params={ + """ + Find all image references for a given image resource. + This uses the DAM Asset Manager > Image > File References tap's backend + + Args: + path: The path of the image to check (no the rendition) + + Returns: + The references of the image (see Chrome/Firefox developer tab for details) + """ + response = self._session.get(self._image_references, params={ 'path': path }) return response.json()['pages'] def get_node_raw(self, path: str): - url = urljoin(self.data_root, '.' + path + JSON_DATA_EXTENSION) + """ + Get the raw JSON dictionary of a node. + This is mostly an internal method. + + Args: + path: The path of the node + + Returns: + A dict representing the node + """ + url = urljoin(self._data_root, '.' + path + JSON_DATA_EXTENSION) try: - response = self.session.get(url) + response = self._session.get(url) except requests.exceptions.RequestException as exception: raise CrxException() # todo more specific exceptions @@ -90,21 +130,44 @@ class Connection: return data + def get_simple_node(self, path: str) -> SimpleNode: + """ + Get a Node as a `SimpleNode` object. + + Args: + path: The path of the node + + Returns: + The SimpleNode object for that path + """ + return SimpleNode(path, self.get_node_raw(path), self) + def download_binary(self, path: str) -> bytes: + """ + Download the binary data of a node. (usually jcr:data). + Usually called via `SimpleNode.download()` + + Args: + path: The path of the node property to download + + Returns: + The binary content of the response + """ # TODO verify if it is not b64 encoded. for some reason it is in FireFox - resp = self.session.get( - urljoin(self.data_root, '.' + path) + resp = self._session.get( + urljoin(self._data_root, '.' + path) ) return resp.content - 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 = self._session.post(self._data_root, data={':diff': diff}) resp.raise_for_status() + def start_patch_builder(self) -> PatchBuilder: + self._patch_builder = PatchBuilder(self) + return self._patch_builder + def apply_diff(self, diff: Union[str, bytes]): files = { ':diff': ( @@ -114,5 +177,5 @@ class Connection: ) } # todo check for exception - resp = self.session.post(self.data_root, files=files) + resp = self._session.post(self._data_root, files=files) resp.raise_for_status() diff --git a/Crx/patchbuilder.py b/Crx/patchbuilder.py new file mode 100644 index 0000000..62fac02 --- /dev/null +++ b/Crx/patchbuilder.py @@ -0,0 +1,64 @@ +from typing import Dict, List, Union +from logging import Logger +import json + +if False: + from .connection import Connection + from .simplenode import SimpleNode + + +LOGGER = Logger(__name__) + + +def _build_patch_set(change: dict) -> str: + line = ['^', change['path'], ' : '] + if change['type'] in ('Long', 'Float', 'Boolean', 'String'): + line.append(json.dumps(change['value'])) + else: + raise ValueError(f'Type {change["type"]!r} is currently not supported!') + + return ''.join(line) + + +_patch_builders = { + 'set': _build_patch_set +} + + +class PatchBuilder: + changes: List[Dict] + + def __init__(self, connection: 'Connection'): + self.connection = connection + self.changes = [] + self.saved = False + + def set_value(self, path: str, value: Union[str, int, float], value_type: str): + self.changes.append({ + 'action': 'set', + 'path': path, + 'value': value, + 'type': value_type + }) + + def save(self): + patch = [] + for entry in self.changes: + patch.append(_patch_builders[entry['action']](entry)) + patch = list(filter(None, patch)) + if len(patch) == 0: + LOGGER.warning("No patch to submit") + return + + self.connection.apply_diff('\n'.join(patch)) + self.saved = True + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self.saved: + LOGGER.warning("Patch not saved") + + if self.connection: + self.connection._patch_builder = None diff --git a/Crx/simplenode.py b/Crx/simplenode.py index 0086fdd..9420193 100644 --- a/Crx/simplenode.py +++ b/Crx/simplenode.py @@ -8,6 +8,8 @@ LOGGER = Logger(__name__) class SimpleNode: + _normal_attrs = ['path', '_data', '_connection'] + def __init__(self, path: str, data: dict, connection: 'Connection'): self.path = path self._data = data @@ -16,13 +18,37 @@ class SimpleNode: def download(self, key: str = 'jcr:data') -> bytes: if ':' + key not in self._data: LOGGER.warning(f"Key :{key} is not present, binary probably not available") - # TODO value of the :{key} must be an int + size = self._data.get(':' + key) + if not isinstance(size, int): + LOGGER.warning("Size is not present") # TODO value denotes file size, warn/deny large files? return self._connection.download_binary(self.path + '/' + key) def __getitem__(self, item): return getattr(self, item) + def __setitem__(self, key, value): + return self.__setattr__(key, value) + + def __setattr__(self, key, value): + if key in self._normal_attrs: + return super(SimpleNode, self).__setattr__(key, value) + if (':' + key) in self._data: + value_type = self._data[':' + key] + if isinstance(value_type, int): + value_type = 'Binary' + else: + if isinstance(value, str): + value_type = 'String' + elif isinstance(value, int): + value_type = 'Long' + elif isinstance(value, bool): + value_type = 'Boolean' + else: + raise ValueError(f"Unknown value type {type(value)!r}") + + self._connection._patch_builder.set_value(self.path + '/' + key, value, value_type) + def __getattr__(self, item: str): try: return super(SimpleNode, self).__getattr__(item) diff --git a/example.py b/example.py new file mode 100644 index 0000000..b4f3611 --- /dev/null +++ b/example.py @@ -0,0 +1,14 @@ +from Crx import Connection, get_simple_con + + +def main(): + con = get_simple_con() + node = con.get_simple_node('/apps/tmp/test') + + with con.start_patch_builder() as ses: + node['banana'] = 'asdf' + ses.save() + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt index 751b266..c11a8e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ requests >= 2.21.0 -pywin32 >= 224 -lxml >= 4.3.0 \ No newline at end of file +lxml >= 4.3.0 + +# For unprotect function +# pywin32 >= 224