Added some comments and basic write support

This commit is contained in:
2019-02-14 23:19:09 +01:00
parent e3319908a1
commit 6e7ee89148
5 changed files with 191 additions and 22 deletions

View File

@@ -1,10 +1,10 @@
from typing import Union, List from typing import Union, List, Optional
import requests import requests
from urllib.parse import urljoin from urllib.parse import urljoin
from .simplenode import SimpleNode from .simplenode import SimpleNode
from .patchbuilder import PatchBuilder
# TODO validation # TODO validation
@@ -47,18 +47,38 @@ class Connection:
query: str = CRX_QUERY, query: str = CRX_QUERY,
image_references: str = WCM_REFERENCES): image_references: str = WCM_REFERENCES):
self.host = f'{protocol}://{host}:{port}' self._host = f'{protocol}://{host}:{port}'
self.data_root = self.host + root self._data_root = self._host + root
self.query_path = self.host + query self._query_path = self._host + query
self.image_references = self.host + image_references 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): 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]: 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', '_charset': 'utf-8',
'type': QUERY_TYPES.get(query_type, query_type), 'type': QUERY_TYPES.get(query_type, query_type),
'stmt': query, 'stmt': query,
@@ -71,15 +91,35 @@ class Connection:
return list(map(lambda node: node['path'], data['results'])) return list(map(lambda node: node['path'], data['results']))
def get_image_references(self, path: str): 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 'path': path
}) })
return response.json()['pages'] return response.json()['pages']
def get_node_raw(self, path: str): 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: try:
response = self.session.get(url) response = self._session.get(url)
except requests.exceptions.RequestException as exception: except requests.exceptions.RequestException as exception:
raise CrxException() # todo more specific exceptions raise CrxException() # todo more specific exceptions
@@ -90,21 +130,44 @@ class Connection:
return data 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: 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 # TODO verify if it is not b64 encoded. for some reason it is in FireFox
resp = self.session.get( resp = self._session.get(
urljoin(self.data_root, '.' + path) urljoin(self._data_root, '.' + path)
) )
return resp.content 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): def rename_node(self, old_path: str, new_path: str):
diff = f'>{old_path} : {new_path}' 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() 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]): def apply_diff(self, diff: Union[str, bytes]):
files = { files = {
':diff': ( ':diff': (
@@ -114,5 +177,5 @@ class Connection:
) )
} }
# todo check for exception # 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() resp.raise_for_status()

64
Crx/patchbuilder.py Normal file
View File

@@ -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

View File

@@ -8,6 +8,8 @@ LOGGER = Logger(__name__)
class SimpleNode: class SimpleNode:
_normal_attrs = ['path', '_data', '_connection']
def __init__(self, path: str, data: dict, connection: 'Connection'): def __init__(self, path: str, data: dict, connection: 'Connection'):
self.path = path self.path = path
self._data = data self._data = data
@@ -16,13 +18,37 @@ class SimpleNode:
def download(self, key: str = 'jcr:data') -> bytes: def download(self, key: str = 'jcr:data') -> bytes:
if ':' + key not in self._data: if ':' + key not in self._data:
LOGGER.warning(f"Key :{key} is not present, binary probably not available") 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? # TODO value denotes file size, warn/deny large files?
return self._connection.download_binary(self.path + '/' + key) return self._connection.download_binary(self.path + '/' + key)
def __getitem__(self, item): def __getitem__(self, item):
return getattr(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): def __getattr__(self, item: str):
try: try:
return super(SimpleNode, self).__getattr__(item) return super(SimpleNode, self).__getattr__(item)

14
example.py Normal file
View File

@@ -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()

View File

@@ -1,3 +1,5 @@
requests >= 2.21.0 requests >= 2.21.0
pywin32 >= 224
lxml >= 4.3.0 lxml >= 4.3.0
# For unprotect function
# pywin32 >= 224