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
|
||||
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
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:
|
||||
_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
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
|
||||
pywin32 >= 224
|
||||
lxml >= 4.3.0
|
||||
|
||||
# For unprotect function
|
||||
# pywin32 >= 224
|
||||
|
||||
Reference in New Issue
Block a user