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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: surfdataverse
3
- Version: 4.0.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", "prefix_")
80
- table_reader = connect_table("logical_table_name_1", "prefix_")
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", "prefix_")
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", "prefix_")
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", "prefix_")
51
- table_reader = connect_table("logical_table_name_1", "prefix_")
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", "prefix_")
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", "prefix_")
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()
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "surfdataverse"
7
- version = "4.0.0"
7
+ version = "4.0.2"
8
8
  description = "A Python package for ionysis Microsoft Dataverse integration"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -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 = "", prefix: str = "") -> DataverseTable:
37
+ def connect_table(logical_name: str = "") -> DataverseTable:
38
38
  """Connect to a Dataverse table"""
39
- return container.table_factory(logical_name, prefix)
39
+ return container.table_factory(logical_name)
40
40
 
41
41
 
42
- def connect_entity(table_logical_name: str, table_prefix: str = "") -> DataverseEntity:
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, table_prefix)
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 = "", prefix="", client=None):
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.table_logical_name_prefix = prefix
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=None, name_column=None) -> uuid.UUID | None:
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, table_prefix="", client=None):
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, table_prefix, client)
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 == "File" or (field_type == "Virtual" and "json" in column.lower()):
703
+ if field_type in ["File", "Image", "Virtual"]:
720
704
  self.set_file(column, value)
721
- # Lookup columns
722
- elif column in self.get_table_relationships():
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
- # Choice columns (picklist/option sets)
725
- elif column in self.filtered_choices:
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.filtered_choices:
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
- if field.startswith(self.table_logical_name_prefix) or field.endswith("id"):
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
- if field.startswith(self.table_logical_name_prefix) or field.endswith("id"):
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
- value = self.name_to_guid(target_entity, value)
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.filtered_choices[column].get(value) # Get numeric value for choice
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.filtered_choices.get(column, {})
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 list for JSON data."
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):