PAWpy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PAWpy/Exceptions/Exceptions.py +63 -0
- PAWpy/Exceptions/__init__.py +17 -0
- PAWpy/Objects/Asset.py +51 -0
- PAWpy/Objects/TM1Object.py +29 -0
- PAWpy/Objects/__init__.py +4 -0
- PAWpy/PAWService.py +99 -0
- PAWpy/Services/AdminService.py +39 -0
- PAWpy/Services/BookService.py +43 -0
- PAWpy/Services/ContentService.py +142 -0
- PAWpy/Services/ObjectService.py +14 -0
- PAWpy/Services/RestService.py +343 -0
- PAWpy/Services/TM1ProxyService.py +90 -0
- PAWpy/Services/UIService.py +123 -0
- PAWpy/Services/ViewService.py +36 -0
- PAWpy/Services/__init__.py +19 -0
- PAWpy/Utils/Utils.py +57 -0
- PAWpy/Utils/__init__.py +7 -0
- PAWpy/__init__.py +31 -0
- pawpy-0.1.0.dist-info/METADATA +169 -0
- pawpy-0.1.0.dist-info/RECORD +23 -0
- pawpy-0.1.0.dist-info/WHEEL +5 -0
- pawpy-0.1.0.dist-info/licenses/LICENSE +21 -0
- pawpy-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Exception hierarchy for PAWpy.
|
|
2
|
+
|
|
3
|
+
Mirrors the spirit of TM1py's exception design: a single base exception
|
|
4
|
+
(`PAWException`) with specialised subclasses so callers can catch broadly or
|
|
5
|
+
narrowly. HTTP failures carry the status code, the request method/url, and the
|
|
6
|
+
(possibly truncated) response body to make debugging against a remote PAW
|
|
7
|
+
instance practical.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PAWException(Exception):
|
|
16
|
+
"""Base class for every error raised by PAWpy."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PAWConfigException(PAWException):
|
|
20
|
+
"""Raised when PAWpy is constructed with an invalid/incomplete configuration."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PAWAuthenticationException(PAWException):
|
|
24
|
+
"""Raised when login / token acquisition fails."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PAWTimeoutException(PAWException):
|
|
28
|
+
"""Raised when a request exceeds the configured timeout."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, method: str, url: str, timeout: float):
|
|
31
|
+
self.method = method
|
|
32
|
+
self.url = url
|
|
33
|
+
self.timeout = timeout
|
|
34
|
+
super().__init__(f"Request '{method} {url}' timed out after {timeout}s")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PAWRestException(PAWException):
|
|
38
|
+
"""Raised when PAW returns a non-2xx HTTP status code."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
status_code: int,
|
|
43
|
+
reason: str,
|
|
44
|
+
method: str,
|
|
45
|
+
url: str,
|
|
46
|
+
response_body: Optional[str] = None,
|
|
47
|
+
):
|
|
48
|
+
self.status_code = status_code
|
|
49
|
+
self.reason = reason
|
|
50
|
+
self.method = method
|
|
51
|
+
self.url = url
|
|
52
|
+
self.response_body = response_body
|
|
53
|
+
body_preview = ""
|
|
54
|
+
if response_body:
|
|
55
|
+
body_preview = response_body if len(response_body) <= 2000 else response_body[:2000] + "…"
|
|
56
|
+
body_preview = f"\nResponse body: {body_preview}"
|
|
57
|
+
super().__init__(
|
|
58
|
+
f"PAW REST request failed: {method} {url} -> {status_code} {reason}{body_preview}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PAWNotFoundException(PAWRestException):
|
|
63
|
+
"""Raised on HTTP 404 — the requested asset/resource does not exist."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from PAWpy.Exceptions.Exceptions import (
|
|
2
|
+
PAWException,
|
|
3
|
+
PAWRestException,
|
|
4
|
+
PAWAuthenticationException,
|
|
5
|
+
PAWConfigException,
|
|
6
|
+
PAWNotFoundException,
|
|
7
|
+
PAWTimeoutException,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"PAWException",
|
|
12
|
+
"PAWRestException",
|
|
13
|
+
"PAWAuthenticationException",
|
|
14
|
+
"PAWConfigException",
|
|
15
|
+
"PAWNotFoundException",
|
|
16
|
+
"PAWTimeoutException",
|
|
17
|
+
]
|
PAWpy/Objects/Asset.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Asset — a node in the PAW content store (folder, book/dashboard, view…).
|
|
2
|
+
|
|
3
|
+
Maps the properties documented for the Content Services API
|
|
4
|
+
(``/pacontent/v1/Assets``): ``id``, ``type``, ``name``, ``path``,
|
|
5
|
+
``description``, ``content``, ``custom_properties`` plus the read-only system
|
|
6
|
+
timestamps.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
from PAWpy.Objects.TM1Object import PAWObject
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Asset(PAWObject):
|
|
17
|
+
@property
|
|
18
|
+
def id(self) -> Optional[str]:
|
|
19
|
+
return self._body.get("id")
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def type(self) -> Optional[str]:
|
|
23
|
+
return self._body.get("type")
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def name(self) -> Optional[str]:
|
|
27
|
+
return self._body.get("name")
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def path(self) -> Optional[str]:
|
|
31
|
+
return self._body.get("path")
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def description(self) -> Optional[str]:
|
|
35
|
+
return self._body.get("description")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def content(self) -> Any:
|
|
39
|
+
return self._body.get("content")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def custom_properties(self) -> Dict[str, Any]:
|
|
43
|
+
return self._body.get("custom_properties") or {}
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_folder(self) -> bool:
|
|
47
|
+
return (self._body.get("type") or "").lower() == "folder"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def is_book(self) -> bool:
|
|
51
|
+
return (self._body.get("type") or "").lower() in ("book", "dashboard")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Base object — a light wrapper around the raw JSON body PAW returns.
|
|
2
|
+
|
|
3
|
+
Like TM1py's object layer, every concrete object keeps the original ``body``
|
|
4
|
+
dict around (``raw``) so nothing the API returns is ever lost, while exposing
|
|
5
|
+
the common fields as typed properties.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PAWObject:
|
|
14
|
+
def __init__(self, body: Dict[str, Any]):
|
|
15
|
+
self._body: Dict[str, Any] = dict(body or {})
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def raw(self) -> Dict[str, Any]:
|
|
19
|
+
"""The untouched JSON dict PAW returned for this resource."""
|
|
20
|
+
return self._body
|
|
21
|
+
|
|
22
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
23
|
+
return self._body.get(key, default)
|
|
24
|
+
|
|
25
|
+
def __getitem__(self, key: str) -> Any:
|
|
26
|
+
return self._body[key]
|
|
27
|
+
|
|
28
|
+
def __repr__(self) -> str:
|
|
29
|
+
return f"{self.__class__.__name__}({self._body!r})"
|
PAWpy/PAWService.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""PAWService — the top-level entry point (mirrors TM1py's ``TM1Service``).
|
|
2
|
+
|
|
3
|
+
Construct one ``PAWService`` with connection + auth parameters and reach every
|
|
4
|
+
sub-service through it::
|
|
5
|
+
|
|
6
|
+
with PAWService(host="paw.acme.com", auth_mode="oauth",
|
|
7
|
+
client_id="id", client_secret="secret",
|
|
8
|
+
token_url="https://idp/token") as paw:
|
|
9
|
+
books = paw.books.get_all("/shared/FP&A")
|
|
10
|
+
url = paw.books.get_embed_url("/shared/FP&A/Monthly Report")
|
|
11
|
+
servers = paw.admin.get_tm1_servers()
|
|
12
|
+
data = paw.tm1("Global FPA").execute_mdx("SELECT ...")
|
|
13
|
+
embed = paw.ui.cube_viewer_url("Global FPA", "Revenue Cube", "Monthly View")
|
|
14
|
+
|
|
15
|
+
All keyword arguments are forwarded to :class:`RestService`, which performs
|
|
16
|
+
authentication on construction (unless ``connect=False``).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Dict
|
|
22
|
+
|
|
23
|
+
from PAWpy.Services.AdminService import AdminService, DEFAULT_ADMIN_BASE
|
|
24
|
+
from PAWpy.Services.BookService import BookService
|
|
25
|
+
from PAWpy.Services.ContentService import ContentService, DEFAULT_CONTENT_BASE
|
|
26
|
+
from PAWpy.Services.RestService import RestService
|
|
27
|
+
from PAWpy.Services.TM1ProxyService import TM1ProxyService
|
|
28
|
+
from PAWpy.Services.UIService import UIService
|
|
29
|
+
from PAWpy.Services.ViewService import ViewService
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PAWService:
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
host: str,
|
|
36
|
+
*,
|
|
37
|
+
database: str = None,
|
|
38
|
+
content_base: str = DEFAULT_CONTENT_BASE,
|
|
39
|
+
admin_base: str = DEFAULT_ADMIN_BASE,
|
|
40
|
+
**rest_kwargs,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
:param host: PAW hostname (no scheme), e.g. ``paw.acme.com``.
|
|
44
|
+
:param database: default TM1 database/server for :meth:`tm1` when called
|
|
45
|
+
with no argument.
|
|
46
|
+
:param content_base: base path of the content services API.
|
|
47
|
+
:param admin_base: base path of the admin API.
|
|
48
|
+
:param rest_kwargs: forwarded to :class:`RestService` (auth_mode,
|
|
49
|
+
client_id, port, ssl, verify, timeout, tenant_id, …).
|
|
50
|
+
"""
|
|
51
|
+
self._default_database = database
|
|
52
|
+
self._rest = RestService(host=host, **rest_kwargs)
|
|
53
|
+
|
|
54
|
+
# Core services
|
|
55
|
+
self.content = ContentService(self._rest, content_base=content_base)
|
|
56
|
+
self.ui = UIService(self._rest)
|
|
57
|
+
self.books = BookService(self._rest, self.content, self.ui)
|
|
58
|
+
self.views = ViewService(self._rest, self.content, self.ui)
|
|
59
|
+
self.admin = AdminService(self._rest, admin_base=admin_base)
|
|
60
|
+
|
|
61
|
+
# Cache of per-database TM1 proxy services.
|
|
62
|
+
self._tm1_cache: Dict[str, TM1ProxyService] = {}
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------ #
|
|
65
|
+
# TM1 proxy access
|
|
66
|
+
# ------------------------------------------------------------------ #
|
|
67
|
+
def tm1(self, database: str = None) -> TM1ProxyService:
|
|
68
|
+
"""Return a :class:`TM1ProxyService` for *database* (or the default).
|
|
69
|
+
|
|
70
|
+
Instances are cached per database name so repeated calls are cheap.
|
|
71
|
+
"""
|
|
72
|
+
db = database or self._default_database
|
|
73
|
+
if not db:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"No TM1 database specified and no default 'database' was set on PAWService"
|
|
76
|
+
)
|
|
77
|
+
if db not in self._tm1_cache:
|
|
78
|
+
self._tm1_cache[db] = TM1ProxyService(self._rest, db)
|
|
79
|
+
return self._tm1_cache[db]
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------ #
|
|
82
|
+
# Lifecycle
|
|
83
|
+
# ------------------------------------------------------------------ #
|
|
84
|
+
@property
|
|
85
|
+
def rest(self) -> RestService:
|
|
86
|
+
return self._rest
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def base_url(self) -> str:
|
|
90
|
+
return self._rest.base_url
|
|
91
|
+
|
|
92
|
+
def logout(self) -> None:
|
|
93
|
+
self._rest.logout()
|
|
94
|
+
|
|
95
|
+
def __enter__(self) -> "PAWService":
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
99
|
+
self.logout()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""AdminService — PAW administration REST surface (``/api/v1/admin``).
|
|
2
|
+
|
|
3
|
+
Covers the administrative endpoints exposed by recent PAW builds: the list of
|
|
4
|
+
registered TM1 / database servers, users and groups. IBM documents the broad
|
|
5
|
+
shape (``/api/v1/admin/...``) but the per-build resource names vary, so this
|
|
6
|
+
service offers typed helpers for the common resources **and** a generic
|
|
7
|
+
:meth:`get` escape hatch for anything not yet wrapped.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from PAWpy.Services.ObjectService import ObjectService
|
|
15
|
+
from PAWpy.Services.RestService import RestService
|
|
16
|
+
from PAWpy.Utils.Utils import odata_value_list
|
|
17
|
+
|
|
18
|
+
DEFAULT_ADMIN_BASE = "/api/v1/admin"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AdminService(ObjectService):
|
|
22
|
+
def __init__(self, rest: RestService, admin_base: str = DEFAULT_ADMIN_BASE):
|
|
23
|
+
super().__init__(rest)
|
|
24
|
+
self._base = "/" + admin_base.strip("/")
|
|
25
|
+
|
|
26
|
+
def get(self, resource: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
27
|
+
"""Generic GET against an admin sub-resource (e.g. ``"servers"``)."""
|
|
28
|
+
resp = self._rest.GET(f"{self._base}/{resource.lstrip('/')}", params=params)
|
|
29
|
+
return resp.json()
|
|
30
|
+
|
|
31
|
+
def get_tm1_servers(self) -> List[Dict[str, Any]]:
|
|
32
|
+
"""List the TM1 / database servers registered with this PAW instance."""
|
|
33
|
+
return odata_value_list(self.get("servers"))
|
|
34
|
+
|
|
35
|
+
def get_users(self) -> List[Dict[str, Any]]:
|
|
36
|
+
return odata_value_list(self.get("users"))
|
|
37
|
+
|
|
38
|
+
def get_groups(self) -> List[Dict[str, Any]]:
|
|
39
|
+
return odata_value_list(self.get("groups"))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""BookService — convenience layer over ContentService for PAW books.
|
|
2
|
+
|
|
3
|
+
A "book" is just a content asset of type ``book``/``dashboard``. This service
|
|
4
|
+
filters the content store down to books and pairs each with an embed URL from
|
|
5
|
+
:class:`UIService`, so the common "list the books in a folder and get an iframe
|
|
6
|
+
URL" workflow is one call.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
from PAWpy.Objects.Asset import Asset
|
|
14
|
+
from PAWpy.Services.ContentService import ContentService
|
|
15
|
+
from PAWpy.Services.ObjectService import ObjectService
|
|
16
|
+
from PAWpy.Services.RestService import RestService
|
|
17
|
+
from PAWpy.Services.UIService import UIService
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BookService(ObjectService):
|
|
21
|
+
def __init__(self, rest: RestService, content: ContentService, ui: UIService):
|
|
22
|
+
super().__init__(rest)
|
|
23
|
+
self._content = content
|
|
24
|
+
self._ui = ui
|
|
25
|
+
|
|
26
|
+
def get_all(self, folder_path: str) -> List[Asset]:
|
|
27
|
+
"""List the books/dashboards directly under *folder_path*."""
|
|
28
|
+
children = self._content.list_children(folder_path)
|
|
29
|
+
return [a for a in children if a.is_book]
|
|
30
|
+
|
|
31
|
+
def get(self, path: str, expand_content: bool = False) -> Asset:
|
|
32
|
+
"""Fetch a single book by content-store path."""
|
|
33
|
+
return self._content.get_by_path(path, expand_content=expand_content)
|
|
34
|
+
|
|
35
|
+
def get_embed_url(self, path: str, embed: bool = True) -> str:
|
|
36
|
+
"""Build the ``/ui?type=book`` iframe URL for the book at *path*."""
|
|
37
|
+
return self._ui.book_url(path, embed=embed)
|
|
38
|
+
|
|
39
|
+
def exists(self, path: str) -> bool:
|
|
40
|
+
return self._content.exists(path)
|
|
41
|
+
|
|
42
|
+
def delete(self, path: str) -> None:
|
|
43
|
+
self._content.delete_by_path(path)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""ContentService — CRUD over the PAW Content Services API.
|
|
2
|
+
|
|
3
|
+
Wraps the OData-style ``/pacontent/v1/Assets`` surface documented by IBM:
|
|
4
|
+
|
|
5
|
+
GET /pacontent/v1/Assets('<id>') fetch by id
|
|
6
|
+
GET /pacontent/v1/Assets(path='<path>') fetch by path (encoded x2)
|
|
7
|
+
GET /pacontent/v1/Assets(path='<folder>')/Assets list children
|
|
8
|
+
POST /pacontent/v1/Assets create folder / dashboard
|
|
9
|
+
PUT /pacontent/v1/Assets(id='<id>', type='<t>') update content / props
|
|
10
|
+
DELETE /pacontent/v1/Assets(id='<id>', type='<t>') delete by id+type
|
|
11
|
+
DELETE /pacontent/v1/Assets(path='<path>') delete by path
|
|
12
|
+
|
|
13
|
+
Supports the OData query options ``$filter / $select / $expand / $orderby /
|
|
14
|
+
$top / $skip`` via :func:`PAWpy.Utils.odata_query`.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
from PAWpy.Objects.Asset import Asset
|
|
22
|
+
from PAWpy.Services.ObjectService import ObjectService
|
|
23
|
+
from PAWpy.Services.RestService import RestService
|
|
24
|
+
from PAWpy.Utils.Utils import encode_path_twice, odata_query, odata_value_list
|
|
25
|
+
|
|
26
|
+
# Default base path of the content services API. Override via the constructor
|
|
27
|
+
# for deployments that expose content under a different prefix.
|
|
28
|
+
DEFAULT_CONTENT_BASE = "/pacontent/v1"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ContentService(ObjectService):
|
|
32
|
+
def __init__(self, rest: RestService, content_base: str = DEFAULT_CONTENT_BASE):
|
|
33
|
+
super().__init__(rest)
|
|
34
|
+
self._base = "/" + content_base.strip("/")
|
|
35
|
+
|
|
36
|
+
# ------------------------------------------------------------------ #
|
|
37
|
+
# Read
|
|
38
|
+
# ------------------------------------------------------------------ #
|
|
39
|
+
def get(self, asset_id: str, expand_content: bool = False) -> Asset:
|
|
40
|
+
"""Fetch a single asset by id."""
|
|
41
|
+
params = odata_query(expand="content") if expand_content else None
|
|
42
|
+
resp = self._rest.GET(f"{self._base}/Assets('{asset_id}')", params=params)
|
|
43
|
+
return Asset(resp.json())
|
|
44
|
+
|
|
45
|
+
def get_by_path(self, path: str, expand_content: bool = False) -> Asset:
|
|
46
|
+
"""Fetch a single asset by its content-store path (e.g. ``/shared/FP&A``)."""
|
|
47
|
+
encoded = encode_path_twice(path)
|
|
48
|
+
params = odata_query(expand="content") if expand_content else None
|
|
49
|
+
resp = self._rest.GET(f"{self._base}/Assets(path='{encoded}')", params=params)
|
|
50
|
+
return Asset(resp.json())
|
|
51
|
+
|
|
52
|
+
def list_children(
|
|
53
|
+
self,
|
|
54
|
+
folder_path: str,
|
|
55
|
+
*,
|
|
56
|
+
filter: Optional[str] = None,
|
|
57
|
+
select: Optional[str] = None,
|
|
58
|
+
expand: Optional[str] = None,
|
|
59
|
+
orderby: Optional[str] = None,
|
|
60
|
+
top: Optional[int] = None,
|
|
61
|
+
skip: Optional[int] = None,
|
|
62
|
+
) -> List[Asset]:
|
|
63
|
+
"""List the child assets inside the folder at *folder_path*."""
|
|
64
|
+
encoded = encode_path_twice(folder_path)
|
|
65
|
+
params = odata_query(
|
|
66
|
+
filter=filter, select=select, expand=expand,
|
|
67
|
+
orderby=orderby, top=top, skip=skip,
|
|
68
|
+
)
|
|
69
|
+
resp = self._rest.GET(
|
|
70
|
+
f"{self._base}/Assets(path='{encoded}')/Assets",
|
|
71
|
+
params=params or None,
|
|
72
|
+
)
|
|
73
|
+
return [Asset(item) for item in odata_value_list(resp.json())]
|
|
74
|
+
|
|
75
|
+
# ------------------------------------------------------------------ #
|
|
76
|
+
# Create
|
|
77
|
+
# ------------------------------------------------------------------ #
|
|
78
|
+
def create(
|
|
79
|
+
self,
|
|
80
|
+
type: str,
|
|
81
|
+
name: str,
|
|
82
|
+
path: str,
|
|
83
|
+
content: Any = None,
|
|
84
|
+
custom_properties: Optional[Dict[str, Any]] = None,
|
|
85
|
+
) -> Asset:
|
|
86
|
+
"""Create a new asset (``type`` = ``folder`` or ``dashboard``)."""
|
|
87
|
+
body: Dict[str, Any] = {"type": type, "name": name, "path": path}
|
|
88
|
+
if content is not None:
|
|
89
|
+
body["content"] = content
|
|
90
|
+
if custom_properties is not None:
|
|
91
|
+
body["custom_properties"] = custom_properties
|
|
92
|
+
resp = self._rest.POST(f"{self._base}/Assets", json=body)
|
|
93
|
+
return Asset(resp.json())
|
|
94
|
+
|
|
95
|
+
def create_folder(self, name: str, path: str, **kwargs) -> Asset:
|
|
96
|
+
return self.create("folder", name, path, **kwargs)
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------------------ #
|
|
99
|
+
# Update
|
|
100
|
+
# ------------------------------------------------------------------ #
|
|
101
|
+
def update(
|
|
102
|
+
self,
|
|
103
|
+
asset_id: str,
|
|
104
|
+
type: str,
|
|
105
|
+
content: Any = None,
|
|
106
|
+
custom_properties: Optional[Dict[str, Any]] = None,
|
|
107
|
+
expand_content: bool = True,
|
|
108
|
+
) -> Asset:
|
|
109
|
+
"""Update an asset's ``content`` and/or ``custom_properties``."""
|
|
110
|
+
body: Dict[str, Any] = {}
|
|
111
|
+
if content is not None:
|
|
112
|
+
body["content"] = content
|
|
113
|
+
if custom_properties is not None:
|
|
114
|
+
body["custom_properties"] = custom_properties
|
|
115
|
+
params = odata_query(expand="content") if expand_content else None
|
|
116
|
+
resp = self._rest.PUT(
|
|
117
|
+
f"{self._base}/Assets(id='{asset_id}', type='{type}')",
|
|
118
|
+
json=body,
|
|
119
|
+
params=params,
|
|
120
|
+
)
|
|
121
|
+
return Asset(resp.json())
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------ #
|
|
124
|
+
# Delete
|
|
125
|
+
# ------------------------------------------------------------------ #
|
|
126
|
+
def delete(self, asset_id: str, type: str) -> None:
|
|
127
|
+
self._rest.DELETE(f"{self._base}/Assets(id='{asset_id}', type='{type}')")
|
|
128
|
+
|
|
129
|
+
def delete_by_path(self, path: str) -> None:
|
|
130
|
+
encoded = encode_path_twice(path)
|
|
131
|
+
self._rest.DELETE(f"{self._base}/Assets(path='{encoded}')")
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------ #
|
|
134
|
+
# Convenience
|
|
135
|
+
# ------------------------------------------------------------------ #
|
|
136
|
+
def exists(self, path: str) -> bool:
|
|
137
|
+
from PAWpy.Exceptions.Exceptions import PAWNotFoundException
|
|
138
|
+
try:
|
|
139
|
+
self.get_by_path(path)
|
|
140
|
+
return True
|
|
141
|
+
except PAWNotFoundException:
|
|
142
|
+
return False
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Base for every service — holds the shared RestService handle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PAWpy.Services.RestService import RestService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ObjectService:
|
|
9
|
+
def __init__(self, rest: RestService):
|
|
10
|
+
self._rest = rest
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def rest(self) -> RestService:
|
|
14
|
+
return self._rest
|