albert 1.11.2__py3-none-any.whl → 1.13.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.
albert/__init__.py CHANGED
@@ -4,4 +4,4 @@ from albert.core.auth.sso import AlbertSSOClient
4
4
 
5
5
  __all__ = ["Albert", "AlbertClientCredentials", "AlbertSSOClient"]
6
6
 
7
- __version__ = "1.11.2"
7
+ __version__ = "1.13.0"
@@ -31,6 +31,10 @@ class RequestHandler(BaseHTTPRequestHandler):
31
31
  status = "successful" if self.server.token else "failed (no token found)"
32
32
  self.send_response(200)
33
33
  self.send_header("Content-Type", "text/html")
34
+ self.send_header(
35
+ "Content-Security-Policy",
36
+ "default-src 'none'; frame-ancestors 'none'; base-uri 'none';",
37
+ )
34
38
  self.end_headers()
35
39
  self.wfile.write(
36
40
  f"""
@@ -38,8 +42,6 @@ class RequestHandler(BaseHTTPRequestHandler):
38
42
  <body>
39
43
  <h1>Authentication {status}</h1>
40
44
  <p>You can close this window now.</p>
41
- <script>window.close()</script>
42
- <button onclick="window.close()">Close Window</button>
43
45
  </body>
44
46
  </html>
45
47
  """.encode()
@@ -14,6 +14,7 @@ from albert.resources._mixins import HydrationMixin
14
14
  from albert.resources.acls import ACL
15
15
  from albert.resources.cas import Cas
16
16
  from albert.resources.companies import Company
17
+ from albert.resources.lists import ListItem
17
18
  from albert.resources.locations import Location
18
19
  from albert.resources.tagged_base import BaseTaggedResource
19
20
  from albert.resources.tags import Tag
@@ -75,6 +76,8 @@ class CasAmount(BaseAlbertModel):
75
76
  The SMILES string of the CAS Number resource. Obtained from the Cas object when provided.
76
77
  number: str | None
77
78
  The CAS number. Obtained from the Cas object when provided.
79
+ inventory_function: list[ListItem | EntityLink | str] | None
80
+ Business-controlled functions associated with the CAS in this inventory context.
78
81
 
79
82
  !!! tip
80
83
  ---
@@ -87,6 +90,9 @@ class CasAmount(BaseAlbertModel):
87
90
  target: float | None = Field(default=None, alias="inventoryValue")
88
91
  id: str | None = Field(default=None)
89
92
  cas_category: str | None = Field(default=None, alias="casCategory")
93
+ inventory_function: list[SerializeAsEntityLink[ListItem] | str] | None = Field(
94
+ default=None, alias="inventoryFunction"
95
+ )
90
96
  type: str | None = Field(default=None)
91
97
  classification_type: str | None = Field(default=None, alias="classificationType")
92
98
 
albert/resources/lists.py CHANGED
@@ -25,9 +25,11 @@ class ListItem(BaseResource):
25
25
  id : str | None
26
26
  The Albert ID of the list item. Set when the list item is retrieved from Albert.
27
27
  category : ListItemCategory | None
28
- The category of the list item. Allowed values are `businessDefined`, `userDefined`, `projects`, and `extensions`.
28
+ The category of the list item. Allowed values are `businessDefined`, `userDefined`, `projects`, `extensions`,
29
+ and `inventory`.
29
30
  list_type : str | None
30
- The type of the list item. Allowed values are `projectState` for `projects` and `extensions` for `extensions`.
31
+ The type of the list item. Allowed values are `projectState` for `projects`, `extensions` for `extensions`,
32
+ and `casCategory` or `inventoryFunction` for `inventory`.
31
33
  """
32
34
 
33
35
  name: str
@@ -37,14 +39,15 @@ class ListItem(BaseResource):
37
39
 
38
40
  @model_validator(mode="after")
39
41
  def validate_list_type(self) -> ListItem:
42
+ allowed_by_category = {
43
+ ListItemCategory.PROJECTS: {"projectState"},
44
+ ListItemCategory.EXTENSIONS: {"extensions"},
45
+ ListItemCategory.INVENTORY: {"casCategory", "inventoryFunction"},
46
+ }
40
47
  if (
41
- self.category == ListItemCategory.PROJECTS
42
- and self.list_type is not None
43
- and self.list_type != "projectState"
44
- ) or (
45
- self.category == ListItemCategory.EXTENSIONS
46
- and self.list_type is not None
47
- and self.list_type != "extensions"
48
+ self.list_type is not None
49
+ and self.category in allowed_by_category
50
+ and self.list_type not in allowed_by_category[self.category]
48
51
  ):
49
52
  raise ValueError(
50
53
  f"List type {self.list_type} is not allowed for category {self.category}"
albert/utils/inventory.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from collections.abc import Iterable
2
2
  from typing import Any
3
3
 
4
+ from albert.core.shared.models.base import BaseResource, EntityLink
4
5
  from albert.resources.inventory import CasAmount
5
6
 
6
7
 
@@ -56,6 +57,61 @@ def _build_cas_delete_operation(identifier: str) -> dict[str, Any]:
56
57
  }
57
58
 
58
59
 
60
+ def _normalize_inventory_function_ids(
61
+ value: list[BaseResource | EntityLink | str] | None,
62
+ ) -> list[str]:
63
+ if not value:
64
+ return []
65
+ ids: list[str] = []
66
+ for item in value:
67
+ if isinstance(item, str):
68
+ if item:
69
+ ids.append(item)
70
+ continue
71
+ if isinstance(item, BaseResource):
72
+ if item.id:
73
+ ids.append(item.id)
74
+ continue
75
+ if isinstance(item, EntityLink):
76
+ if item.id:
77
+ ids.append(item.id)
78
+ continue
79
+ return ids
80
+
81
+
82
+ def _build_inventory_function_operations(
83
+ *,
84
+ entity_id: str,
85
+ existing: list[BaseResource | EntityLink | str] | None,
86
+ updated: list[BaseResource | EntityLink | str] | None,
87
+ ) -> list[dict[str, Any]]:
88
+ existing_ids = set(_normalize_inventory_function_ids(existing))
89
+ updated_ids = set(_normalize_inventory_function_ids(updated))
90
+ to_add = sorted(updated_ids - existing_ids)
91
+ to_delete = sorted(existing_ids - updated_ids)
92
+
93
+ operations: list[dict[str, Any]] = []
94
+ if to_add:
95
+ operations.append(
96
+ {
97
+ "attribute": "inventoryFunction",
98
+ "entityId": entity_id,
99
+ "operation": "add",
100
+ "newValue": to_add,
101
+ }
102
+ )
103
+ if to_delete:
104
+ operations.append(
105
+ {
106
+ "attribute": "inventoryFunction",
107
+ "entityId": entity_id,
108
+ "operation": "delete",
109
+ "oldValue": to_delete,
110
+ }
111
+ )
112
+ return operations
113
+
114
+
59
115
  def _build_cas_scalar_operation(
60
116
  *,
61
117
  attribute: str,
@@ -117,6 +173,14 @@ def _build_cas_update_operations(existing: CasAmount, updated: CasAmount) -> lis
117
173
  if operation is not None:
118
174
  operations.append(operation)
119
175
 
176
+ operations.extend(
177
+ _build_inventory_function_operations(
178
+ entity_id=identifier,
179
+ existing=existing.inventory_function,
180
+ updated=updated.inventory_function,
181
+ )
182
+ )
183
+
120
184
  return operations
121
185
 
122
186
 
@@ -159,6 +223,13 @@ def _build_cas_patch_operations(
159
223
  )
160
224
  if target_operation is not None:
161
225
  operations.append(target_operation)
226
+ operations.extend(
227
+ _build_inventory_function_operations(
228
+ entity_id=identifier,
229
+ existing=None,
230
+ updated=cas_amount.inventory_function,
231
+ )
232
+ )
162
233
 
163
234
  removals = [existing_lookup[key] for key in existing_lookup.keys() - updated_lookup.keys()]
164
235
  for cas_amount in removals:
@@ -2,7 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import ast
6
+ import math
5
7
  import mimetypes
8
+ import operator
6
9
  import re
7
10
  import uuid
8
11
  from collections.abc import Callable
@@ -573,6 +576,63 @@ def get_all_columns_used_in_calculations(*, first_row_data_column: list):
573
576
  return used_columns
574
577
 
575
578
 
579
+ _ALLOWED_BINOPS = {
580
+ ast.Add: operator.add,
581
+ ast.Sub: operator.sub,
582
+ ast.Mult: operator.mul,
583
+ ast.Div: operator.truediv,
584
+ ast.Mod: operator.mod,
585
+ ast.Pow: operator.pow,
586
+ }
587
+ _ALLOWED_UNARYOPS = {
588
+ ast.UAdd: operator.pos,
589
+ ast.USub: operator.neg,
590
+ }
591
+ _ALLOWED_FUNCS: dict[str, tuple[Callable[..., float], int]] = {
592
+ "log10": (math.log10, 1),
593
+ "ln": (math.log, 1),
594
+ "sqrt": (math.sqrt, 1),
595
+ "pi": (lambda: math.pi, 0),
596
+ }
597
+ _ALLOWED_NAMES = {"pi": math.pi}
598
+
599
+
600
+ def _safe_eval_math(*, expression: str) -> float:
601
+ """Safely evaluate supported math expressions."""
602
+ parsed = ast.parse(expression, mode="eval")
603
+
604
+ def _eval(node: ast.AST) -> float:
605
+ if isinstance(node, ast.Expression):
606
+ return _eval(node.body)
607
+ if isinstance(node, ast.Constant) and isinstance(node.value, (int | float)):
608
+ return node.value
609
+ if isinstance(node, ast.BinOp) and type(node.op) in _ALLOWED_BINOPS:
610
+ return _ALLOWED_BINOPS[type(node.op)](_eval(node.left), _eval(node.right))
611
+ if isinstance(node, ast.UnaryOp) and type(node.op) in _ALLOWED_UNARYOPS:
612
+ return _ALLOWED_UNARYOPS[type(node.op)](_eval(node.operand))
613
+ if isinstance(node, ast.Call):
614
+ if not isinstance(node.func, ast.Name):
615
+ raise ValueError("Unsupported function call.")
616
+ func_name = node.func.id
617
+ if func_name not in _ALLOWED_FUNCS:
618
+ raise ValueError("Unsupported function.")
619
+ if node.keywords:
620
+ raise ValueError("Keyword arguments are not supported.")
621
+ func, arity = _ALLOWED_FUNCS[func_name]
622
+ if len(node.args) != arity:
623
+ raise ValueError("Unsupported function arity.")
624
+ if arity == 0:
625
+ return func()
626
+ return func(_eval(node.args[0]))
627
+ if isinstance(node, ast.Name):
628
+ if node.id in _ALLOWED_NAMES:
629
+ return _ALLOWED_NAMES[node.id]
630
+ raise ValueError("Unsupported name.")
631
+ raise ValueError("Unsupported expression.")
632
+
633
+ return _eval(parsed)
634
+
635
+
576
636
  def evaluate_calculation(*, calculation: str, column_values: dict) -> float | None:
577
637
  """Evaluate a calculation expression against column values."""
578
638
  calculation = calculation.lstrip("=")
@@ -589,7 +649,7 @@ def evaluate_calculation(*, calculation: str, column_values: dict) -> float | No
589
649
  calculation = pattern.sub(repl, calculation)
590
650
 
591
651
  calculation = calculation.replace("^", "**")
592
- return eval(calculation)
652
+ return _safe_eval_math(expression=calculation)
593
653
  except Exception as e:
594
654
  logger.info(
595
655
  "Error evaluating calculation '%s': %s. Likely do not have all values needed.",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: albert
3
- Version: 1.11.2
3
+ Version: 1.13.0
4
4
  Summary: The official Python SDK for the Albert Invent platform.
5
5
  Project-URL: Homepage, https://www.albertinvent.com/
6
6
  Project-URL: Documentation, https://docs.developer.albertinvent.com/albert-python
@@ -1,4 +1,4 @@
1
- albert/__init__.py,sha256=w6OATl6C5XTZwB_fGPlL2-I1i3fMb3WkBFdxI2uKTSc,239
1
+ albert/__init__.py,sha256=GFvvmokCWEGy95K7pNqV4Rme5Dfut9E7_lnKxNTtL1o,239
2
2
  albert/client.py,sha256=9SUy9AJpnFEUlSfxvbP1gJI776WcOoZykgPHx0EcF8g,12038
3
3
  albert/exceptions.py,sha256=-oxOJGE0A__aPUhri3qqb5YQ5qanECcTqamS73vGajM,3172
4
4
  albert/collections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -51,7 +51,7 @@ albert/core/logging.py,sha256=sqNbIC3CZyaTyLnoV9mn0NCkxKH-jUNDJkAVMxgPSFY,820
51
51
  albert/core/pagination.py,sha256=aK8wUHknptP0lfLoNT5qsxlkR8UA0xXghZroFzgA2rY,4440
52
52
  albert/core/session.py,sha256=kCeTsjc4k-6bwqByc3R-tpG1ikvc17a_IRBKnflrCYY,3860
53
53
  albert/core/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
- albert/core/auth/_listener.py,sha256=Vinmz3vr2COjdN7tQHUz63J3COZZd9QAce9GBUJE2cM,2261
54
+ albert/core/auth/_listener.py,sha256=0RJnqofGrAQWkQ_UMWzk68_wGkzLmeX07Yn6NqWAeck,2293
55
55
  albert/core/auth/_manager.py,sha256=g4PUxADWJTTfwEP0-ob33ckjU_6mrFOBA4MWxaulsv4,1061
56
56
  albert/core/auth/credentials.py,sha256=uaN4RFIUsnCKPHgLnaZbNe9p-hhEDMOU6naE6vSrp8M,4234
57
57
  albert/core/auth/sso.py,sha256=4NEDN8wVT1fXvPEEgJNj20VCb5P5XZqG_6b8ovmloMI,7526
@@ -80,9 +80,9 @@ albert/resources/entity_types.py,sha256=7lessP8StBDfi4DZMifvRNN4lspdQ5X0olBIu7lT
80
80
  albert/resources/facet.py,sha256=tjm6CsOL7T5LSV4G9406C1c5mpzY1gpfQYAttP6Hckw,372
81
81
  albert/resources/files.py,sha256=0q5H6rJ9il0Oy1FnwxVo3otq_w8YQeQjo29DgsTY8JQ,1067
82
82
  albert/resources/hazards.py,sha256=ltlbYct3PhvpTojqic0OcfJVKd2s6m59XqszzZZAuWo,375
83
- albert/resources/inventory.py,sha256=f0f8WGEGbbDgXUe9FoipsVsEmJkeuIKEId0WuEV_QXc,13608
83
+ albert/resources/inventory.py,sha256=xkPZDOR-KFB79We56K4A35Td_L3kqe2OJ8Tvzrj1vQE,13944
84
84
  albert/resources/links.py,sha256=te7KIO7vfXrA-Qjngvb4JHy--SUVTG5M7GBFz8XdKto,1032
85
- albert/resources/lists.py,sha256=-ViW_wHJbaQh47zXNr1eszOMmwSFX-nQWTfOA8BzOG8,1687
85
+ albert/resources/lists.py,sha256=UinEaBEEo80Q1Vf5Ys4ViPcNTSvAeCRaZiW41CHY4f0,1861
86
86
  albert/resources/locations.py,sha256=xqbSoO-IjLSyK74W1jSdsNL9kUN5jD6Wf9XNxw8czYA,821
87
87
  albert/resources/lots.py,sha256=8YC6fHv08uYvhh09ULzNtFmdyfLeM1z6Hkm9wcTvcX4,6839
88
88
  albert/resources/notebooks.py,sha256=PebrXEbR8J9afqwwTwGjFlEkYCdqRh3AtvVwYMypex8,9581
@@ -114,10 +114,10 @@ albert/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
114
114
  albert/utils/_auth.py,sha256=YjzaGIzI9qP53nwdyE2Ezs-9UokzA38kgdE7Sxnyjd8,124
115
115
  albert/utils/_patch.py,sha256=e-bD2x6W3Wt4FaKFK477h3kZeyucn3DEB9m5DR7LzaA,24273
116
116
  albert/utils/data_template.py,sha256=AUwzfQ-I2HY-osq_Tme5KLwXfMzW2pJpiud7HAMh148,27874
117
- albert/utils/inventory.py,sha256=ViHxb62DVxniEJqOfD7Gf8HltLeCbq21y4rS1xkVRnY,5349
118
- albert/utils/property_data.py,sha256=US_5PYZu2Yv3CmAuWMQfYztjSIgNaRrCz9SsVzLOGcw,22906
117
+ albert/utils/inventory.py,sha256=hhL1wCn2vtF2z5FGwlX-3XIla4paB26QbmbStkG3yTQ,7433
118
+ albert/utils/property_data.py,sha256=xb0CZAh_0zFiN5yQwcw3Mjk4En8MxMrJF9DKQI19UhM,25074
119
119
  albert/utils/tasks.py,sha256=ejhXD9yQR_ReDKLJ3k8ZxWxkYl-Mta_4OPXrz_lQiBI,19878
120
- albert-1.11.2.dist-info/METADATA,sha256=zBwfJkO8ubUMm-06Wj3wA8c4eE97E_LbUj5K03bEKY4,15427
121
- albert-1.11.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
122
- albert-1.11.2.dist-info/licenses/LICENSE,sha256=S7_vRdIhQmG7PmTlU8-BCCveuEcFZ6_3IUVdcoaJMuA,11348
123
- albert-1.11.2.dist-info/RECORD,,
120
+ albert-1.13.0.dist-info/METADATA,sha256=9QEz1ieKLpVnsyjDhJdyfZ0BfYZOpn6aJAP-3vdYCAU,15427
121
+ albert-1.13.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
122
+ albert-1.13.0.dist-info/licenses/LICENSE,sha256=S7_vRdIhQmG7PmTlU8-BCCveuEcFZ6_3IUVdcoaJMuA,11348
123
+ albert-1.13.0.dist-info/RECORD,,