wtinydb 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.
- wtinydb-0.1.0/LICENSE +21 -0
- wtinydb-0.1.0/PKG-INFO +93 -0
- wtinydb-0.1.0/README.md +75 -0
- wtinydb-0.1.0/pyproject.toml +33 -0
- wtinydb-0.1.0/setup.cfg +4 -0
- wtinydb-0.1.0/tests/test_async.py +50 -0
- wtinydb-0.1.0/tests/test_cli.py +32 -0
- wtinydb-0.1.0/tests/test_database.py +140 -0
- wtinydb-0.1.0/tests/test_models.py +42 -0
- wtinydb-0.1.0/tests/test_query.py +90 -0
- wtinydb-0.1.0/wtinydb/__init__.py +23 -0
- wtinydb-0.1.0/wtinydb/cli/__init__.py +1 -0
- wtinydb-0.1.0/wtinydb/cli/main.py +60 -0
- wtinydb-0.1.0/wtinydb/core/__init__.py +1 -0
- wtinydb-0.1.0/wtinydb/core/async_db.py +106 -0
- wtinydb-0.1.0/wtinydb/core/database.py +410 -0
- wtinydb-0.1.0/wtinydb/core/query.py +91 -0
- wtinydb-0.1.0/wtinydb/exceptions.py +29 -0
- wtinydb-0.1.0/wtinydb/models.py +44 -0
- wtinydb-0.1.0/wtinydb.egg-info/PKG-INFO +93 -0
- wtinydb-0.1.0/wtinydb.egg-info/SOURCES.txt +23 -0
- wtinydb-0.1.0/wtinydb.egg-info/dependency_links.txt +1 -0
- wtinydb-0.1.0/wtinydb.egg-info/entry_points.txt +2 -0
- wtinydb-0.1.0/wtinydb.egg-info/requires.txt +7 -0
- wtinydb-0.1.0/wtinydb.egg-info/top_level.txt +1 -0
wtinydb-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Rodriguez
|
|
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.
|
wtinydb-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wtinydb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pydantic-powered document database abstraction layer on top of TinyDB
|
|
5
|
+
Author-email: "WILLIAM R." <william@ecapturedtech.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: tinydb,pydantic,nosql,document-database,orm,database
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: tinydb>=4.8.0
|
|
12
|
+
Requires-Dist: pydantic>=2.0.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# WTinyDB: Lightweight Pydantic Document Database Engine
|
|
20
|
+
|
|
21
|
+
`wtinydb` is a high-level, document-oriented NoSQL database abstraction built on top of **TinyDB** and **Pydantic**. It provides schema-validated CRUD operations, fluent query builders, async support, audit mixins, and command-line management for embedded Python applications.
|
|
22
|
+
|
|
23
|
+
## Key Technologies & Libraries
|
|
24
|
+
|
|
25
|
+
- **[TinyDB](https://tinydb.readthedocs.io/)**: Pure Python document-oriented database storing records as JSON.
|
|
26
|
+
- **[Pydantic v2](https://docs.pydantic.dev/)**: Data validation, settings management, and JSON-schema document serialization.
|
|
27
|
+
- **[Pytest](https://docs.pytest.org/)**: Modern Python testing framework.
|
|
28
|
+
- **[Pytest-Cov](https://pytest-cov.readthedocs.io/)**: Code coverage measurement for pytest.
|
|
29
|
+
- **[Docker](https://www.docker.com/)**: Containerization for reproducible testing environments.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **Pydantic Model Mapping**: Automatic serialization, deserialization, and schema validation.
|
|
36
|
+
- **Fluent Query Builder (`Q`)**: Intuitive query building with comparison operators, regex matching, and text search.
|
|
37
|
+
- **Async Support (`AsyncWTinyDB`)**: Non-blocking thread-pool execution for FastAPI and Starlette apps.
|
|
38
|
+
- **Soft Delete & Audit Mixins**: Built-in support for `created_at`, `updated_at`, `is_deleted`, and `deleted_at`.
|
|
39
|
+
- **Thread Safety**: Integrated reentrant locks for safe multi-threaded access.
|
|
40
|
+
- **CLI Utility**: Command-line tool `wtinydb` to inspect, count, and export JSON tables.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Quickstart Example
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from pydantic import BaseModel, Field
|
|
48
|
+
from wtinydb import WTinyDB, Q, SoftDeleteMixin
|
|
49
|
+
|
|
50
|
+
# Define document schema
|
|
51
|
+
class User(SoftDeleteMixin, BaseModel):
|
|
52
|
+
name: str
|
|
53
|
+
email: str = Field(description="User email")
|
|
54
|
+
age: int = 18
|
|
55
|
+
|
|
56
|
+
# Initialize WTinyDB repository (in-memory or file-backed)
|
|
57
|
+
db = WTinyDB(User, in_memory=True)
|
|
58
|
+
|
|
59
|
+
# 1. Insert
|
|
60
|
+
user = db.insert(User(name="Alice", email="alice@example.com", age=30))
|
|
61
|
+
|
|
62
|
+
# 2. Query
|
|
63
|
+
results = db.find(Q("age").gte("age", 25))
|
|
64
|
+
|
|
65
|
+
# 3. Soft Delete & Restore
|
|
66
|
+
db.delete(1, hard=False)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Running Unit Tests & Coverage
|
|
72
|
+
|
|
73
|
+
### Local Pytest Execution
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Run pytest test suite
|
|
77
|
+
pytest tests/
|
|
78
|
+
|
|
79
|
+
# Calculate code coverage
|
|
80
|
+
./scripts/run_coverage.sh
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Docker Containerized Test Execution
|
|
84
|
+
|
|
85
|
+
All unit tests can be executed inside an isolated Docker container:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
./scripts/run_tests_docker.sh
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
*Part of the wisrovi SUITE ecosystem.*
|
wtinydb-0.1.0/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# WTinyDB: Lightweight Pydantic Document Database Engine
|
|
2
|
+
|
|
3
|
+
`wtinydb` is a high-level, document-oriented NoSQL database abstraction built on top of **TinyDB** and **Pydantic**. It provides schema-validated CRUD operations, fluent query builders, async support, audit mixins, and command-line management for embedded Python applications.
|
|
4
|
+
|
|
5
|
+
## Key Technologies & Libraries
|
|
6
|
+
|
|
7
|
+
- **[TinyDB](https://tinydb.readthedocs.io/)**: Pure Python document-oriented database storing records as JSON.
|
|
8
|
+
- **[Pydantic v2](https://docs.pydantic.dev/)**: Data validation, settings management, and JSON-schema document serialization.
|
|
9
|
+
- **[Pytest](https://docs.pytest.org/)**: Modern Python testing framework.
|
|
10
|
+
- **[Pytest-Cov](https://pytest-cov.readthedocs.io/)**: Code coverage measurement for pytest.
|
|
11
|
+
- **[Docker](https://www.docker.com/)**: Containerization for reproducible testing environments.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Pydantic Model Mapping**: Automatic serialization, deserialization, and schema validation.
|
|
18
|
+
- **Fluent Query Builder (`Q`)**: Intuitive query building with comparison operators, regex matching, and text search.
|
|
19
|
+
- **Async Support (`AsyncWTinyDB`)**: Non-blocking thread-pool execution for FastAPI and Starlette apps.
|
|
20
|
+
- **Soft Delete & Audit Mixins**: Built-in support for `created_at`, `updated_at`, `is_deleted`, and `deleted_at`.
|
|
21
|
+
- **Thread Safety**: Integrated reentrant locks for safe multi-threaded access.
|
|
22
|
+
- **CLI Utility**: Command-line tool `wtinydb` to inspect, count, and export JSON tables.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quickstart Example
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from pydantic import BaseModel, Field
|
|
30
|
+
from wtinydb import WTinyDB, Q, SoftDeleteMixin
|
|
31
|
+
|
|
32
|
+
# Define document schema
|
|
33
|
+
class User(SoftDeleteMixin, BaseModel):
|
|
34
|
+
name: str
|
|
35
|
+
email: str = Field(description="User email")
|
|
36
|
+
age: int = 18
|
|
37
|
+
|
|
38
|
+
# Initialize WTinyDB repository (in-memory or file-backed)
|
|
39
|
+
db = WTinyDB(User, in_memory=True)
|
|
40
|
+
|
|
41
|
+
# 1. Insert
|
|
42
|
+
user = db.insert(User(name="Alice", email="alice@example.com", age=30))
|
|
43
|
+
|
|
44
|
+
# 2. Query
|
|
45
|
+
results = db.find(Q("age").gte("age", 25))
|
|
46
|
+
|
|
47
|
+
# 3. Soft Delete & Restore
|
|
48
|
+
db.delete(1, hard=False)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Running Unit Tests & Coverage
|
|
54
|
+
|
|
55
|
+
### Local Pytest Execution
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Run pytest test suite
|
|
59
|
+
pytest tests/
|
|
60
|
+
|
|
61
|
+
# Calculate code coverage
|
|
62
|
+
./scripts/run_coverage.sh
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Docker Containerized Test Execution
|
|
66
|
+
|
|
67
|
+
All unit tests can be executed inside an isolated Docker container:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
./scripts/run_tests_docker.sh
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
*Part of the wisrovi SUITE ecosystem.*
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wtinydb"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pydantic-powered document database abstraction layer on top of TinyDB"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "WILLIAM R.", email = "william@ecapturedtech.com"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["tinydb", "pydantic", "nosql", "document-database", "orm", "database"]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"tinydb>=4.8.0",
|
|
18
|
+
"pydantic>=2.0.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=7.0.0",
|
|
24
|
+
"pytest-cov>=4.0.0",
|
|
25
|
+
"pytest-asyncio>=0.21.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
wtinydb = "wtinydb.cli.main:main"
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
33
|
+
python_files = ["test_*.py"]
|
wtinydb-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Unit tests for AsyncWTinyDB async database wrapper.
|
|
2
|
+
|
|
3
|
+
Validates non-blocking async CRUD operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from wtinydb import AsyncWTinyDB, WTinyDB
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Item(BaseModel):
|
|
13
|
+
"""Pydantic model for async testing."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
quantity: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.asyncio
|
|
20
|
+
async def test_async_crud_operations():
|
|
21
|
+
"""Validates asynchronous insert, get, update, count, and delete operations."""
|
|
22
|
+
sync_db = WTinyDB(Item, in_memory=True)
|
|
23
|
+
async_db = AsyncWTinyDB(sync_db)
|
|
24
|
+
|
|
25
|
+
# Async insert
|
|
26
|
+
inserted = await async_db.insert(Item(name="Widget", quantity=10))
|
|
27
|
+
assert inserted.name == "Widget"
|
|
28
|
+
|
|
29
|
+
# Async count
|
|
30
|
+
cnt = await async_db.count()
|
|
31
|
+
assert cnt == 1
|
|
32
|
+
|
|
33
|
+
# Async get
|
|
34
|
+
retrieved = await async_db.get(1)
|
|
35
|
+
assert retrieved.quantity == 10
|
|
36
|
+
|
|
37
|
+
# Async update
|
|
38
|
+
updated = await async_db.update(1, {"quantity": 15})
|
|
39
|
+
assert updated.quantity == 15
|
|
40
|
+
|
|
41
|
+
# Async get_all
|
|
42
|
+
all_items = await async_db.get_all()
|
|
43
|
+
assert len(all_items) == 1
|
|
44
|
+
|
|
45
|
+
# Async delete
|
|
46
|
+
deleted = await async_db.delete(1)
|
|
47
|
+
assert deleted is True
|
|
48
|
+
assert await async_db.count() == 0
|
|
49
|
+
|
|
50
|
+
await async_db.close()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Unit tests for WTinyDB command-line interface (CLI)."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from wtinydb import WTinyDB
|
|
7
|
+
from wtinydb.cli.main import main
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Task(BaseModel):
|
|
11
|
+
"""Task model for CLI test."""
|
|
12
|
+
|
|
13
|
+
title: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_cli_inspect_and_count(tmp_path):
|
|
17
|
+
"""Validates CLI inspect and count subcommands on a temporary JSON database file."""
|
|
18
|
+
db_file = str(tmp_path / "test_cli.json")
|
|
19
|
+
db = WTinyDB(Task, db_path=db_file)
|
|
20
|
+
db.insert(Task(title="Do homework"))
|
|
21
|
+
db.insert(Task(title="Buy milk"))
|
|
22
|
+
db.close()
|
|
23
|
+
|
|
24
|
+
# Test CLI inspect command
|
|
25
|
+
test_args = ["wtinydb", "inspect", db_file]
|
|
26
|
+
with patch.object(sys, "argv", test_args):
|
|
27
|
+
main()
|
|
28
|
+
|
|
29
|
+
# Test CLI count command
|
|
30
|
+
test_args_count = ["wtinydb", "count", db_file, "--table", "task"]
|
|
31
|
+
with patch.object(sys, "argv", test_args_count):
|
|
32
|
+
main()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Unit tests for WTinyDB synchronous operations.
|
|
2
|
+
|
|
3
|
+
This test file validates CRUD operations, soft-deletion handling,
|
|
4
|
+
batch inserts, document lookups, exceptions, and WMongo-compatible collection operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from wtinydb import WTinyDB, DocumentNotFoundError, SoftDeleteMixin
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class User(BaseModel):
|
|
15
|
+
"""Pydantic model representing a User document."""
|
|
16
|
+
|
|
17
|
+
name: str = Field(description="User full name")
|
|
18
|
+
email: str = Field(description="User email address")
|
|
19
|
+
age: int = Field(default=18, description="User age")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SoftUser(SoftDeleteMixin, BaseModel):
|
|
23
|
+
"""Pydantic model representing a User document with soft-delete capabilities."""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
role: str = "member"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_insert_and_get():
|
|
30
|
+
"""Validates model insertion and retrieval by doc_id."""
|
|
31
|
+
db = WTinyDB(User, in_memory=True)
|
|
32
|
+
user_data = User(name="Alice", email="alice@example.com", age=30)
|
|
33
|
+
inserted_user = db.insert(user_data)
|
|
34
|
+
|
|
35
|
+
assert inserted_user.name == "Alice"
|
|
36
|
+
assert inserted_user.email == "alice@example.com"
|
|
37
|
+
assert getattr(inserted_user, "_doc_id") == 1
|
|
38
|
+
|
|
39
|
+
retrieved_user = db.get(1)
|
|
40
|
+
assert retrieved_user.name == "Alice"
|
|
41
|
+
assert retrieved_user.age == 30
|
|
42
|
+
db.close()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_wmongo_collection_crud():
|
|
46
|
+
"""Validates WMongo-compatible collection CRUD operations (insert, find, update, delete)."""
|
|
47
|
+
db = WTinyDB(in_memory=True)
|
|
48
|
+
|
|
49
|
+
# Collection insert
|
|
50
|
+
doc_id = db.insert("customers", {"name": "John Doe", "status": "active"})
|
|
51
|
+
assert doc_id == 1
|
|
52
|
+
|
|
53
|
+
# Collection find
|
|
54
|
+
found = db.find("customers", {"status": "active"})
|
|
55
|
+
assert len(found) == 1
|
|
56
|
+
assert found[0]["name"] == "John Doe"
|
|
57
|
+
|
|
58
|
+
# Collection update
|
|
59
|
+
updated_count = db.update("customers", {"name": "John Doe"}, {"status": "inactive"})
|
|
60
|
+
assert updated_count == 1
|
|
61
|
+
|
|
62
|
+
# Collection delete
|
|
63
|
+
deleted_count = db.delete("customers", {"status": "inactive"})
|
|
64
|
+
assert deleted_count == 1
|
|
65
|
+
|
|
66
|
+
db.close()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_insert_many_and_get_all():
|
|
70
|
+
"""Validates batch insertion of multiple documents."""
|
|
71
|
+
db = WTinyDB(User, in_memory=True)
|
|
72
|
+
users = [
|
|
73
|
+
User(name="Bob", email="bob@example.com", age=25),
|
|
74
|
+
User(name="Charlie", email="charlie@example.com", age=35),
|
|
75
|
+
]
|
|
76
|
+
inserted = db.insert_many(users)
|
|
77
|
+
|
|
78
|
+
assert len(inserted) == 2
|
|
79
|
+
assert db.count() == 2
|
|
80
|
+
|
|
81
|
+
all_users = db.get_all()
|
|
82
|
+
assert len(all_users) == 2
|
|
83
|
+
assert all_users[0].name == "Bob"
|
|
84
|
+
assert all_users[1].name == "Charlie"
|
|
85
|
+
db.close()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_get_by_field():
|
|
89
|
+
"""Validates document lookup by specific field name and value."""
|
|
90
|
+
db = WTinyDB(User, in_memory=True)
|
|
91
|
+
db.insert(User(name="David", email="david@example.com", age=40))
|
|
92
|
+
|
|
93
|
+
user = db.get_by_field("email", "david@example.com")
|
|
94
|
+
assert user is not None
|
|
95
|
+
assert user.name == "David"
|
|
96
|
+
|
|
97
|
+
missing_user = db.get_by_field("email", "nonexistent@example.com")
|
|
98
|
+
assert missing_user is None
|
|
99
|
+
db.close()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_update_document():
|
|
103
|
+
"""Validates document modification using dictionary update data."""
|
|
104
|
+
db = WTinyDB(User, in_memory=True)
|
|
105
|
+
inserted = db.insert(User(name="Eve", email="eve@example.com", age=22))
|
|
106
|
+
|
|
107
|
+
updated = db.update(1, {"age": 23})
|
|
108
|
+
assert updated.age == 23
|
|
109
|
+
|
|
110
|
+
retrieved = db.get(1)
|
|
111
|
+
assert retrieved.age == 23
|
|
112
|
+
db.close()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_soft_delete():
|
|
116
|
+
"""Validates soft-delete functionality using SoftDeleteMixin."""
|
|
117
|
+
db = WTinyDB(SoftUser, in_memory=True)
|
|
118
|
+
user = db.insert(SoftUser(name="Frank", role="admin"))
|
|
119
|
+
|
|
120
|
+
db.delete(1, hard=False)
|
|
121
|
+
|
|
122
|
+
with pytest.raises(DocumentNotFoundError):
|
|
123
|
+
db.get(1)
|
|
124
|
+
|
|
125
|
+
assert len(db.get_all(include_deleted=False)) == 0
|
|
126
|
+
|
|
127
|
+
all_docs = db.get_all(include_deleted=True)
|
|
128
|
+
assert len(all_docs) == 1
|
|
129
|
+
assert all_docs[0].is_deleted is True
|
|
130
|
+
db.close()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_document_not_found_exception():
|
|
134
|
+
"""Validates that accessing a non-existent document ID raises DocumentNotFoundError."""
|
|
135
|
+
db = WTinyDB(User, in_memory=True)
|
|
136
|
+
|
|
137
|
+
with pytest.raises(DocumentNotFoundError):
|
|
138
|
+
db.get(999)
|
|
139
|
+
|
|
140
|
+
db.close()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Unit tests for TimestampMixin, SoftDeleteMixin, and AuditMixin."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from wtinydb.models import AuditMixin, SoftDeleteMixin, TimestampMixin
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SampleTimestampDoc(TimestampMixin, BaseModel):
|
|
8
|
+
"""Model using TimestampMixin."""
|
|
9
|
+
|
|
10
|
+
name: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SampleAuditDoc(AuditMixin, BaseModel):
|
|
14
|
+
"""Model using AuditMixin."""
|
|
15
|
+
|
|
16
|
+
title: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_timestamp_mixin():
|
|
20
|
+
"""Validates default timestamp creation and touch functionality."""
|
|
21
|
+
doc = SampleTimestampDoc(name="test")
|
|
22
|
+
assert doc.created_at is not None
|
|
23
|
+
assert doc.updated_at is not None
|
|
24
|
+
|
|
25
|
+
initial_updated = doc.updated_at
|
|
26
|
+
doc.touch()
|
|
27
|
+
assert doc.updated_at >= initial_updated
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_audit_mixin():
|
|
31
|
+
"""Validates soft deletion and restoration capabilities in AuditMixin."""
|
|
32
|
+
doc = SampleAuditDoc(title="audit test")
|
|
33
|
+
assert doc.is_deleted is False
|
|
34
|
+
assert doc.deleted_at is None
|
|
35
|
+
|
|
36
|
+
doc.soft_delete()
|
|
37
|
+
assert doc.is_deleted is True
|
|
38
|
+
assert doc.deleted_at is not None
|
|
39
|
+
|
|
40
|
+
doc.restore()
|
|
41
|
+
assert doc.is_deleted is False
|
|
42
|
+
assert doc.deleted_at is None
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Unit tests for QueryBuilder and Q helper functionality.
|
|
2
|
+
|
|
3
|
+
This file validates fluent query construction, comparison operators,
|
|
4
|
+
regex matching, nested dotted path querying, and list containment queries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from wtinydb import Q, QueryBuilder, WTinyDB
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Product(BaseModel):
|
|
12
|
+
"""Pydantic model representing a product in inventory."""
|
|
13
|
+
|
|
14
|
+
title: str
|
|
15
|
+
price: float
|
|
16
|
+
category: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Geo(BaseModel):
|
|
20
|
+
lat: float
|
|
21
|
+
lon: float
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Address(BaseModel):
|
|
25
|
+
city: str
|
|
26
|
+
geo: Geo
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Organization(BaseModel):
|
|
30
|
+
name: str
|
|
31
|
+
address: Address
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_query_builder_operators():
|
|
35
|
+
"""Validates equality, greater-than, and list comparison operators with QueryBuilder."""
|
|
36
|
+
db = WTinyDB(Product, in_memory=True)
|
|
37
|
+
|
|
38
|
+
db.insert(Product(title="Laptop", price=999.99, category="electronics"))
|
|
39
|
+
db.insert(Product(title="Mouse", price=25.00, category="electronics"))
|
|
40
|
+
db.insert(Product(title="Book", price=15.00, category="books"))
|
|
41
|
+
|
|
42
|
+
books = db.find(Q("category").eq("category", "books"))
|
|
43
|
+
assert len(books) == 1
|
|
44
|
+
assert books[0].title == "Book"
|
|
45
|
+
|
|
46
|
+
expensive = db.find(Q("price").gte("price", 100.00))
|
|
47
|
+
assert len(expensive) == 1
|
|
48
|
+
assert expensive[0].title == "Laptop"
|
|
49
|
+
|
|
50
|
+
items = db.find(Q("category").in_list("category", ["electronics", "books"]))
|
|
51
|
+
assert len(items) == 3
|
|
52
|
+
|
|
53
|
+
db.close()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_query_regex_and_text_search():
|
|
57
|
+
"""Validates regex pattern matching and text search functionality."""
|
|
58
|
+
db = WTinyDB(Product, in_memory=True)
|
|
59
|
+
db.insert(Product(title="Wireless Keyboard", price=45.00, category="electronics"))
|
|
60
|
+
db.insert(Product(title="Mechanical Keyboard", price=85.00, category="electronics"))
|
|
61
|
+
|
|
62
|
+
keyboards = db.find(Q("title").search_text("title", "keyboard"))
|
|
63
|
+
assert len(keyboards) == 2
|
|
64
|
+
|
|
65
|
+
wireless = db.find(Q("title").matches("title", r"^Wireless"))
|
|
66
|
+
assert len(wireless) == 1
|
|
67
|
+
assert wireless[0].title == "Wireless Keyboard"
|
|
68
|
+
|
|
69
|
+
db.close()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_nested_dotted_path_query():
|
|
73
|
+
"""Validates querying N-level nested JSON fields using dotted notation paths."""
|
|
74
|
+
db = WTinyDB(Organization, in_memory=True)
|
|
75
|
+
org = Organization(
|
|
76
|
+
name="Acme",
|
|
77
|
+
address=Address(city="Madrid", geo=Geo(lat=40.4168, lon=-3.7038)),
|
|
78
|
+
)
|
|
79
|
+
db.insert(org)
|
|
80
|
+
|
|
81
|
+
# Query 3-level deep nested field
|
|
82
|
+
found_city = db.find(Q("address.city").eq("address.city", "Madrid"))
|
|
83
|
+
assert len(found_city) == 1
|
|
84
|
+
assert found_city[0].name == "Acme"
|
|
85
|
+
|
|
86
|
+
# Query 4-level deep nested numeric field
|
|
87
|
+
found_geo = db.find(Q("address.geo.lat").gte("address.geo.lat", 40.0))
|
|
88
|
+
assert len(found_geo) == 1
|
|
89
|
+
|
|
90
|
+
db.close()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""WTinyDB package initialization."""
|
|
2
|
+
|
|
3
|
+
from wtinydb.core.async_db import AsyncWTinyDB
|
|
4
|
+
from wtinydb.core.database import WTinyDB
|
|
5
|
+
from wtinydb.core.query import Q, QueryBuilder
|
|
6
|
+
from wtinydb.exceptions import DocumentNotFoundError, StorageError, ValidationError, WTinyDBError
|
|
7
|
+
from wtinydb.models import AuditMixin, SoftDeleteMixin, TimestampMixin
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"WTinyDB",
|
|
13
|
+
"AsyncWTinyDB",
|
|
14
|
+
"QueryBuilder",
|
|
15
|
+
"Q",
|
|
16
|
+
"TimestampMixin",
|
|
17
|
+
"SoftDeleteMixin",
|
|
18
|
+
"AuditMixin",
|
|
19
|
+
"WTinyDBError",
|
|
20
|
+
"DocumentNotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"StorageError",
|
|
23
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package for WTinyDB."""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""WTinyDB Command Line Interface (CLI)."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from tinydb import TinyDB
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
"""Main CLI entrypoint for WTinyDB commands."""
|
|
11
|
+
parser = argparse.ArgumentParser(description="WTinyDB CLI Manager")
|
|
12
|
+
subparsers = parser.add_subparsers(dest="command", help="Subcommand to run")
|
|
13
|
+
|
|
14
|
+
# Command: inspect
|
|
15
|
+
inspect_parser = subparsers.add_parser("inspect", help="Inspect tables in a TinyDB JSON database file")
|
|
16
|
+
inspect_parser.add_argument("db_path", help="Path to TinyDB JSON file")
|
|
17
|
+
|
|
18
|
+
# Command: count
|
|
19
|
+
count_parser = subparsers.add_parser("count", help="Count documents in a table")
|
|
20
|
+
count_parser.add_argument("db_path", help="Path to TinyDB JSON file")
|
|
21
|
+
count_parser.add_argument("--table", default="_default", help="Table name (default: _default)")
|
|
22
|
+
|
|
23
|
+
# Command: export
|
|
24
|
+
export_parser = subparsers.add_parser("export", help="Export table content as formatted JSON")
|
|
25
|
+
export_parser.add_argument("db_path", help="Path to TinyDB JSON file")
|
|
26
|
+
export_parser.add_argument("--table", default="_default", help="Table name")
|
|
27
|
+
|
|
28
|
+
args = parser.parse_args()
|
|
29
|
+
|
|
30
|
+
if not args.command:
|
|
31
|
+
parser.print_help()
|
|
32
|
+
sys.exit(0)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
db = TinyDB(args.db_path)
|
|
36
|
+
if args.command == "inspect":
|
|
37
|
+
tables = list(db.tables())
|
|
38
|
+
print(f"Database: {args.db_path}")
|
|
39
|
+
print(f"Tables found: {len(tables)}")
|
|
40
|
+
for t in tables:
|
|
41
|
+
table_obj = db.table(t)
|
|
42
|
+
print(f" - Table '{t}': {len(table_obj)} document(s)")
|
|
43
|
+
|
|
44
|
+
elif args.command == "count":
|
|
45
|
+
table_obj = db.table(args.table)
|
|
46
|
+
print(f"Table '{args.table}' contains {len(table_obj)} document(s).")
|
|
47
|
+
|
|
48
|
+
elif args.command == "export":
|
|
49
|
+
table_obj = db.table(args.table)
|
|
50
|
+
docs = table_obj.all()
|
|
51
|
+
print(json.dumps(docs, indent=2))
|
|
52
|
+
|
|
53
|
+
db.close()
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core modules for WTinyDB."""
|