Added some comments and basic write support
This commit is contained in:
@@ -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
64
Crx/patchbuilder.py
Normal 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
|
||||||
@@ -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
14
example.py
Normal 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()
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user