winipedia-django 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
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.
Potentially problematic release.
This version of winipedia-django might be problematic. Click here for more details.
- winipedia_django-0.2.2.dist-info/METADATA +702 -0
- {winipedia_django-0.2.0.dist-info → winipedia_django-0.2.2.dist-info}/RECORD +4 -4
- winipedia_django-0.2.0.dist-info/METADATA +0 -20
- {winipedia_django-0.2.0.dist-info → winipedia_django-0.2.2.dist-info}/WHEEL +0 -0
- {winipedia_django-0.2.0.dist-info → winipedia_django-0.2.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: winipedia-django
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: A utils package for django
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Winipedia
|
|
8
|
+
Author-email: win.steveker@gmx.de
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: django (>=5.2.7,<6.0.0)
|
|
15
|
+
Requires-Dist: winipedia-utils (>=0.2.10,<0.3.0)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# winipedia_django
|
|
19
|
+
(Some parts of the README are AI generated, so some parts might not be fully accurate)
|
|
20
|
+
|
|
21
|
+
[](https://www.python.org/downloads/)
|
|
22
|
+
[](https://www.djangoproject.com/)
|
|
23
|
+
[](LICENSE)
|
|
24
|
+
|
|
25
|
+
A comprehensive utility package for Django that provides efficient bulk operations, abstract base classes for management commands, and database utilities. Designed to simplify common Django operations while maintaining type safety and performance.
|
|
26
|
+
|
|
27
|
+
## Table of Contents
|
|
28
|
+
|
|
29
|
+
- [Features](#features)
|
|
30
|
+
- [Installation](#installation)
|
|
31
|
+
- [Quick Start](#quick-start)
|
|
32
|
+
- [Core Modules](#core-modules)
|
|
33
|
+
- [Bulk Operations](#bulk-operations)
|
|
34
|
+
- [Management Commands](#management-commands)
|
|
35
|
+
- [Database Utilities](#database-utilities)
|
|
36
|
+
- [Usage Examples](#usage-examples)
|
|
37
|
+
- [API Reference](#api-reference)
|
|
38
|
+
- [Best Practices](#best-practices)
|
|
39
|
+
- [Contributing](#contributing)
|
|
40
|
+
- [License](#license)
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
✨ **Efficient Bulk Operations**
|
|
45
|
+
- Bulk create, update, and delete with automatic chunking
|
|
46
|
+
- Multithreaded processing for improved performance
|
|
47
|
+
- Topological sorting for respecting model dependencies
|
|
48
|
+
- Cascade deletion simulation without database modifications
|
|
49
|
+
|
|
50
|
+
🎯 **Abstract Base Command**
|
|
51
|
+
- Template method pattern for consistent command structure
|
|
52
|
+
- Automatic logging of command options
|
|
53
|
+
- Pre-configured common arguments (dry-run, batch-size, threads, etc.)
|
|
54
|
+
- Type-safe command implementation
|
|
55
|
+
|
|
56
|
+
🗄️ **Database Utilities**
|
|
57
|
+
- Model introspection and field extraction
|
|
58
|
+
- Topological sorting of models by dependencies
|
|
59
|
+
- Raw SQL execution with parameter binding
|
|
60
|
+
- Model hashing for comparison operations
|
|
61
|
+
- Abstract BaseModel with timestamps
|
|
62
|
+
|
|
63
|
+
🔒 **Type Safety**
|
|
64
|
+
- Full type hints throughout the codebase
|
|
65
|
+
- Strict mypy configuration
|
|
66
|
+
- Compatible with modern Python type checking tools
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
### Using pip
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install winipedia-django
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Using Poetry
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
poetry add winipedia-django
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Using uv
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
uv pip install winipedia-django
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Requirements
|
|
89
|
+
|
|
90
|
+
- Python 3.12 or higher
|
|
91
|
+
- Django 5.2 or higher
|
|
92
|
+
- winipedia-utils 0.2.10 or higher
|
|
93
|
+
|
|
94
|
+
## Quick Start
|
|
95
|
+
|
|
96
|
+
### Basic Bulk Creation
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from winipedia_django.bulk import bulk_create_in_steps
|
|
100
|
+
from myapp.models import MyModel
|
|
101
|
+
|
|
102
|
+
# Create 10,000 objects efficiently
|
|
103
|
+
objects = [MyModel(name=f"item_{i}") for i in range(10000)]
|
|
104
|
+
created = bulk_create_in_steps(MyModel, objects, step=1000)
|
|
105
|
+
print(f"Created {len(created)} objects")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Creating a Management Command
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from argparse import ArgumentParser
|
|
112
|
+
from typing import Any
|
|
113
|
+
from winipedia_django.command import ABCBaseCommand
|
|
114
|
+
|
|
115
|
+
class MyCommand(ABCBaseCommand):
|
|
116
|
+
def add_command_arguments(self, parser: ArgumentParser) -> None:
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
'--input-file',
|
|
119
|
+
type=str,
|
|
120
|
+
required=True,
|
|
121
|
+
help='Path to input file'
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def handle_command(self, *args: Any, **options: Any) -> None:
|
|
125
|
+
input_file = options['input_file']
|
|
126
|
+
batch_size = options['batch_size']
|
|
127
|
+
dry_run = options['dry_run']
|
|
128
|
+
|
|
129
|
+
if dry_run:
|
|
130
|
+
self.stdout.write('DRY RUN MODE - No changes will be made')
|
|
131
|
+
|
|
132
|
+
# Your command logic here
|
|
133
|
+
self.stdout.write(self.style.SUCCESS('Command completed successfully'))
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Using Database Utilities
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from winipedia_django.database import (
|
|
140
|
+
get_fields,
|
|
141
|
+
topological_sort_models,
|
|
142
|
+
BaseModel
|
|
143
|
+
)
|
|
144
|
+
from django.db import models
|
|
145
|
+
|
|
146
|
+
# Get all fields from a model
|
|
147
|
+
fields = get_fields(MyModel)
|
|
148
|
+
field_names = [f.name for f in fields if hasattr(f, 'name')]
|
|
149
|
+
|
|
150
|
+
# Sort models by dependencies
|
|
151
|
+
models_to_create = [Book, Author, Publisher]
|
|
152
|
+
sorted_models = topological_sort_models(models_to_create)
|
|
153
|
+
# Result: [Author, Publisher, Book] - dependencies first
|
|
154
|
+
|
|
155
|
+
# Use BaseModel for automatic timestamps
|
|
156
|
+
class Article(BaseModel):
|
|
157
|
+
title = models.CharField(max_length=200)
|
|
158
|
+
content = models.TextField()
|
|
159
|
+
|
|
160
|
+
class Meta:
|
|
161
|
+
app_label = 'blog'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Core Modules
|
|
165
|
+
|
|
166
|
+
### Bulk Operations
|
|
167
|
+
|
|
168
|
+
The `bulk` module provides efficient operations for handling large datasets in Django.
|
|
169
|
+
|
|
170
|
+
#### Key Functions
|
|
171
|
+
|
|
172
|
+
**`bulk_create_in_steps(model, bulk, step=1000)`**
|
|
173
|
+
|
|
174
|
+
Creates model instances in chunks with multithreading support.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from winipedia_django.bulk import bulk_create_in_steps
|
|
178
|
+
|
|
179
|
+
users = [User(username=f"user_{i}", email=f"user_{i}@example.com")
|
|
180
|
+
for i in range(5000)]
|
|
181
|
+
created_users = bulk_create_in_steps(User, users, step=500)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**`bulk_update_in_steps(model, bulk, update_fields, step=1000)`**
|
|
185
|
+
|
|
186
|
+
Updates model instances efficiently in chunks.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from winipedia_django.bulk import bulk_update_in_steps
|
|
190
|
+
|
|
191
|
+
users = User.objects.all()[:1000]
|
|
192
|
+
for user in users:
|
|
193
|
+
user.is_active = False
|
|
194
|
+
|
|
195
|
+
updated_count = bulk_update_in_steps(User, users, ['is_active'], step=500)
|
|
196
|
+
print(f"Updated {updated_count} users")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**`bulk_delete_in_steps(model, bulk, step=1000)`**
|
|
200
|
+
|
|
201
|
+
Deletes model instances in chunks, respecting cascade relationships.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from winipedia_django.bulk import bulk_delete_in_steps
|
|
205
|
+
|
|
206
|
+
users_to_delete = User.objects.filter(is_inactive=True)[:1000]
|
|
207
|
+
total_deleted, deleted_by_model = bulk_delete_in_steps(User, users_to_delete)
|
|
208
|
+
print(f"Deleted {total_deleted} objects total")
|
|
209
|
+
print(f"Breakdown: {deleted_by_model}")
|
|
210
|
+
# Output: Breakdown: {'User': 1000, 'UserProfile': 1000, ...}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**`bulk_create_bulks_in_steps(bulk_by_class, step=1000)`**
|
|
214
|
+
|
|
215
|
+
Creates multiple model types respecting foreign key dependencies.
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from winipedia_django.bulk import bulk_create_bulks_in_steps
|
|
219
|
+
|
|
220
|
+
authors = [Author(name=f"Author {i}") for i in range(100)]
|
|
221
|
+
books = [Book(title=f"Book {i}", author=authors[0]) for i in range(500)]
|
|
222
|
+
|
|
223
|
+
bulk_by_class = {
|
|
224
|
+
Author: authors,
|
|
225
|
+
Book: books,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
results = bulk_create_bulks_in_steps(bulk_by_class, step=100)
|
|
229
|
+
# Authors are created first, then books (respecting FK dependency)
|
|
230
|
+
print(f"Created {len(results[Author])} authors")
|
|
231
|
+
print(f"Created {len(results[Book])} books")
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**`get_differences_between_bulks(bulk1, bulk2, fields)`**
|
|
235
|
+
|
|
236
|
+
Compares two lists of model instances and returns differences.
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from winipedia_django.bulk import get_differences_between_bulks
|
|
240
|
+
from winipedia_django.database import get_fields
|
|
241
|
+
|
|
242
|
+
bulk1 = [User(id=1, name="Alice"), User(id=2, name="Bob")]
|
|
243
|
+
bulk2 = [User(id=1, name="Alice"), User(id=3, name="Charlie")]
|
|
244
|
+
|
|
245
|
+
fields = get_fields(User)
|
|
246
|
+
only_in_1, only_in_2, common_1, common_2 = get_differences_between_bulks(
|
|
247
|
+
bulk1, bulk2, fields
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
print(f"Only in bulk1: {len(only_in_1)}") # Bob
|
|
251
|
+
print(f"Only in bulk2: {len(only_in_2)}") # Charlie
|
|
252
|
+
print(f"In both: {len(common_1)}") # Alice
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**`simulate_bulk_deletion(model_class, entries)`**
|
|
256
|
+
|
|
257
|
+
Preview what would be deleted including cascade deletions, without modifying the database.
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from winipedia_django.bulk import simulate_bulk_deletion
|
|
261
|
+
|
|
262
|
+
users_to_delete = User.objects.filter(is_inactive=True)[:100]
|
|
263
|
+
deletion_preview = simulate_bulk_deletion(User, users_to_delete)
|
|
264
|
+
|
|
265
|
+
for model, objects in deletion_preview.items():
|
|
266
|
+
print(f"{model.__name__}: {len(objects)} objects would be deleted")
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Management Commands
|
|
270
|
+
|
|
271
|
+
The `command` module provides an abstract base class for creating well-structured Django management commands.
|
|
272
|
+
|
|
273
|
+
#### ABCBaseCommand
|
|
274
|
+
|
|
275
|
+
A template method pattern implementation that enforces consistent command structure.
|
|
276
|
+
|
|
277
|
+
**Features:**
|
|
278
|
+
- Automatic logging of all command options
|
|
279
|
+
- Pre-configured common arguments
|
|
280
|
+
- Type-safe implementation
|
|
281
|
+
- Enforced abstract methods
|
|
282
|
+
|
|
283
|
+
**Common Arguments (automatically added):**
|
|
284
|
+
- `--dry-run`: Show what would be done without executing
|
|
285
|
+
- `--size`: Size parameter for operations
|
|
286
|
+
- `--force`: Force an action
|
|
287
|
+
- `--delete`: Enable deletion
|
|
288
|
+
- `--quiet`: Suppress non-error output
|
|
289
|
+
- `--debug`: Print debug output
|
|
290
|
+
- `--yes`: Answer yes to all prompts
|
|
291
|
+
- `--config`: Configuration file or JSON string
|
|
292
|
+
- `--timeout`: Timeout for operations
|
|
293
|
+
- `--batch-size`: Number of items per batch
|
|
294
|
+
- `--no-input`: Do not prompt for user input
|
|
295
|
+
- `--threads`: Number of threads for processing
|
|
296
|
+
- `--processes`: Number of processes for processing
|
|
297
|
+
|
|
298
|
+
**Example Implementation:**
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
from argparse import ArgumentParser
|
|
302
|
+
from typing import Any
|
|
303
|
+
from winipedia_django.command import ABCBaseCommand
|
|
304
|
+
|
|
305
|
+
class ImportDataCommand(ABCBaseCommand):
|
|
306
|
+
"""Import data from a CSV file."""
|
|
307
|
+
|
|
308
|
+
def add_command_arguments(self, parser: ArgumentParser) -> None:
|
|
309
|
+
"""Add command-specific arguments."""
|
|
310
|
+
parser.add_argument(
|
|
311
|
+
'--file',
|
|
312
|
+
type=str,
|
|
313
|
+
required=True,
|
|
314
|
+
help='Path to CSV file to import'
|
|
315
|
+
)
|
|
316
|
+
parser.add_argument(
|
|
317
|
+
'--model',
|
|
318
|
+
type=str,
|
|
319
|
+
required=True,
|
|
320
|
+
choices=['user', 'product', 'order'],
|
|
321
|
+
help='Model to import data into'
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def handle_command(self, *args: Any, **options: Any) -> None:
|
|
325
|
+
"""Execute the import command."""
|
|
326
|
+
file_path = options['file']
|
|
327
|
+
model_name = options['model']
|
|
328
|
+
batch_size = options['batch_size'] or 1000
|
|
329
|
+
dry_run = options['dry_run']
|
|
330
|
+
threads = options['threads'] or 4
|
|
331
|
+
|
|
332
|
+
if not dry_run:
|
|
333
|
+
self.stdout.write(
|
|
334
|
+
self.style.WARNING(f'Importing {model_name} from {file_path}')
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
self.stdout.write(
|
|
338
|
+
self.style.WARNING('DRY RUN - No changes will be made')
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Your import logic here
|
|
342
|
+
self.stdout.write(
|
|
343
|
+
self.style.SUCCESS('Import completed successfully')
|
|
344
|
+
)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Usage:**
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
# Normal execution
|
|
351
|
+
python manage.py import_data --file data.csv --model user --batch-size 500
|
|
352
|
+
|
|
353
|
+
# Dry run
|
|
354
|
+
python manage.py import_data --file data.csv --model user --dry-run
|
|
355
|
+
|
|
356
|
+
# With threading
|
|
357
|
+
python manage.py import_data --file data.csv --model user --threads 8
|
|
358
|
+
|
|
359
|
+
# Quiet mode
|
|
360
|
+
python manage.py import_data --file data.csv --model user --quiet
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Database Utilities
|
|
364
|
+
|
|
365
|
+
The `database` module provides utilities for working with Django models and the database.
|
|
366
|
+
|
|
367
|
+
#### Model Introspection
|
|
368
|
+
|
|
369
|
+
**`get_model_meta(model)`**
|
|
370
|
+
|
|
371
|
+
Get the Django model metadata options object.
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
from winipedia_django.database import get_model_meta
|
|
375
|
+
|
|
376
|
+
meta = get_model_meta(User)
|
|
377
|
+
print(meta.db_table) # 'auth_user'
|
|
378
|
+
print(meta.app_label) # 'auth'
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**`get_fields(model)`**
|
|
382
|
+
|
|
383
|
+
Get all fields from a model including relationships.
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
from winipedia_django.database import get_fields
|
|
387
|
+
|
|
388
|
+
fields = get_fields(User)
|
|
389
|
+
for field in fields:
|
|
390
|
+
if hasattr(field, 'name'):
|
|
391
|
+
print(f"Field: {field.name}, Type: {type(field).__name__}")
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**`get_field_names(fields)`**
|
|
395
|
+
|
|
396
|
+
Extract field names from field objects.
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
from winipedia_django.database import get_fields, get_field_names
|
|
400
|
+
|
|
401
|
+
fields = get_fields(User)
|
|
402
|
+
field_names = get_field_names(fields)
|
|
403
|
+
print(field_names) # ['id', 'username', 'email', 'is_active', ...]
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
#### Model Sorting
|
|
407
|
+
|
|
408
|
+
**`topological_sort_models(models)`**
|
|
409
|
+
|
|
410
|
+
Sort Django models in dependency order using topological sorting.
|
|
411
|
+
|
|
412
|
+
```python
|
|
413
|
+
from winipedia_django.database import topological_sort_models
|
|
414
|
+
|
|
415
|
+
# Models with dependencies
|
|
416
|
+
models = [Review, Book, Author, Publisher]
|
|
417
|
+
sorted_models = topological_sort_models(models)
|
|
418
|
+
# Result: [Author, Publisher, Book, Review]
|
|
419
|
+
# Dependencies are created before dependents
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
#### Database Operations
|
|
423
|
+
|
|
424
|
+
**`execute_sql(sql, params=None)`**
|
|
425
|
+
|
|
426
|
+
Execute raw SQL query and return column names with results.
|
|
427
|
+
|
|
428
|
+
```python
|
|
429
|
+
from winipedia_django.database import execute_sql
|
|
430
|
+
|
|
431
|
+
sql = "SELECT id, username FROM auth_user WHERE is_active = %(active)s"
|
|
432
|
+
params = {"active": True}
|
|
433
|
+
columns, rows = execute_sql(sql, params)
|
|
434
|
+
|
|
435
|
+
for row in rows:
|
|
436
|
+
print(f"ID: {row[0]}, Username: {row[1]}")
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
#### Model Hashing
|
|
440
|
+
|
|
441
|
+
**`hash_model_instance(instance, fields)`**
|
|
442
|
+
|
|
443
|
+
Hash a model instance based on its field values for comparison.
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
from winipedia_django.database import hash_model_instance, get_fields
|
|
447
|
+
|
|
448
|
+
user1 = User(name="Alice", email="alice@example.com")
|
|
449
|
+
user2 = User(name="Alice", email="alice@example.com")
|
|
450
|
+
|
|
451
|
+
fields = get_fields(User)
|
|
452
|
+
hash1 = hash_model_instance(user1, fields)
|
|
453
|
+
hash2 = hash_model_instance(user2, fields)
|
|
454
|
+
|
|
455
|
+
print(hash1 == hash2) # True - same field values
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
#### BaseModel
|
|
459
|
+
|
|
460
|
+
An abstract base model with automatic timestamp fields.
|
|
461
|
+
|
|
462
|
+
```python
|
|
463
|
+
from winipedia_django.database import BaseModel
|
|
464
|
+
from django.db import models
|
|
465
|
+
|
|
466
|
+
class Article(BaseModel):
|
|
467
|
+
"""Article model with automatic timestamps."""
|
|
468
|
+
|
|
469
|
+
title = models.CharField(max_length=200)
|
|
470
|
+
content = models.TextField()
|
|
471
|
+
author = models.CharField(max_length=100)
|
|
472
|
+
|
|
473
|
+
class Meta:
|
|
474
|
+
app_label = 'blog'
|
|
475
|
+
|
|
476
|
+
def __str__(self) -> str:
|
|
477
|
+
return self.title
|
|
478
|
+
|
|
479
|
+
# Usage
|
|
480
|
+
article = Article(title="My Article", content="...", author="John")
|
|
481
|
+
article.save()
|
|
482
|
+
|
|
483
|
+
print(article.created_at) # Automatically set
|
|
484
|
+
print(article.updated_at) # Automatically set
|
|
485
|
+
print(str(article)) # Article(id=1, created_at=..., updated_at=..., title=My Article, ...)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## Usage Examples
|
|
489
|
+
|
|
490
|
+
### Example 1: Bulk Import with Progress Tracking
|
|
491
|
+
|
|
492
|
+
```python
|
|
493
|
+
from winipedia_django.bulk import bulk_create_in_steps
|
|
494
|
+
from myapp.models import Product
|
|
495
|
+
import csv
|
|
496
|
+
|
|
497
|
+
def import_products(csv_file):
|
|
498
|
+
"""Import products from CSV file."""
|
|
499
|
+
products = []
|
|
500
|
+
|
|
501
|
+
with open(csv_file, 'r') as f:
|
|
502
|
+
reader = csv.DictReader(f)
|
|
503
|
+
for row in reader:
|
|
504
|
+
products.append(Product(
|
|
505
|
+
name=row['name'],
|
|
506
|
+
price=float(row['price']),
|
|
507
|
+
description=row['description']
|
|
508
|
+
))
|
|
509
|
+
|
|
510
|
+
# Create in steps of 500
|
|
511
|
+
created = bulk_create_in_steps(Product, products, step=500)
|
|
512
|
+
print(f"Successfully imported {len(created)} products")
|
|
513
|
+
return created
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Example 2: Efficient Data Synchronization
|
|
517
|
+
|
|
518
|
+
```python
|
|
519
|
+
from winipedia_django.bulk import get_differences_between_bulks
|
|
520
|
+
from winipedia_django.database import get_fields
|
|
521
|
+
|
|
522
|
+
def sync_users(remote_users, local_users):
|
|
523
|
+
"""Synchronize users between remote and local."""
|
|
524
|
+
fields = get_fields(User)
|
|
525
|
+
|
|
526
|
+
only_remote, only_local, common_remote, common_local = (
|
|
527
|
+
get_differences_between_bulks(remote_users, local_users, fields)
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
# Create new users
|
|
531
|
+
if only_remote:
|
|
532
|
+
bulk_create_in_steps(User, only_remote)
|
|
533
|
+
|
|
534
|
+
# Delete removed users
|
|
535
|
+
if only_local:
|
|
536
|
+
bulk_delete_in_steps(User, only_local)
|
|
537
|
+
|
|
538
|
+
print(f"Created: {len(only_remote)}, Deleted: {len(only_local)}")
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Example 3: Safe Deletion Preview
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
from winipedia_django.bulk import simulate_bulk_deletion
|
|
545
|
+
|
|
546
|
+
def preview_deletion(user_ids):
|
|
547
|
+
"""Preview what would be deleted."""
|
|
548
|
+
users = User.objects.filter(id__in=user_ids)
|
|
549
|
+
preview = simulate_bulk_deletion(User, list(users))
|
|
550
|
+
|
|
551
|
+
print("Deletion Preview:")
|
|
552
|
+
for model, objects in preview.items():
|
|
553
|
+
print(f" {model.__name__}: {len(objects)} objects")
|
|
554
|
+
|
|
555
|
+
return preview
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Example 4: Complex Data Migration
|
|
559
|
+
|
|
560
|
+
```python
|
|
561
|
+
from winipedia_django.bulk import bulk_create_bulks_in_steps
|
|
562
|
+
from winipedia_django.database import topological_sort_models
|
|
563
|
+
|
|
564
|
+
def migrate_data(source_db):
|
|
565
|
+
"""Migrate data from source database."""
|
|
566
|
+
# Fetch data from source
|
|
567
|
+
authors = fetch_authors(source_db)
|
|
568
|
+
publishers = fetch_publishers(source_db)
|
|
569
|
+
books = fetch_books(source_db)
|
|
570
|
+
|
|
571
|
+
# Create in dependency order
|
|
572
|
+
bulk_by_class = {
|
|
573
|
+
Author: authors,
|
|
574
|
+
Publisher: publishers,
|
|
575
|
+
Book: books,
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
results = bulk_create_bulks_in_steps(bulk_by_class, step=1000)
|
|
579
|
+
|
|
580
|
+
for model, instances in results.items():
|
|
581
|
+
print(f"Migrated {len(instances)} {model.__name__} objects")
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## API Reference
|
|
585
|
+
|
|
586
|
+
### Bulk Module (`winipedia_django.bulk`)
|
|
587
|
+
|
|
588
|
+
| Function | Purpose | Returns |
|
|
589
|
+
|----------|---------|---------|
|
|
590
|
+
| `bulk_create_in_steps()` | Create objects in chunks | `list[Model]` |
|
|
591
|
+
| `bulk_update_in_steps()` | Update objects in chunks | `int` (count) |
|
|
592
|
+
| `bulk_delete_in_steps()` | Delete objects in chunks | `tuple[int, dict]` |
|
|
593
|
+
| `bulk_create_bulks_in_steps()` | Create multiple models respecting dependencies | `dict[type[Model], list[Model]]` |
|
|
594
|
+
| `get_differences_between_bulks()` | Compare two model lists | `tuple[list, list, list, list]` |
|
|
595
|
+
| `simulate_bulk_deletion()` | Preview cascade deletions | `dict[type[Model], set[Model]]` |
|
|
596
|
+
| `multi_simulate_bulk_deletion()` | Preview deletions for multiple models | `dict[type[Model], set[Model]]` |
|
|
597
|
+
|
|
598
|
+
### Command Module (`winipedia_django.command`)
|
|
599
|
+
|
|
600
|
+
| Class | Purpose |
|
|
601
|
+
|-------|---------|
|
|
602
|
+
| `ABCBaseCommand` | Abstract base class for management commands |
|
|
603
|
+
|
|
604
|
+
### Database Module (`winipedia_django.database`)
|
|
605
|
+
|
|
606
|
+
| Function | Purpose | Returns |
|
|
607
|
+
|----------|---------|---------|
|
|
608
|
+
| `get_model_meta()` | Get model metadata | `Options[Model]` |
|
|
609
|
+
| `get_fields()` | Get all model fields | `list[Field]` |
|
|
610
|
+
| `get_field_names()` | Extract field names | `list[str]` |
|
|
611
|
+
| `topological_sort_models()` | Sort models by dependencies | `list[type[Model]]` |
|
|
612
|
+
| `execute_sql()` | Execute raw SQL | `tuple[list[str], list[Any]]` |
|
|
613
|
+
| `hash_model_instance()` | Hash model instance | `int` |
|
|
614
|
+
|
|
615
|
+
| Class | Purpose |
|
|
616
|
+
|-------|---------|
|
|
617
|
+
| `BaseModel` | Abstract base model with timestamps |
|
|
618
|
+
|
|
619
|
+
## Best Practices
|
|
620
|
+
|
|
621
|
+
### 1. Bulk Operations
|
|
622
|
+
|
|
623
|
+
✅ **Do:**
|
|
624
|
+
- Use appropriate step sizes (default 1000 is usually good)
|
|
625
|
+
- Use `bulk_create_bulks_in_steps()` for related models
|
|
626
|
+
- Preview deletions with `simulate_bulk_deletion()` before actual deletion
|
|
627
|
+
- Use `--dry-run` flag in commands to test before executing
|
|
628
|
+
|
|
629
|
+
❌ **Don't:**
|
|
630
|
+
- Use bulk operations inside nested transactions without careful consideration
|
|
631
|
+
- Create very large bulks without chunking
|
|
632
|
+
- Ignore cascade deletion warnings
|
|
633
|
+
|
|
634
|
+
### 2. Management Commands
|
|
635
|
+
|
|
636
|
+
✅ **Do:**
|
|
637
|
+
- Use `--dry-run` for destructive operations
|
|
638
|
+
- Provide meaningful `--batch-size` options
|
|
639
|
+
- Use `--quiet` for automation scripts
|
|
640
|
+
- Log important operations
|
|
641
|
+
|
|
642
|
+
❌ **Don't:**
|
|
643
|
+
- Override `add_arguments()` or `handle()` methods
|
|
644
|
+
- Ignore the template method pattern
|
|
645
|
+
- Skip implementing abstract methods
|
|
646
|
+
|
|
647
|
+
### 3. Database Operations
|
|
648
|
+
|
|
649
|
+
✅ **Do:**
|
|
650
|
+
- Use `topological_sort_models()` when creating related models
|
|
651
|
+
- Use `get_fields()` for introspection instead of hardcoding field names
|
|
652
|
+
- Use `execute_sql()` with parameter binding for security
|
|
653
|
+
- Inherit from `BaseModel` for automatic timestamps
|
|
654
|
+
|
|
655
|
+
❌ **Don't:**
|
|
656
|
+
- Use raw string concatenation in SQL queries
|
|
657
|
+
- Assume field order in models
|
|
658
|
+
- Manually manage created_at/updated_at fields
|
|
659
|
+
|
|
660
|
+
## Performance Tips
|
|
661
|
+
|
|
662
|
+
1. **Adjust Step Size**: Larger steps are faster but use more memory
|
|
663
|
+
```python
|
|
664
|
+
# For large objects, use smaller steps
|
|
665
|
+
bulk_create_in_steps(LargeModel, objects, step=100)
|
|
666
|
+
|
|
667
|
+
# For small objects, use larger steps
|
|
668
|
+
bulk_create_in_steps(SmallModel, objects, step=5000)
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
2. **Use Threading**: Leverage multithreading for I/O-bound operations
|
|
672
|
+
```bash
|
|
673
|
+
python manage.py my_command --threads 8
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
3. **Batch Processing**: Process data in batches to reduce memory usage
|
|
677
|
+
```python
|
|
678
|
+
for batch in get_step_chunks(large_dataset, 1000):
|
|
679
|
+
process_batch(batch)
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
## Contributing
|
|
683
|
+
|
|
684
|
+
Contributions are welcome! Please ensure:
|
|
685
|
+
|
|
686
|
+
- Code follows the project's style guide (enforced by ruff)
|
|
687
|
+
- All tests pass: `pytest`
|
|
688
|
+
- Type checking passes: `mypy`
|
|
689
|
+
- Code is documented with docstrings
|
|
690
|
+
|
|
691
|
+
## License
|
|
692
|
+
|
|
693
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
694
|
+
|
|
695
|
+
## Support
|
|
696
|
+
|
|
697
|
+
For issues, questions, or suggestions, please open an issue on the project repository.
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
**Made with ❤️ by Winipedia**
|
|
702
|
+
|
|
@@ -3,7 +3,7 @@ winipedia_django/bulk.py,sha256=PmGJE6g1S7_fqqWOWRGV9uExwMcexb5SeR0Hj0k46z0,1987
|
|
|
3
3
|
winipedia_django/command.py,sha256=WS9kO_0uvimH7fnxy5GJZp0mREViPPoodBT_4l8DCzM,13461
|
|
4
4
|
winipedia_django/database.py,sha256=fvjLy0hrR5SEgJ9inGB2tbXblipvpMhX84tS3yjrdEs,10646
|
|
5
5
|
winipedia_django/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
winipedia_django-0.2.
|
|
7
|
-
winipedia_django-0.2.
|
|
8
|
-
winipedia_django-0.2.
|
|
9
|
-
winipedia_django-0.2.
|
|
6
|
+
winipedia_django-0.2.2.dist-info/METADATA,sha256=qb3TOiopwaKW2VJvJiCc1hvGOdMqcusfm8bNCOq4L68,19976
|
|
7
|
+
winipedia_django-0.2.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
8
|
+
winipedia_django-0.2.2.dist-info/licenses/LICENSE,sha256=o316mE2gGzd__JT69p7S_zlOmKiHh8YjpImCCcWyTvM,1066
|
|
9
|
+
winipedia_django-0.2.2.dist-info/RECORD,,
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: winipedia-django
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: A utils package for django
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Author: Winipedia
|
|
8
|
-
Author-email: win.steveker@gmx.de
|
|
9
|
-
Requires-Python: >=3.12
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
-
Requires-Dist: django (>=5.2.7,<6.0.0)
|
|
15
|
-
Requires-Dist: winipedia-utils (>=0.2.10,<0.3.0)
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
|
|
18
|
-
# winipedia_django
|
|
19
|
-
A utils package for django
|
|
20
|
-
|
|
File without changes
|
|
File without changes
|