loyverse-sdk 0.1.0__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.
- loyverse_sdk-0.1.0/.gitignore +28 -0
- loyverse_sdk-0.1.0/.python-version +1 -0
- loyverse_sdk-0.1.0/LICENSE +21 -0
- loyverse_sdk-0.1.0/PKG-INFO +518 -0
- loyverse_sdk-0.1.0/README.md +489 -0
- loyverse_sdk-0.1.0/examples/duckdb_export.py +315 -0
- loyverse_sdk-0.1.0/pyproject.toml +66 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/__init__.py +4 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/auth.py +23 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/client.py +340 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/core/config.py +15 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/core/console.py +7 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/db/__init__.py +47 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/db/connection.py +217 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/db/converters.py +630 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/db/exporter.py +537 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/db/schema_builder.py +1017 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/__init__.py +16 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/base.py +29 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/categories.py +31 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/customers.py +30 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/devices.py +30 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/discounts.py +31 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/employees.py +24 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/inventory.py +33 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/items.py +34 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/merchant.py +13 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/mixins.py +204 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/modifiers.py +34 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/receipts.py +38 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/shifts.py +22 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/stores.py +22 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/suppliers.py +30 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/taxes.py +28 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/variants.py +28 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/endpoints/webhooks.py +21 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/exceptions.py +312 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/helpers.py +148 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/__init__.py +21 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/category.py +54 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/common.py +30 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/device.py +22 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/discount.py +54 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/inventory.py +28 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/item.py +48 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/merchant.py +29 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/modifier.py +21 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/receipt.py +124 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/shift.py +47 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/store.py +17 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/supplier.py +21 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/tax.py +15 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/user.py +62 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/variant.py +29 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/models/webhook.py +32 -0
- loyverse_sdk-0.1.0/src/loyverse_sdk/utils.py +7 -0
- loyverse_sdk-0.1.0/tests/integration/conftest.py +27 -0
- loyverse_sdk-0.1.0/tests/integration/db/test_full_export.py +306 -0
- loyverse_sdk-0.1.0/tests/integration/test_categories_endpoint.py +5 -0
- loyverse_sdk-0.1.0/tests/unit/conftest.py +7 -0
- loyverse_sdk-0.1.0/tests/unit/db/test_converters.py +635 -0
- loyverse_sdk-0.1.0/tests/unit/db/test_exporter.py +773 -0
- loyverse_sdk-0.1.0/tests/unit/db/test_schema_builder.py +439 -0
- loyverse_sdk-0.1.0/tests/unit/endpoints/__init__.py +0 -0
- loyverse_sdk-0.1.0/tests/unit/endpoints/test_inventory_endpoint.py +155 -0
- loyverse_sdk-0.1.0/tests/unit/models/test_category_model.py +37 -0
- loyverse_sdk-0.1.0/tests/unit/models/test_customer_model.py +62 -0
- loyverse_sdk-0.1.0/tests/unit/models/test_discount_model.py +88 -0
- loyverse_sdk-0.1.0/tests/unit/models/test_employee_model.py +48 -0
- loyverse_sdk-0.1.0/tests/unit/models/test_inventory_model.py +112 -0
- loyverse_sdk-0.1.0/tests/unit/models/test_item_model.py +67 -0
- loyverse_sdk-0.1.0/tests/unit/models/test_payment_type_model.py +52 -0
- loyverse_sdk-0.1.0/tests/unit/models/test_pos_device_model.py +56 -0
- loyverse_sdk-0.1.0/tests/unit/models/test_receipt_model.py +163 -0
- loyverse_sdk-0.1.0/tests/unit/models/test_shift_model.py +352 -0
- loyverse_sdk-0.1.0/uv.lock +634 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Command runners
|
|
11
|
+
justfile
|
|
12
|
+
Makefile
|
|
13
|
+
|
|
14
|
+
# Virtual environments
|
|
15
|
+
.venv
|
|
16
|
+
.env
|
|
17
|
+
|
|
18
|
+
# Databases
|
|
19
|
+
*.sqlite*
|
|
20
|
+
*.db
|
|
21
|
+
*.duckdb
|
|
22
|
+
|
|
23
|
+
# Markdown files
|
|
24
|
+
TODO.md
|
|
25
|
+
CLAUDE.md
|
|
26
|
+
AGENTS.md
|
|
27
|
+
|
|
28
|
+
.planning/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 dagsdags212 <jegsamson.dev@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loyverse-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Asynchronous Python SDK for the Loyverse API
|
|
5
|
+
Project-URL: Homepage, https://github.com/dagsdags212/loyverse_sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/dagsdags212/loyverse_sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/dagsdags212/loyverse_sdk/issues
|
|
8
|
+
Author-email: dagsdags212 <jegsamson.dev@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,async,loyverse,pos,sdk
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: duckdb>=1.1.0
|
|
20
|
+
Requires-Dist: httpx>=0.28.1
|
|
21
|
+
Requires-Dist: polars>=1.0.0
|
|
22
|
+
Requires-Dist: pyarrow>=15.0.0
|
|
23
|
+
Requires-Dist: pydantic-settings>=2.12.0
|
|
24
|
+
Requires-Dist: pydantic>=2.12.4
|
|
25
|
+
Requires-Dist: pytz>=2025.2
|
|
26
|
+
Requires-Dist: rich>=14.2.0
|
|
27
|
+
Requires-Dist: sqlmodel>=0.0.27
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Loyverse SDK
|
|
31
|
+
|
|
32
|
+
Asynchronous Python SDK for the [Loyverse API](https://developer.loyverse.com/docs/), a point-of-sale (POS) system for managing business transactions, inventory, and customer data.
|
|
33
|
+
|
|
34
|
+
## Overview
|
|
35
|
+
|
|
36
|
+
The SDK provides:
|
|
37
|
+
- **Async/await** interface using `httpx` for non-blocking API calls
|
|
38
|
+
- **Type-safe** request/response models using Pydantic
|
|
39
|
+
- **Automatic pagination** with cursor-based iteration via `iter_all()`
|
|
40
|
+
- **Full CRUD operations** for supported endpoints
|
|
41
|
+
- **16 endpoints**: categories, customers, discounts, devices, employees, inventory, items, merchant, modifiers, receipts, shifts, stores, suppliers, taxes, webhooks, variants
|
|
42
|
+
|
|
43
|
+
### Codebase Structure
|
|
44
|
+
|
|
45
|
+
**`src/loyverse_sdk/`** contains:
|
|
46
|
+
- `client.py` - Main `LoyverseClient` class with endpoint access
|
|
47
|
+
- `endpoints/` - Endpoint classes using mixin pattern for CRUD operations
|
|
48
|
+
- `models/` - Pydantic models for request/response validation
|
|
49
|
+
- `auth.py` - Token-based authentication
|
|
50
|
+
- `core/` - Configuration, logging, and utilities
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv pip install git+https://github.com/dagsdags212/loyverse_sdk.git
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Setup
|
|
59
|
+
|
|
60
|
+
Set your API token as an environment variable:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export LOYVERSE_API_TOKEN=your_api_token
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or create a `.env` file in your project root:
|
|
67
|
+
|
|
68
|
+
```env
|
|
69
|
+
LOYVERSE_API_TOKEN=your_api_token
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
from loyverse_sdk import LoyverseClient
|
|
77
|
+
|
|
78
|
+
async def main():
|
|
79
|
+
# Create client (automatically loads token from environment)
|
|
80
|
+
client = LoyverseClient()
|
|
81
|
+
|
|
82
|
+
# List customers
|
|
83
|
+
response = await client.customers.list(limit=10)
|
|
84
|
+
print(f"Found {len(response.items)} customers")
|
|
85
|
+
|
|
86
|
+
# Close connection
|
|
87
|
+
await client.close()
|
|
88
|
+
|
|
89
|
+
asyncio.run(main())
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Usage Examples
|
|
93
|
+
|
|
94
|
+
### Customers Endpoint
|
|
95
|
+
|
|
96
|
+
The customers endpoint manages customer data from your POS system.
|
|
97
|
+
|
|
98
|
+
**List customers with pagination:**
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# Get first page of customers
|
|
102
|
+
response = await client.customers.list(limit=50)
|
|
103
|
+
|
|
104
|
+
for customer in response.items:
|
|
105
|
+
print(f"{customer.name} - {customer.email}")
|
|
106
|
+
print(f" Total visits: {customer.total_visits}")
|
|
107
|
+
print(f" Total spent: ${customer.total_spent}")
|
|
108
|
+
|
|
109
|
+
# Get next page using cursor
|
|
110
|
+
if response.cursor:
|
|
111
|
+
next_page = await client.customers.list(limit=50, cursor=response.cursor)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Retrieve a single customer:**
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
customer = await client.customers.retrieve(id="customer-uuid-here")
|
|
118
|
+
print(customer.name)
|
|
119
|
+
print(customer.phone_number)
|
|
120
|
+
print(customer.address)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Create a new customer:**
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
new_customer = await client.customers.create({
|
|
127
|
+
"name": "Jane Smith",
|
|
128
|
+
"email": "jane@example.com",
|
|
129
|
+
"phone_number": "+1234567890",
|
|
130
|
+
"address": "123 Main St",
|
|
131
|
+
"city": "San Francisco",
|
|
132
|
+
"postal_code": "94102",
|
|
133
|
+
"customer_code": "CUST001"
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
print(f"Created customer: {new_customer.id}")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Update an existing customer:**
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
updated = await client.customers.update(
|
|
143
|
+
id=customer.id,
|
|
144
|
+
payload={"email": "newemail@example.com", "note": "VIP customer"}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
print(f"Updated {updated.name}")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Delete a customer:**
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
result = await client.customers.delete(id=customer.id)
|
|
154
|
+
print(result) # {'deleted_object_ids': ['customer-uuid']}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Iterate through all customers:**
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# Automatically handles pagination across all pages
|
|
161
|
+
async for customer in client.customers.iter_all():
|
|
162
|
+
print(f"{customer.name} - Last visit: {customer.last_visit}")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Filter customers by date:**
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from datetime import datetime
|
|
169
|
+
|
|
170
|
+
# Get customers created in the last 30 days
|
|
171
|
+
start_date = datetime.now() - timedelta(days=30)
|
|
172
|
+
|
|
173
|
+
async for customer in client.customers.iter_all(created_at_min=start_date):
|
|
174
|
+
tenure = customer.tenure() # timedelta between first and last visit
|
|
175
|
+
print(f"{customer.name} - Customer for {tenure.days} days")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Other Endpoints
|
|
179
|
+
|
|
180
|
+
All endpoints follow the same pattern. Available endpoints:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
client.categories # Item categories
|
|
184
|
+
client.customers # Customer records
|
|
185
|
+
client.discounts # Discount rules
|
|
186
|
+
client.devices # POS devices
|
|
187
|
+
client.employees # Staff members
|
|
188
|
+
client.inventory # Stock levels
|
|
189
|
+
client.items # Inventory items
|
|
190
|
+
client.merchant # Merchant info
|
|
191
|
+
client.modifiers # Item modifiers
|
|
192
|
+
client.receipts # Transaction receipts
|
|
193
|
+
client.shifts # Employee shifts
|
|
194
|
+
client.stores # Store locations
|
|
195
|
+
client.suppliers # Supplier records
|
|
196
|
+
client.taxes # Tax configurations
|
|
197
|
+
client.variants # Item variants
|
|
198
|
+
client.webhooks # Webhook subscriptions
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Each endpoint supports operations based on the [Loyverse API capabilities](https://developer.loyverse.com/docs/).
|
|
202
|
+
|
|
203
|
+
## DuckDB Export
|
|
204
|
+
|
|
205
|
+
The SDK includes powerful export functionality to save all your Loyverse data to a local DuckDB database for analytics, reporting, and data warehousing.
|
|
206
|
+
|
|
207
|
+
### Why DuckDB?
|
|
208
|
+
|
|
209
|
+
DuckDB is an analytics-focused database perfect for:
|
|
210
|
+
- **Fast analytical queries** on large datasets
|
|
211
|
+
- **Local data warehousing** without server infrastructure
|
|
212
|
+
- **SQL analytics** with familiar syntax
|
|
213
|
+
- **Integration** with Python, R, and BI tools
|
|
214
|
+
- **Efficient storage** with columnar compression
|
|
215
|
+
|
|
216
|
+
### Features
|
|
217
|
+
|
|
218
|
+
- ✅ **15 main resource tables** (categories, items, receipts, etc.)
|
|
219
|
+
- ✅ **Relational schema** with foreign keys and indexes
|
|
220
|
+
- ✅ **Junction tables** for many-to-many relationships
|
|
221
|
+
- ✅ **Child tables** for nested data (line items, modifier options)
|
|
222
|
+
- ✅ **Full and incremental exports** with date range filtering
|
|
223
|
+
- ✅ **Streaming export** for memory efficiency
|
|
224
|
+
- ✅ **UPSERT support** (INSERT OR REPLACE) to prevent duplicates
|
|
225
|
+
- ✅ **Progress tracking** with callback support
|
|
226
|
+
|
|
227
|
+
### Quick Start
|
|
228
|
+
|
|
229
|
+
**Full export:**
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
import asyncio
|
|
233
|
+
from loyverse_sdk import LoyverseClient
|
|
234
|
+
|
|
235
|
+
async def main():
|
|
236
|
+
client = LoyverseClient()
|
|
237
|
+
|
|
238
|
+
# Export all data to DuckDB
|
|
239
|
+
counts = await client.export_to_duckdb("loyverse.duckdb")
|
|
240
|
+
|
|
241
|
+
print(f"Exported {sum(counts.values())} total records")
|
|
242
|
+
# Output: {'categories': 15, 'customers': 1250, 'receipts': 45000, ...}
|
|
243
|
+
|
|
244
|
+
await client.close()
|
|
245
|
+
|
|
246
|
+
asyncio.run(main())
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Query exported data:**
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
import duckdb
|
|
253
|
+
|
|
254
|
+
conn = duckdb.connect("loyverse.duckdb")
|
|
255
|
+
|
|
256
|
+
# Top 10 customers by total spent
|
|
257
|
+
result = conn.execute("""
|
|
258
|
+
SELECT
|
|
259
|
+
c.name,
|
|
260
|
+
COUNT(DISTINCT r.id) as receipt_count,
|
|
261
|
+
SUM(r.total_amount) as total_spent
|
|
262
|
+
FROM customers c
|
|
263
|
+
JOIN receipts r ON c.id = r.customer_id
|
|
264
|
+
WHERE r.receipt_type = 'SALE'
|
|
265
|
+
GROUP BY c.id, c.name
|
|
266
|
+
ORDER BY total_spent DESC
|
|
267
|
+
LIMIT 10
|
|
268
|
+
""").fetchall()
|
|
269
|
+
|
|
270
|
+
conn.close()
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Export Methods
|
|
274
|
+
|
|
275
|
+
#### 1. Full Export with Options
|
|
276
|
+
|
|
277
|
+
Export all or selected resources with comprehensive filtering:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
from datetime import datetime, timedelta
|
|
281
|
+
|
|
282
|
+
client = LoyverseClient()
|
|
283
|
+
|
|
284
|
+
# Export with all options
|
|
285
|
+
counts = await client.export_to_duckdb(
|
|
286
|
+
db_path="loyverse.duckdb",
|
|
287
|
+
resources=["receipts", "customers", "items"], # Optional: specific resources
|
|
288
|
+
created_at_min=datetime(2024, 1, 1), # Optional: start date
|
|
289
|
+
created_at_max=datetime(2024, 12, 31), # Optional: end date
|
|
290
|
+
updated_at_min=datetime.now() - timedelta(days=7), # Optional: updated after
|
|
291
|
+
batch_size=1000, # Optional: records per batch
|
|
292
|
+
progress_callback=lambda res, curr, total: print(f"{res}: {curr}"), # Optional
|
|
293
|
+
create_indexes=True # Optional: create indexes after
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
print(f"Exported: {counts}")
|
|
297
|
+
# Returns: {'receipts': 5000, 'customers': 1200, 'items': 350}
|
|
298
|
+
|
|
299
|
+
await client.close()
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
#### 2. Single Resource Export
|
|
303
|
+
|
|
304
|
+
Export one resource with fine-grained control:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
client = LoyverseClient()
|
|
308
|
+
|
|
309
|
+
# Export only receipts from last 30 days
|
|
310
|
+
count = await client.export_resource_to_duckdb(
|
|
311
|
+
resource_name="receipts",
|
|
312
|
+
db_path="loyverse.duckdb",
|
|
313
|
+
created_at_min=datetime.now() - timedelta(days=30)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
print(f"Exported {count} receipts")
|
|
317
|
+
|
|
318
|
+
await client.close()
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### 3. Schema Initialization
|
|
322
|
+
|
|
323
|
+
Create database schema without exporting data:
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
client = LoyverseClient()
|
|
327
|
+
|
|
328
|
+
# Initialize empty database with schema
|
|
329
|
+
client.init_duckdb_schema("loyverse.duckdb")
|
|
330
|
+
|
|
331
|
+
# Or reset existing database
|
|
332
|
+
client.init_duckdb_schema("loyverse.duckdb", drop_existing=True)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Advanced Usage
|
|
336
|
+
|
|
337
|
+
**Progress tracking:**
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
def progress_callback(resource_name: str, current: int, total: int):
|
|
341
|
+
"""Called for each batch of records."""
|
|
342
|
+
print(f"Exporting {resource_name}: {current:,} records processed...")
|
|
343
|
+
|
|
344
|
+
counts = await client.export_to_duckdb(
|
|
345
|
+
"loyverse.duckdb",
|
|
346
|
+
progress_callback=progress_callback
|
|
347
|
+
)
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Incremental updates:**
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
# Export only records updated in last 24 hours
|
|
354
|
+
yesterday = datetime.now() - timedelta(days=1)
|
|
355
|
+
|
|
356
|
+
counts = await client.export_to_duckdb(
|
|
357
|
+
"loyverse.duckdb",
|
|
358
|
+
updated_at_min=yesterday
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# UPSERT semantics: existing records are updated, new ones inserted
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Selective export:**
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
# Export only what you need
|
|
368
|
+
counts = await client.export_to_duckdb(
|
|
369
|
+
"loyverse.duckdb",
|
|
370
|
+
resources=[
|
|
371
|
+
"receipts", # Transaction data
|
|
372
|
+
"customers", # Customer profiles
|
|
373
|
+
"items", # Product catalog
|
|
374
|
+
"categories" # Item categories
|
|
375
|
+
]
|
|
376
|
+
)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Database Schema
|
|
380
|
+
|
|
381
|
+
The exported database includes:
|
|
382
|
+
|
|
383
|
+
**Main Tables (15):**
|
|
384
|
+
- `categories` - Item categories
|
|
385
|
+
- `stores` - Store locations
|
|
386
|
+
- `suppliers` - Supplier records
|
|
387
|
+
- `taxes` - Tax configurations
|
|
388
|
+
- `modifiers` - Item modifiers
|
|
389
|
+
- `discounts` - Discount rules
|
|
390
|
+
- `employees` - Staff members
|
|
391
|
+
- `customers` - Customer records
|
|
392
|
+
- `pos_devices` - POS devices
|
|
393
|
+
- `payment_types` - Payment methods
|
|
394
|
+
- `items` - Inventory items
|
|
395
|
+
- `variants` - Item variants
|
|
396
|
+
- `receipts` - Transaction receipts
|
|
397
|
+
- `inventory` - Stock levels
|
|
398
|
+
- `merchant` - Merchant info
|
|
399
|
+
|
|
400
|
+
**Junction Tables (8):**
|
|
401
|
+
- `employee_store` - Employee-to-store assignments
|
|
402
|
+
- `item_tax` - Item-to-tax relationships
|
|
403
|
+
- `item_modifier` - Item-to-modifier relationships
|
|
404
|
+
- `modifier_store` - Modifier-to-store assignments
|
|
405
|
+
- `tax_store` - Tax-to-store assignments
|
|
406
|
+
- `discount_store` - Discount-to-store assignments
|
|
407
|
+
- `payment_type_store` - Payment type availability by store
|
|
408
|
+
- `variant_store` - Variant inventory by store
|
|
409
|
+
|
|
410
|
+
**Child Tables (2):**
|
|
411
|
+
- `receipt_line_items` - Individual line items per receipt
|
|
412
|
+
- `modifier_options` - Options within modifiers
|
|
413
|
+
|
|
414
|
+
**Metadata:**
|
|
415
|
+
- `sync_metadata` - Tracks export history and record counts
|
|
416
|
+
|
|
417
|
+
### Example Queries
|
|
418
|
+
|
|
419
|
+
**Daily revenue:**
|
|
420
|
+
|
|
421
|
+
```sql
|
|
422
|
+
SELECT
|
|
423
|
+
DATE(receipt_date) as date,
|
|
424
|
+
COUNT(*) as receipt_count,
|
|
425
|
+
SUM(total_amount) as revenue
|
|
426
|
+
FROM receipts
|
|
427
|
+
WHERE receipt_type = 'SALE'
|
|
428
|
+
AND receipt_date >= CURRENT_DATE - INTERVAL '30 days'
|
|
429
|
+
GROUP BY DATE(receipt_date)
|
|
430
|
+
ORDER BY date DESC;
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Best-selling items:**
|
|
434
|
+
|
|
435
|
+
```sql
|
|
436
|
+
SELECT
|
|
437
|
+
i.name,
|
|
438
|
+
SUM(l.quantity) as units_sold,
|
|
439
|
+
SUM(l.quantity * l.price) as revenue
|
|
440
|
+
FROM items i
|
|
441
|
+
JOIN receipt_line_items l ON i.id = l.item_id
|
|
442
|
+
JOIN receipts r ON l.receipt_id = r.id
|
|
443
|
+
WHERE r.receipt_type = 'SALE'
|
|
444
|
+
GROUP BY i.id, i.name
|
|
445
|
+
ORDER BY units_sold DESC
|
|
446
|
+
LIMIT 10;
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**Customer lifetime value:**
|
|
450
|
+
|
|
451
|
+
```sql
|
|
452
|
+
SELECT
|
|
453
|
+
c.name,
|
|
454
|
+
c.total_visits,
|
|
455
|
+
c.total_spent,
|
|
456
|
+
c.total_spent / NULLIF(c.total_visits, 0) as avg_per_visit
|
|
457
|
+
FROM customers c
|
|
458
|
+
WHERE c.total_visits > 0
|
|
459
|
+
ORDER BY c.total_spent DESC
|
|
460
|
+
LIMIT 20;
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**Inventory by category:**
|
|
464
|
+
|
|
465
|
+
```sql
|
|
466
|
+
SELECT
|
|
467
|
+
cat.name as category,
|
|
468
|
+
COUNT(DISTINCT i.id) as item_count,
|
|
469
|
+
COUNT(DISTINCT v.id) as variant_count
|
|
470
|
+
FROM categories cat
|
|
471
|
+
LEFT JOIN items i ON cat.id = i.category_id
|
|
472
|
+
LEFT JOIN variants v ON i.id = v.item_id
|
|
473
|
+
GROUP BY cat.id, cat.name
|
|
474
|
+
ORDER BY item_count DESC;
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Performance Tips
|
|
478
|
+
|
|
479
|
+
1. **Batch size**: Default is 1000 records per transaction. Increase for faster exports on powerful machines:
|
|
480
|
+
```python
|
|
481
|
+
counts = await client.export_to_duckdb("loyverse.duckdb", batch_size=5000)
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
2. **Indexes**: Created automatically after export. Disable for faster initial load:
|
|
485
|
+
```python
|
|
486
|
+
counts = await client.export_to_duckdb("loyverse.duckdb", create_indexes=False)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
3. **Memory**: DuckDB is configured with 4GB memory limit by default. Efficient for datasets with millions of records.
|
|
490
|
+
|
|
491
|
+
4. **Incremental updates**: Export only changed records to minimize transfer time:
|
|
492
|
+
```python
|
|
493
|
+
# Daily sync: export only yesterday's data
|
|
494
|
+
yesterday = datetime.now() - timedelta(days=1)
|
|
495
|
+
counts = await client.export_to_duckdb("loyverse.duckdb", created_at_min=yesterday)
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Use Cases
|
|
499
|
+
|
|
500
|
+
- **Business Intelligence**: Connect DuckDB to Metabase, Superset, or Tableau
|
|
501
|
+
- **Custom Reports**: Write SQL queries for specific business questions
|
|
502
|
+
- **Data Science**: Analyze sales patterns, customer behavior, inventory trends
|
|
503
|
+
- **Backup**: Maintain local copy of all POS data
|
|
504
|
+
- **Data Warehouse**: Centralize data for cross-system analytics
|
|
505
|
+
- **Migration**: Export data for migration to other systems
|
|
506
|
+
|
|
507
|
+
### Complete Example
|
|
508
|
+
|
|
509
|
+
See `examples/duckdb_export.py` for comprehensive examples including:
|
|
510
|
+
- Full and selective exports
|
|
511
|
+
- Date range filtering
|
|
512
|
+
- Progress tracking
|
|
513
|
+
- Querying exported data
|
|
514
|
+
- Incremental updates
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
python examples/duckdb_export.py
|
|
518
|
+
```
|