surfdataverse 4.0.0__tar.gz → 4.0.2__tar.gz
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.
- {surfdataverse-4.0.0 → surfdataverse-4.0.2}/PKG-INFO +5 -5
- {surfdataverse-4.0.0 → surfdataverse-4.0.2}/README.md +4 -4
- {surfdataverse-4.0.0 → surfdataverse-4.0.2}/pyproject.toml +1 -1
- {surfdataverse-4.0.0 → surfdataverse-4.0.2}/src/surfdataverse/container.py +4 -4
- {surfdataverse-4.0.0 → surfdataverse-4.0.2}/src/surfdataverse/core.py +43 -42
- {surfdataverse-4.0.0 → surfdataverse-4.0.2}/src/surfdataverse/__init__.py +0 -0
- {surfdataverse-4.0.0 → surfdataverse-4.0.2}/src/surfdataverse/exceptions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: surfdataverse
|
|
3
|
-
Version: 4.0.
|
|
3
|
+
Version: 4.0.2
|
|
4
4
|
Summary: A Python package for ionysis Microsoft Dataverse integration
|
|
5
5
|
Keywords: dataverse,microsoft,crm,api
|
|
6
6
|
Author: ionysis
|
|
@@ -76,8 +76,8 @@ client.get_authenticated_session()
|
|
|
76
76
|
client.test_connection()
|
|
77
77
|
|
|
78
78
|
# Create entities/tables using modern DI factory functions
|
|
79
|
-
product = connect_entity("logical_table_name_1"
|
|
80
|
-
table_reader = connect_table("logical_table_name_1"
|
|
79
|
+
product = connect_entity("logical_table_name_1")
|
|
80
|
+
table_reader = connect_table("logical_table_name_1")
|
|
81
81
|
|
|
82
82
|
# Set data using write() method with automatic type detection
|
|
83
83
|
product.write("prefix_name", "My Product")
|
|
@@ -167,7 +167,7 @@ default_entity = connect_entity("prefix_tablename", "prefix_")
|
|
|
167
167
|
custom_entity = connect_entity("myorg_product", "myorg_")
|
|
168
168
|
|
|
169
169
|
# For reading data
|
|
170
|
-
table_reader = connect_table("prefix_tablename"
|
|
170
|
+
table_reader = connect_table("prefix_tablename")
|
|
171
171
|
|
|
172
172
|
# The system automatically:
|
|
173
173
|
# - Filters columns starting with your prefix
|
|
@@ -183,7 +183,7 @@ Fetch data from Dataverse tables as pandas DataFrames using modern DI patterns:
|
|
|
183
183
|
from surfdataverse import connect_table, get_client
|
|
184
184
|
|
|
185
185
|
# Create table reader using dependency injection
|
|
186
|
-
table_reader = connect_table("logical_table_name_1"
|
|
186
|
+
table_reader = connect_table("logical_table_name_1")
|
|
187
187
|
|
|
188
188
|
# Get table data as pandas DataFrame
|
|
189
189
|
df = table_reader.get_table_data()
|
|
@@ -47,8 +47,8 @@ client.get_authenticated_session()
|
|
|
47
47
|
client.test_connection()
|
|
48
48
|
|
|
49
49
|
# Create entities/tables using modern DI factory functions
|
|
50
|
-
product = connect_entity("logical_table_name_1"
|
|
51
|
-
table_reader = connect_table("logical_table_name_1"
|
|
50
|
+
product = connect_entity("logical_table_name_1")
|
|
51
|
+
table_reader = connect_table("logical_table_name_1")
|
|
52
52
|
|
|
53
53
|
# Set data using write() method with automatic type detection
|
|
54
54
|
product.write("prefix_name", "My Product")
|
|
@@ -138,7 +138,7 @@ default_entity = connect_entity("prefix_tablename", "prefix_")
|
|
|
138
138
|
custom_entity = connect_entity("myorg_product", "myorg_")
|
|
139
139
|
|
|
140
140
|
# For reading data
|
|
141
|
-
table_reader = connect_table("prefix_tablename"
|
|
141
|
+
table_reader = connect_table("prefix_tablename")
|
|
142
142
|
|
|
143
143
|
# The system automatically:
|
|
144
144
|
# - Filters columns starting with your prefix
|
|
@@ -154,7 +154,7 @@ Fetch data from Dataverse tables as pandas DataFrames using modern DI patterns:
|
|
|
154
154
|
from surfdataverse import connect_table, get_client
|
|
155
155
|
|
|
156
156
|
# Create table reader using dependency injection
|
|
157
|
-
table_reader = connect_table("logical_table_name_1"
|
|
157
|
+
table_reader = connect_table("logical_table_name_1")
|
|
158
158
|
|
|
159
159
|
# Get table data as pandas DataFrame
|
|
160
160
|
df = table_reader.get_table_data()
|
|
@@ -34,14 +34,14 @@ def get_client(config_path=None) -> DataverseClient:
|
|
|
34
34
|
return container.client()
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def connect_table(logical_name: str = ""
|
|
37
|
+
def connect_table(logical_name: str = "") -> DataverseTable:
|
|
38
38
|
"""Connect to a Dataverse table"""
|
|
39
|
-
return container.table_factory(logical_name
|
|
39
|
+
return container.table_factory(logical_name)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def connect_entity(table_logical_name: str
|
|
42
|
+
def connect_entity(table_logical_name: str) -> DataverseEntity:
|
|
43
43
|
"""Connect to a new Dataverse entity instance"""
|
|
44
|
-
return container.entity_factory(table_logical_name
|
|
44
|
+
return container.entity_factory(table_logical_name)
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def reset_container():
|
|
@@ -7,7 +7,7 @@ from decimal import Decimal
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
|
-
import msal
|
|
10
|
+
import msal # type: ignore
|
|
11
11
|
import pandas as pd
|
|
12
12
|
import requests
|
|
13
13
|
|
|
@@ -194,7 +194,7 @@ class DataverseClient:
|
|
|
194
194
|
class DataverseBase:
|
|
195
195
|
"""Base class for Dataverse operations with shared functionality"""
|
|
196
196
|
|
|
197
|
-
def __init__(self, table_logical_name: str = "",
|
|
197
|
+
def __init__(self, table_logical_name: str = "", client=None):
|
|
198
198
|
if client is None:
|
|
199
199
|
raise ValueError(
|
|
200
200
|
"Client is required. Use connect_entity() or connect_table() instead of direct instantiation."
|
|
@@ -202,20 +202,16 @@ class DataverseBase:
|
|
|
202
202
|
self.client = client
|
|
203
203
|
|
|
204
204
|
self.table_logical_name = table_logical_name
|
|
205
|
-
self.
|
|
206
|
-
self.name = (
|
|
207
|
-
table_logical_name.replace(self.table_logical_name_prefix, "").title().replace("_", " ")
|
|
208
|
-
)
|
|
205
|
+
self.name = table_logical_name.title().replace("_", " ")
|
|
209
206
|
|
|
210
207
|
# Stored values for this session
|
|
211
|
-
self._guid_mapping = {}
|
|
212
|
-
self._all_relationships = {} # Populated by first call
|
|
213
|
-
self._global_choices = {} # Populated by first call
|
|
208
|
+
self._guid_mapping: dict[str, dict] = {}
|
|
209
|
+
self._all_relationships: dict = {} # Populated by first call
|
|
210
|
+
self._global_choices: dict = {} # Populated by first call
|
|
214
211
|
|
|
215
|
-
self._table_metadata = {}
|
|
216
|
-
self._table_definitions = {}
|
|
217
|
-
self._table_relationships = {}
|
|
218
|
-
self._filtered_choices = {}
|
|
212
|
+
self._table_metadata: dict = {}
|
|
213
|
+
self._table_definitions: dict = {}
|
|
214
|
+
self._table_relationships: dict = {}
|
|
219
215
|
|
|
220
216
|
# === RELATIONSHIPS
|
|
221
217
|
@property
|
|
@@ -278,17 +274,6 @@ class DataverseBase:
|
|
|
278
274
|
|
|
279
275
|
return self._global_choices
|
|
280
276
|
|
|
281
|
-
@property
|
|
282
|
-
def filtered_choices(self):
|
|
283
|
-
"""Get choices filtered by table prefix"""
|
|
284
|
-
if not self._filtered_choices:
|
|
285
|
-
self._filtered_choices = {
|
|
286
|
-
name: options
|
|
287
|
-
for name, options in self.global_choices.items()
|
|
288
|
-
if self.table_logical_name_prefix in name
|
|
289
|
-
}
|
|
290
|
-
return self._filtered_choices
|
|
291
|
-
|
|
292
277
|
# === TABLES
|
|
293
278
|
def get_table_definitions(self):
|
|
294
279
|
if not self._table_definitions:
|
|
@@ -376,7 +361,7 @@ class DataverseBase:
|
|
|
376
361
|
raise DataverseAPIError(error_msg, response.status_code, response.text)
|
|
377
362
|
|
|
378
363
|
# === HELPER METHODS
|
|
379
|
-
def name_to_guid(self, table_name, name=
|
|
364
|
+
def name_to_guid(self, table_name, name: str="", name_column=None) -> str | None:
|
|
380
365
|
"""
|
|
381
366
|
Fetches a mapping of unique names to GUIDs in a Dataverse table
|
|
382
367
|
|
|
@@ -683,14 +668,13 @@ class DataverseTable(DataverseBase):
|
|
|
683
668
|
class DataverseEntity(DataverseBase):
|
|
684
669
|
"""Generic class for Dataverse entities"""
|
|
685
670
|
|
|
686
|
-
def __init__(self, table_logical_name,
|
|
671
|
+
def __init__(self, table_logical_name, client=None):
|
|
687
672
|
"""
|
|
688
673
|
Args:
|
|
689
674
|
table_logical_name: The pluralized name of the Dataverse table
|
|
690
|
-
table_prefix: The prefix used for custom table/column names (default: "prefix_")
|
|
691
675
|
client: Optional DataverseClient instance (will create singleton if not provided)
|
|
692
676
|
"""
|
|
693
|
-
super().__init__(table_logical_name,
|
|
677
|
+
super().__init__(table_logical_name, client)
|
|
694
678
|
|
|
695
679
|
self.guid = None
|
|
696
680
|
self.data = {} # Dictionary to store record fields
|
|
@@ -716,14 +700,25 @@ class DataverseEntity(DataverseBase):
|
|
|
716
700
|
field_type = metadata[column].get("AttributeType")
|
|
717
701
|
|
|
718
702
|
# File columns (check first since they can have special handling)
|
|
719
|
-
if field_type
|
|
703
|
+
if field_type in ["File", "Image", "Virtual"]:
|
|
720
704
|
self.set_file(column, value)
|
|
721
|
-
|
|
722
|
-
elif
|
|
705
|
+
|
|
706
|
+
elif field_type == "Lookup":
|
|
707
|
+
if column not in self.get_table_relationships():
|
|
708
|
+
raise EntityError(
|
|
709
|
+
f"Crosscheck: Column {column} is not a valid lookup for this entity despite "
|
|
710
|
+
f"being lookup"
|
|
711
|
+
)
|
|
723
712
|
self.set_lookup(column, value)
|
|
724
|
-
|
|
725
|
-
elif
|
|
713
|
+
|
|
714
|
+
elif field_type == "Picklist":
|
|
715
|
+
if column not in self.global_choices:
|
|
716
|
+
raise EntityError(
|
|
717
|
+
f"Crosscheck: Column {column} is not a valid choice for this entity despite "
|
|
718
|
+
f"being picklist"
|
|
719
|
+
)
|
|
726
720
|
self.set_choice(column, value)
|
|
721
|
+
|
|
727
722
|
# Regular data columns (including DateTime, String, Integer, Decimal, etc.)
|
|
728
723
|
else:
|
|
729
724
|
self.set_data(column, value)
|
|
@@ -762,7 +757,7 @@ class DataverseEntity(DataverseBase):
|
|
|
762
757
|
elif column in self.get_table_relationships():
|
|
763
758
|
return self.get_lookup(column)
|
|
764
759
|
# Choice columns (picklist/option sets)
|
|
765
|
-
elif column in self.
|
|
760
|
+
elif column in self.global_choices:
|
|
766
761
|
return self.get_choice(column)
|
|
767
762
|
# Regular data columns (including DateTime, String, Integer, Decimal, etc.)
|
|
768
763
|
else:
|
|
@@ -793,8 +788,7 @@ class DataverseEntity(DataverseBase):
|
|
|
793
788
|
|
|
794
789
|
# Update our data with the fresh record
|
|
795
790
|
for field, fresh_value in record.items():
|
|
796
|
-
|
|
797
|
-
self.data[field] = fresh_value
|
|
791
|
+
self.data[field] = fresh_value
|
|
798
792
|
|
|
799
793
|
logger.info(f"Refreshed data for {self.table_logical_name} GUID: {self.guid}")
|
|
800
794
|
return True
|
|
@@ -837,8 +831,7 @@ class DataverseEntity(DataverseBase):
|
|
|
837
831
|
|
|
838
832
|
# Update our data with the fresh record
|
|
839
833
|
for field, fresh_value in record.items():
|
|
840
|
-
|
|
841
|
-
self.data[field] = fresh_value
|
|
834
|
+
self.data[field] = fresh_value
|
|
842
835
|
|
|
843
836
|
# Return the requested value
|
|
844
837
|
value = self.data.get(column, "")
|
|
@@ -859,7 +852,10 @@ class DataverseEntity(DataverseBase):
|
|
|
859
852
|
target_entity_set_name = self.get_table_entity_set_name(logical_name=target_entity)
|
|
860
853
|
|
|
861
854
|
if not is_valid_guid(value):
|
|
862
|
-
|
|
855
|
+
guid_result = self.name_to_guid(target_entity, value)
|
|
856
|
+
if guid_result is None:
|
|
857
|
+
raise EntityError(f"Could not find GUID for lookup value '{value}'")
|
|
858
|
+
value = guid_result
|
|
863
859
|
|
|
864
860
|
self.data[f"{lookup_column_schema_name}@odata.bind"] = f"/{target_entity_set_name}({value})"
|
|
865
861
|
|
|
@@ -875,7 +871,7 @@ class DataverseEntity(DataverseBase):
|
|
|
875
871
|
# === CHOICE
|
|
876
872
|
def set_choice(self, column, value):
|
|
877
873
|
"""Adds a choice field by looking up its numeric value"""
|
|
878
|
-
choice_value = self.
|
|
874
|
+
choice_value = self.global_choices[column].get(value) # Get numeric value for choice
|
|
879
875
|
if choice_value is not None:
|
|
880
876
|
self.data[column] = choice_value
|
|
881
877
|
else:
|
|
@@ -885,7 +881,7 @@ class DataverseEntity(DataverseBase):
|
|
|
885
881
|
|
|
886
882
|
def get_choice(self, column):
|
|
887
883
|
"""Retrieves the readable choice name from its stored numeric value (Int → String)"""
|
|
888
|
-
choice_dict = self.
|
|
884
|
+
choice_dict = self.global_choices.get(column, {})
|
|
889
885
|
choices_invert = {v: k for k, v in choice_dict.items()} # Invert dict for reverse lookup
|
|
890
886
|
numeric_value = self.data.get(column, None)
|
|
891
887
|
return choices_invert.get(numeric_value, None)
|
|
@@ -956,9 +952,14 @@ class DataverseEntity(DataverseBase):
|
|
|
956
952
|
filename = f"{column}_data.json"
|
|
957
953
|
# logger.info(f"Uploading JSON content as file: {filename} of type {type(json_content)}")
|
|
958
954
|
return self.upload_file(column, file_content, filename)
|
|
955
|
+
elif isinstance(value, bytes):
|
|
956
|
+
# Raw bytes data - upload directly
|
|
957
|
+
file_content = io.BytesIO(value)
|
|
958
|
+
filename = f"{column}_data.bin" # Generic binary file extension
|
|
959
|
+
return self.upload_file(column, file_content, filename)
|
|
959
960
|
else:
|
|
960
961
|
raise ValueError(
|
|
961
|
-
f"Unsupported file value type: {type(value)}. Expected dict or
|
|
962
|
+
f"Unsupported file value type: {type(value)}. Expected dict, list, or bytes for file data."
|
|
962
963
|
)
|
|
963
964
|
|
|
964
965
|
def get_file_url(self, column):
|
|
File without changes
|
|
File without changes
|