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.
- surfdataverse-4.0.1/PKG-INFO +401 -0
- surfdataverse-4.0.1/README.md +372 -0
- {surfdataverse-3.0.0 → surfdataverse-4.0.1}/pyproject.toml +1 -1
- {surfdataverse-3.0.0 → surfdataverse-4.0.1}/src/surfdataverse/container.py +4 -4
- {surfdataverse-3.0.0 → surfdataverse-4.0.1}/src/surfdataverse/core.py +144 -166
- surfdataverse-3.0.0/PKG-INFO +0 -321
- surfdataverse-3.0.0/README.md +0 -292
- {surfdataverse-3.0.0 → surfdataverse-4.0.1}/src/surfdataverse/__init__.py +0 -0
- {surfdataverse-3.0.0 → surfdataverse-4.0.1}/src/surfdataverse/exceptions.py +0 -0
|
@@ -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
|