Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
627dea1
Refactor web docs layout and improve content (#36)
seapagan Sep 28, 2024
37b213f
required modifications to make 'pk' non-optional
seapagan Sep 30, 2024
3258799
use pydantic mypy plugin
seapagan Sep 30, 2024
35c2969
fix mypy in pre-commit
seapagan Sep 30, 2024
9c99d61
wip on fixing tests
seapagan Sep 30, 2024
82b6208
make pk non-optional and default to 0
seapagan Sep 30, 2024
58858b9
add pytest hook to clear screen before each test run
seapagan Sep 30, 2024
b4b7d70
ensure insert fails on duplicate pk
seapagan Sep 30, 2024
ac41411
fix final existing tests
seapagan Sep 30, 2024
44cf95b
add specific model unit tests
seapagan Sep 30, 2024
9aa1c77
complete test coverage of changed code
seapagan Sep 30, 2024
a2f3861
fix last mypy issue
seapagan Sep 30, 2024
714e44b
add missing docstrings
seapagan Sep 30, 2024
b5f1262
update docs for pk changes
seapagan Sep 30, 2024
e904bfc
fix docs re update() method
seapagan Sep 30, 2024
12c23b2
required modifications to make 'pk' non-optional
seapagan Sep 30, 2024
8078178
use pydantic mypy plugin
seapagan Sep 30, 2024
d6c1d71
fix mypy in pre-commit
seapagan Sep 30, 2024
03727df
wip on fixing tests
seapagan Sep 30, 2024
04aa3ee
make pk non-optional and default to 0
seapagan Sep 30, 2024
6164250
add pytest hook to clear screen before each test run
seapagan Sep 30, 2024
eef5abe
ensure insert fails on duplicate pk
seapagan Sep 30, 2024
153fa2b
fix final existing tests
seapagan Sep 30, 2024
53f0f67
add specific model unit tests
seapagan Sep 30, 2024
55e9fd8
complete test coverage of changed code
seapagan Sep 30, 2024
358d705
fix last mypy issue
seapagan Sep 30, 2024
607a65e
add missing docstrings
seapagan Sep 30, 2024
9e18cbb
update docs for pk changes
seapagan Sep 30, 2024
2315d01
fix docs re update() method
seapagan Sep 30, 2024
c64cbfe
Merge branch 'no-optional-pk' of github.com:seapagan/sqliter-py into …
seapagan Sep 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ repos:
hooks:
- id: mypy
name: "run mypy"
additional_dependencies:
- pydantic

- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ Website](https://sqliter.grantramsay.dev)

> [!CAUTION]
> This project is still in the early stages of development and is lacking some
> planned functionality. Please use with caution.
> planned functionality. Please use with caution - Classes and methods may
> change until a stable release is made. I'll try to keep this to an absolute
> minimum and the releases and documentation will be very clear about any
> breaking changes.
>
> Also, structures like `list`, `dict`, `set` etc are not supported **at this
> time** as field types, since SQLite does not have a native column type for
Expand All @@ -45,7 +48,7 @@ Website](https://sqliter.grantramsay.dev)

- Table creation based on Pydantic models
- CRUD operations (Create, Read, Update, Delete)
- Basic query building with filtering, ordering, and pagination
- Chained Query building with filtering, ordering, and pagination
- Transaction support
- Custom exceptions for better error handling
- Full type hinting and type checking
Expand Down Expand Up @@ -114,19 +117,19 @@ db.create_table(User)

# Insert a record
user = User(name="John Doe", age=30)
new_record = db.insert(user)
new_user = db.insert(user)

# Query records
results = db.select(User).filter(name="John Doe").fetch_all()
for user in results:
print(f"User: {user.name}, Age: {user.age}")

# Update a record
user.age = 31
db.update(User, new_record)
new_user.age = 31
db.update(new_user)

# Delete a record
db.delete(User, new_record.pk)
db.delete(User, new_user.pk)
```

See the [Usage](https://sqliter.grantramsay.dev/usage) section of the documentation
Expand Down
13 changes: 9 additions & 4 deletions demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ def main() -> None:
level=logging.DEBUG, format="%(levelname)-8s%(message)s"
)

db = SqliterDB(memory=True, auto_commit=True, debug=True)
db = SqliterDB(
"demo.db", memory=False, auto_commit=True, debug=True, reset=True
)
with db:
db.create_table(UserModel) # Create the users table
user1 = UserModel(
Expand All @@ -62,7 +64,7 @@ def main() -> None:
)
try:
db.insert(user1)
db.insert(user2)
user2_instance = db.insert(user2)
db.insert(user3)
except RecordInsertionError as exc:
logging.error(exc) # noqa: TRY400
Expand All @@ -79,8 +81,11 @@ def main() -> None:
)
logging.info(all_reversed)

fetched_user = db.get(UserModel, "jdoe2")
logging.info(fetched_user)
if user2_instance is None:
logging.error("User2 ID not found.")
else:
fetched_user = db.get(UserModel, user2_instance.pk)
logging.info("Fetched (%s)", fetched_user)

count = db.select(UserModel).count()
logging.info("Total Users: %s", count)
Expand Down
20 changes: 18 additions & 2 deletions docs/guide/data-ops.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ into the correct table:

```python
user = User(name="Jane Doe", age=25, email="jane@example.com")
db.insert(user)
result = db.insert(user)
```

The `result` variable will contain a new instance of the model, with the primary
key value set to the newly-created primary key in the database. You should use
this instance to access the primary key value and other fields:

```python
print(f"New record inserted with primary key: {result.pk}")
print(f"Name: {result.name}, Age: {result.age}, Email: {result.email}")
```

> [!IMPORTANT]
Expand Down Expand Up @@ -46,15 +55,22 @@ See [Filtering Results](filtering.md) for more advanced filtering options.

## Updating Records

You can update records in the database by modifying the fields of the model
instance and then calling the `update()` method. You just pass the model
instance to the method:

```python
user.age = 26
db.update(user)
```

## Deleting Records

To delete a record from the database, you need to pass the model class and the
primary key value of the record you want to delete:

```python
db.delete(User, "Jane Doe")
db.delete(User, user.pk)
```

## Commit your changes
Expand Down
11 changes: 11 additions & 0 deletions docs/guide/filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ records, and can be combined with other methods like `order()`, `limit()`, and
result = db.select(User).filter(age__lte=30).limit(10).fetch_all()
```

It is possible to both add multiple filters in the same call, and to chain
multiple filter calls together:

```python
result = db.select(User).filter(age__gte=20, age__lte=30).fetch_all()
```

```python
result = db.select(User).filter(age__gte=20).filter(age__lte=30).fetch_all()
```

## Basic Filters

- `__eq`: Equal to (default if no operator is specified)
Expand Down
23 changes: 19 additions & 4 deletions docs/guide/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ Inserting records is straightforward with SQLiter:

```python
user = User(name="John Doe", age=30, email="john@example.com")
db.insert(user)
new_record = db.insert(user)
```

If successful, `new_record` will contain a model the same as was passed to it,
but including the newly-created primary key value.

## Basic Queries

You can easily query all records from a table:
Expand Down Expand Up @@ -109,7 +112,8 @@ returned.

## Updating Records

Records can be updated seamlessly:
Records can be updated seamlessly. Simply modify the fields of the model
instance and pass that to the `update()` method:

```python
user.age = 31
Expand All @@ -118,12 +122,23 @@ db.update(user)

## Deleting Records

Deleting records is simple as well:
Deleting records is simple as well. You just need to pass the Model that defines
your table and the primary key value of the record you want to delete:

```python
db.delete(User, "John Doe")
db.delete(User, 1)
```

> [!NOTE]
>
> You can get the primary key value from the record or model instance itself,
> e.g., `new_record.pk` and pass that as the second argument to the `delete()`
> method:
>
> ```python
> db.delete(User, new_record.pk)
> ```

## Advanced Query Features

### Ordering
Expand Down
79 changes: 61 additions & 18 deletions docs/guide/models.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# Defining Models
# Models

Models in SQLiter use Pydantic to encapsulate the logic. All models should
inherit from SQLiter's `BaseDBModel`. You can define your
models like this:
Each individual table in your database should be represented by a model. This
model should inherit from `BaseDBModel` and define the fields that should be
stored in the table. Under the hood, the model is a Pydantic model, so you can
use all the features of Pydantic models, such as default values, type hints, and
validation.

## Defining Models

Models are defined like this:

```python
from sqliter.model import BaseDBModel
Expand All @@ -11,23 +17,36 @@ class User(BaseDBModel):
name: str
age: int
email: str

class Meta:
table_name = "users"
primary_key = "name" # Default is "id"
create_pk = False # disable auto-creating an incrementing primary key - default is True
```

For a standard database with an auto-incrementing integer `id` primary key, you
do not need to specify the `primary_key` or `create_pk` fields. If you want to
specify a different primary key field name, you can do so using the
`primary_key` field in the `Meta` class.
You can create as many Models as you need, each representing a different table
in your database. The fields in the model will be used to create the columns in
the table.

> [!IMPORTANT]
>
> - Type-hints are **REQUIRED** for each field in the model.
> - The Model **automatically** creates an **auto-incrementing integer primary
> key** for each table called `pk`, you do not need to define it yourself.

### Custom Table Name

By default, the table name will be the same as the model name, converted to
'snake_case' and pluralized (e.g., `User` -> `users`). Also, any 'Model' suffix
will be removed (e.g., `UserModel` -> `users`). To override this behavior, you
can specify the `table_name` in the `Meta` class manually as below:

```python
from sqliter.model import BaseDBModel

class User(BaseDBModel):
name: str
age: int
email: str

If `table_name` is not specified, the table name will be the same as the model
name, converted to 'snake_case' and pluralized (e.g., `User` -> `users`). Also,
any 'Model' suffix will be removed (e.g., `UserModel` -> `users`). To override
this behavior, you can specify the `table_name` in the `Meta` class manually as
above.
class Meta:
table_name = "people"
```

> [!NOTE]
>
Expand All @@ -36,3 +55,27 @@ above.
> you need more advanced pluralization, you can install the `extras` package as
> mentioned in the [installation](../installation.md#optional-dependencies). Of
> course, you can always specify the `table_name` manually in this case!

## Model Classmethods

There are 2 useful methods you can call on your models. Note that they are
**Class Methods** so should be called on the Model class itself, not an
instance of the model:

### `get_table_name()`

This method returns the actual table name for the model either specified or
automatically generated. This is useful if you need to do any raw SQL queries.

```python
table_name = User.get_table_name()
```

### `get_primary_key()`

This simply returns the name of the primary key for that table. At the moment,
this will always return the string `pk` but this may change in the future.

```python
primary_key = User.get_primary_key()
```
7 changes: 5 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ database-like format without needing to learn SQL or use a full ORM.

> [!CAUTION]
> This project is still in the early stages of development and is lacking some
> planned functionality. Please use with caution.
> planned functionality. Please use with caution - Classes and methods may
> change until a stable release is made. I'll try to keep this to an absolute
> minimum and the releases and documentation will be very clear about any
> breaking changes.
>
> Also, structures like `list`, `dict`, `set` etc are not supported **at this
> time** as field types, since SQLite does not have a native column type for
Expand All @@ -36,7 +39,7 @@ database-like format without needing to learn SQL or use a full ORM.

- Table creation based on Pydantic models
- CRUD operations (Create, Read, Update, Delete)
- Basic query building with filtering, ordering, and pagination
- Chained Query building with filtering, ordering, and pagination
- Transaction support
- Custom exceptions for better error handling
- Full type hinting and type checking
Expand Down
12 changes: 8 additions & 4 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,23 @@ db.create_table(User)

# Insert a record
user = User(name="John Doe", age=30)
db.insert(user)
new_user = db.insert(user)

# Query records
results = db.select(User).filter(name="John Doe").fetch_all()
for user in results:
print(f"User: {user.name}, Age: {user.age}, Admin: {user.admin}")

# Update a record
user.age = 31
db.update(user)
new_user.age = 31
db.update(new_user)

results = db.select(User).filter(name="John Doe").fetch_one()

print("Updated age:", results.age)

# Delete a record
db.delete(User, "John Doe")
db.delete(User, new_user.pk)
```

See the [Guide](guide/guide.md) for more detailed information on how to use `SQLiter`.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ theme:
- navigation.tabs
- navigation.sections
- navigation.indexes
- content.code.copy

extra:
social:
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,10 @@ known-first-party = ["sqliter"]
keep-runtime-typing = true

[tool.mypy]
plugins = ["pydantic.mypy"]

python_version = "3.9"
exclude = ["docs"]

[[tool.mypy.overrides]]
disable_error_code = ["method-assign", "no-untyped-def", "attr-defined"]
module = "tests.*"
Expand Down
2 changes: 1 addition & 1 deletion sqliter/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class RecordUpdateError(SqliterError):
class RecordNotFoundError(SqliterError):
"""Exception raised when a requested record is not found in the database."""

message_template = "Failed to find a record for key '{}' "
message_template = "Failed to find that record in the table (key '{}') "


class RecordFetchError(SqliterError):
Expand Down
Loading