surfdataverse 3.0.0__tar.gz → 4.0.1__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.
@@ -0,0 +1,401 @@
1
+ Metadata-Version: 2.3
2
+ Name: surfdataverse
3
+ Version: 4.0.1
4
+ Summary: A Python package for ionysis Microsoft Dataverse integration
5
+ Keywords: dataverse,microsoft,crm,api
6
+ Author: ionysis
7
+ Author-email: ionysis <friedemann.heinz@ionysis.com>
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Requires-Dist: dependency-injector>=4.42.0
18
+ Requires-Dist: msal>=1.33.0
19
+ Requires-Dist: numpy>=2.3.3
20
+ Requires-Dist: pandas>=2.3.2
21
+ Requires-Dist: polars>=1.34.0
22
+ Requires-Dist: requests>=2.32.5
23
+ Requires-Python: >=3.11
24
+ Project-URL: Bug Tracker, https://github.com/FriedemannHeinz/SurfDataverse/issues
25
+ Project-URL: Documentation, https://github.com/FriedemannHeinz/SurfDataverse#readme
26
+ Project-URL: Homepage, https://github.com/FriedemannHeinz/SurfDataverse
27
+ Project-URL: Repository, https://github.com/FriedemannHeinz/SurfDataverse.git
28
+ Description-Content-Type: text/markdown
29
+
30
+ # SurfDataverse
31
+
32
+ A modern Python package for Microsoft Dataverse integration, providing a clean, object-oriented interface with dependency injection for connecting to, reading from, and writing to Microsoft Dataverse environments.
33
+
34
+ ## Features
35
+
36
+ - **Easy Authentication**: Simplified MSAL-based authentication with token caching
37
+ - **Dependency Injection**: Modern IoC container pattern for better testability and flexibility
38
+ - **Automatic Type Detection**: Automatically detects column types and forwards to appropriate methods
39
+ - **Type Safety**: Built-in validation and error handling with comprehensive type conversion
40
+ - **Configurable Prefixes**: Support for custom table/column naming conventions
41
+ - **Modern Testing**: Comprehensive test suite with proper mocking and isolation patterns
42
+ - **Extensible**: Easy to work with any Dataverse table structure
43
+
44
+ ## Quick Start
45
+
46
+ ### 1. Configuration
47
+
48
+ Create a configuration JSON file with your Dataverse connection details:
49
+
50
+ ```json
51
+ {
52
+ "authorityBase": "https://login.microsoftonline.com/",
53
+ "tenantID": "your-tenant-id",
54
+ "clientID": "your-client-id",
55
+ "environmentURI": "https://yourorg.crm.dynamics.com/",
56
+ "scopeSuffix": "/.default"
57
+ }
58
+ ```
59
+
60
+ ### 2. Basic Usage
61
+
62
+ **Modern Dependency Injection Pattern (Recommended):**
63
+
64
+ ```python
65
+ from surfdataverse import get_client, connect_entity, connect_table
66
+ from pathlib import Path
67
+
68
+ # Get client using dependency injection (singleton)
69
+ client = get_client()
70
+ client.config_path = "connection_configs/your_config.json"
71
+
72
+ # Authenticate
73
+ client.get_authenticated_session()
74
+
75
+ # Test connection
76
+ client.test_connection()
77
+
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")
81
+
82
+ # Set data using write() method with automatic type detection
83
+ product.write("prefix_name", "My Product")
84
+ product.write("prefix_company", "My Company")
85
+ product.write("prefix_articlenr", "EXT-001")
86
+
87
+ # Write to Dataverse
88
+ guid = product.write_to_dataverse()
89
+ print(f"Created/updated product with GUID: {guid}")
90
+
91
+ # Read table data
92
+ df = table_reader.get_table_data()
93
+ print(f"Retrieved {len(df)} records")
94
+ ```
95
+
96
+ **Legacy Direct Instantiation (Still Supported):**
97
+
98
+ ```python
99
+ from surfdataverse import DataverseClient, DataverseEntity
100
+ from pathlib import Path
101
+
102
+ # Initialize client directly
103
+ client = DataverseClient(config_path="connection_configs/your_config.json")
104
+ client.get_authenticated_session()
105
+
106
+ # Create entity directly (will use default DI container)
107
+ product = DataverseEntity("logical_table_name_1", "prefix_", client=client)
108
+ product.write("prefix_name", "My Product")
109
+ guid = product.write_to_dataverse()
110
+ ```
111
+
112
+ ## Automatic Type Detection
113
+
114
+ The core feature of SurfDataverse is its ability to automatically detect column types and forward to appropriate methods. When you use `write()` or `read()`, it:
115
+
116
+ 1. **Fetches table metadata** from Dataverse
117
+ 2. **Analyzes column types** (text, choice, lookup, file, etc.)
118
+ 3. **Forwards to appropriate methods** automatically
119
+ 4. **Handles type conversion** seamlessly
120
+
121
+ ### Example: Working with Different Field Types
122
+
123
+ **Using Generic Methods (Recommended):**
124
+
125
+ ```python
126
+ from surfdataverse import connect_entity
127
+
128
+ # Create entity instances using dependency injection
129
+ product = connect_entity("logical_table_name_1", "prefix_")
130
+ formula = connect_entity("logical_table_name_2", "prefix_")
131
+
132
+ # Text fields (auto-detected as data)
133
+ product.write("prefix_name", "Product Name")
134
+ product.write("prefix_company", "Company Name")
135
+
136
+ # Choice fields (auto-detected and converts text to numeric values)
137
+ formula.write("prefix_type", "Production") # Automatically maps to numeric choice value
138
+
139
+ # Lookup fields (auto-detected relationships to other tables)
140
+ formula.write("prefix_product", product.guid) # Links formula to product
141
+
142
+ # File fields (auto-detected and handles JSON data)
143
+ product.write("prefix_specifications", {
144
+ "weight": 100,
145
+ "dimensions": {"length": 50, "width": 30}
146
+ })
147
+
148
+ # Read data with automatic type detection
149
+ name = product.read("prefix_name") # Returns string
150
+ type_value = formula.read("prefix_type") # Returns choice label
151
+ specifications = product.read("prefix_specifications") # Returns parsed JSON
152
+
153
+ # Save changes
154
+ product.write_to_dataverse()
155
+ formula.write_to_dataverse()
156
+ ```
157
+
158
+ ## Table Prefix Configuration
159
+
160
+ SurfDataverse supports custom table/column prefixes to work with different naming conventions:
161
+
162
+ ```python
163
+ from surfdataverse import connect_entity, connect_table
164
+
165
+ # Using modern DI pattern with prefixes
166
+ default_entity = connect_entity("prefix_tablename", "prefix_")
167
+ custom_entity = connect_entity("myorg_product", "myorg_")
168
+
169
+ # For reading data
170
+ table_reader = connect_table("prefix_tablename")
171
+
172
+ # The system automatically:
173
+ # - Filters columns starting with your prefix
174
+ # - Detects column types for automatic method forwarding
175
+ # - Handles relationships between tables with the same prefix
176
+ ```
177
+
178
+ ## Data Retrieval
179
+
180
+ Fetch data from Dataverse tables as pandas DataFrames using modern DI patterns:
181
+
182
+ ```python
183
+ from surfdataverse import connect_table, get_client
184
+
185
+ # Create table reader using dependency injection
186
+ table_reader = connect_table("logical_table_name_1")
187
+
188
+ # Get table data as pandas DataFrame
189
+ df = table_reader.get_table_data()
190
+
191
+ # Get specific record using entity
192
+ entity = connect_entity("logical_table_name_1", "prefix_")
193
+ entity.fetch_data("guid-here") # Loads data into entity.data
194
+
195
+ # Get table metadata
196
+ metadata = table_reader.get_table_metadata()
197
+
198
+ # Download multiple tables (optionally filtered by schema)
199
+ definitions, data, metadata = table_reader.download_tables_as_df(schema_filter="prefix")
200
+
201
+ # Direct client access when needed
202
+ client = get_client()
203
+ entity_set_name = client.get_table_entity_set_name(logical_name="logical_table_name_1")
204
+ record = client.get_record(entity_set_name, "guid-here")
205
+ ```
206
+
207
+ ## Field Types
208
+
209
+ The system automatically detects different field types and handles them appropriately:
210
+
211
+ ### Data Fields
212
+ Simple text, numeric, and date fields:
213
+ ```python
214
+ entity.write("prefix_name", "Some Value")
215
+ entity.write("prefix_quantity", 100)
216
+ entity.write("prefix_price", 29.99)
217
+
218
+ # Reading returns the appropriate type
219
+ name = entity.read("prefix_name") # Returns string
220
+ quantity = entity.read("prefix_quantity") # Returns int
221
+ price = entity.read("prefix_price") # Returns float
222
+ ```
223
+
224
+ ### Choice Fields
225
+ Option set fields (automatically converts labels to/from numeric values):
226
+ ```python
227
+ entity.write("prefix_status", "Active") # Converts to numeric value
228
+ current_status = entity.read("prefix_status") # Returns "Active" (readable label)
229
+ ```
230
+
231
+ ### Lookup Fields
232
+ Relationships to other tables:
233
+ ```python
234
+ entity.write("prefix_parent_record", "parent-guid-here")
235
+ entity.write("prefix_related_item", related_entity.guid)
236
+
237
+ # Reading returns GUID
238
+ parent_guid = entity.read("prefix_parent_record")
239
+ ```
240
+
241
+ ### File Fields
242
+ Virtual file fields for storing complex data:
243
+ ```python
244
+ entity.write("prefix_metadata", {
245
+ "tags": ["important", "production"],
246
+ "config": {"setting1": "value1"}
247
+ })
248
+
249
+ # Reading returns parsed JSON
250
+ metadata = entity.read("prefix_metadata") # Returns dict
251
+ ```
252
+
253
+ ### Alternative: Explicit Type Methods
254
+ You can also use explicit type-specific methods when needed:
255
+ ```python
256
+ # Explicit methods for specific control
257
+ entity.set_data("prefix_name", "Some Value")
258
+ entity.set_choice("prefix_status", "Active")
259
+ entity.set_lookup("prefix_parent", "guid-here")
260
+ entity.set_file("prefix_data", {"key": "value"})
261
+
262
+ # Corresponding getters
263
+ name = entity.get_data("prefix_name")
264
+ status = entity.get_choice("prefix_status")
265
+ parent_guid = entity.get_lookup("prefix_parent")
266
+ file_data = entity.get_file("prefix_data")
267
+ ```
268
+
269
+ ## Error Handling
270
+
271
+ The package provides comprehensive error handling:
272
+
273
+ ```python
274
+ from surfdataverse import (
275
+ AuthenticationError,
276
+ ConnectionError,
277
+ DataverseAPIError,
278
+ EntityError,
279
+ ValidationError
280
+ )
281
+
282
+ try:
283
+ client.get_authenticated_session()
284
+ entity.write_to_dataverse()
285
+ except AuthenticationError as e:
286
+ print(f"Authentication failed: {e}")
287
+ except DataverseAPIError as e:
288
+ print(f"API error (status {e.status_code}): {e}")
289
+ except ValidationError as e:
290
+ print(f"Data validation error: {e}")
291
+ ```
292
+
293
+ ## Advanced Usage
294
+
295
+ ### Custom Entity Classes
296
+ For complex business logic, you can extend the base classes:
297
+
298
+ ```python
299
+ from surfdataverse import DataverseEntity
300
+
301
+
302
+ # Extend the base class
303
+ class CustomEntity(DataverseEntity):
304
+ def __init__(self, logical_name, prefix="prefix_"):
305
+ super().__init__(logical_name, prefix=prefix)
306
+
307
+ # Add custom business logic
308
+ def validate_data(self):
309
+ name = self.read("prefix_name")
310
+ if not name:
311
+ raise ValueError("Name is required")
312
+ return True
313
+
314
+ def set_defaults(self):
315
+ """Set default values for new entities"""
316
+ self.write("prefix_status", "Active")
317
+ self.write("prefix_created_date", "2024-01-01")
318
+ ```
319
+
320
+ ### Batch Operations
321
+ Work with multiple records efficiently:
322
+
323
+ ```python
324
+ # Create multiple records
325
+ products = []
326
+ for i in range(10):
327
+ product = connect_entity("logical_table_name_1", "prefix_")
328
+ product.write("prefix_name", f"Product {i}")
329
+ product.write("prefix_company", "ACME Corp")
330
+ products.append(product)
331
+
332
+ # Write all records
333
+ for product in products:
334
+ product.write_to_dataverse()
335
+ ```
336
+
337
+ ### Session Management
338
+
339
+ The `DataverseClient` uses a singleton pattern for connection management:
340
+
341
+ ```python
342
+ # First initialization
343
+ client1 = DataverseClient(config_path="config1.json")
344
+
345
+ # Later access (returns same instance)
346
+ client2 = DataverseClient() # Same as client1
347
+ ```
348
+
349
+ ## Project Structure
350
+ ```
351
+ surfdataverse/
352
+ ├── __init__.py # Package initialization
353
+ ├── core.py # Core client and table classes
354
+ └── exceptions.py # Custom exceptions
355
+
356
+ examples/
357
+ ├── basic_usage.py # Basic usage examples
358
+ ├── example_auto_usage.py # Auto-generation examples
359
+ └── schema_visualization.py # Schema analysis tools
360
+
361
+ connection_configs/ # Configuration files (not tracked)
362
+ ├── dev.json
363
+ └── production.json
364
+ ```
365
+
366
+ ## Dependencies
367
+
368
+ - `msal`: Microsoft Authentication Library
369
+ - `requests`: HTTP client
370
+ - `pandas`: Data manipulation and analysis
371
+
372
+ ## Testing
373
+
374
+ ```bash
375
+ python -m pytest tests/ -v
376
+ ```
377
+
378
+ ## Code Style
379
+
380
+ ```bash
381
+ uv run ruff format src/
382
+ uv run ruff check src/
383
+ ```
384
+
385
+ ## Contributing
386
+
387
+ 1. Fork the repository
388
+ 2. Create a feature branch
389
+ 3. Make your changes
390
+ 4. Add tests
391
+ 5. Submit a pull request
392
+
393
+ ## License
394
+
395
+ MIT License - see LICENSE file for details.
396
+
397
+ ## Support
398
+
399
+ For issues and questions:
400
+ - GitHub Issues: [https://github.com/FriedemannHeinz/SurfDataverse/issues](https://github.com/FriedemannHeinz/SurfDataverse/issues)
401
+ - Documentation: This README and inline code documentation