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

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:
_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)

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
pywin32 >= 224
lxml >= 4.3.0
# For unprotect function
# pywin32 >= 224