Skip to content

Commit

Permalink
Merge pull request #8 from Naapperas/refactor/refactor-api
Browse files Browse the repository at this point in the history
Refactor/refactor api
  • Loading branch information
Naapperas authored Jun 20, 2024
2 parents 830805b + 00577d8 commit 5ae1392
Show file tree
Hide file tree
Showing 42 changed files with 3,241 additions and 1,085 deletions.
20 changes: 15 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,23 @@ on: [push]
jobs:
lint:
runs-on: ubuntu-latest
strategy:
max-parallel: 5
matrix:
version: [3.11, 3.12]

steps:
- uses: actions/checkout@v3

- name: Set up Python 3.11
- name: Set up Python ${{ matrix.version}}
uses: actions/setup-python@v3
with:
python-version: '3.11'
python-version: "${{ matrix.version }}"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
- name: Install Pylint
run: pip install pylint
Expand All @@ -28,14 +33,16 @@ jobs:
runs-on: ubuntu-latest
strategy:
max-parallel: 5
matrix:
version: [3.11, 3.12]

steps:
- uses: actions/checkout@v3

- name: Set up Python 3.11
- name: Set up Python ${{ matrix.version}}
uses: actions/setup-python@v3
with:
python-version: '3.11'
python-version: "${{ matrix.version }}"

- name: Install dependencies
run: |
Expand All @@ -49,4 +56,7 @@ jobs:
run: python -m pip install -e .

- name: Run tests
run: pytest
run: pytest tests

- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ htmlcov/
.nox/
.coverage
.coverage.*
coverage.*
.cache
nosetests.xml
coverage.xml
Expand Down
21 changes: 19 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

> This changelog was generated some commits after the [v1.0.0 tag](https://github.com/Naapperas/zon/releases/tag/v1.0.0), so the changelog will have some inconsistencies until the next release.
## [2.0.0] - 2024-06-20

## [Unreleased]
### Added
- Added `ValidationContext` class to keep track of current validation path and errors up until a certain point.
- Added `examples` folder
- Added explanation regarding `ZonString.datetime()` decisions.
- Added `ZonLiteral`, `ZonTuple` and `ZonEnum` classes
- Added more `ZonRecord` methods
- Added coverage

### Changed
- Moved everything into a single file to combat circular reference issues
- Deprecated `ValidationError` in favor of `ZonError`.
- Simplified validation logic
- Now returns a (deep-) copy of the original data after validation. This is more useful for `ZonRecord` and `ZonString` validators that can transform, while transformers are not added.

### Removed
- Removed `between`, `__eq__` and `equals` methods from `ZonNumber`.
- Removed `ZonInteger` and `ZonFloat` in favor of new validation rules in `ZonNumber`
- Removed `true` and `false` methods from `ZonBoolean`

## [1.1.0] - 2024-04-10

Expand Down
200 changes: 170 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# zon - Zod-like validator library for Python

[![Coverage Status](https://coveralls.io/repos/github/Naapperas/zon/badge.svg?branch=refactor/refactor-api)](https://coveralls.io/github/Naapperas/zon?branch=refactor/refactor-api)

Want to have the validation power of [zod](https://zod.dev/) but with the ease-of-use of Python?

Enter `zon`.

`zon` is a Python library that aims to provide a simple, easy-to-use API for validating data, similiar to `zod`'s own API'. In fact, the whole library and its name were inspired by `zod`: **Z**od + Pyth**on** = **Zon** !!!.
`zon` is a Python library that aims to provide a simple, easy-to-use API for validating data, similar to `zod`'s own API'. In fact, the whole library and its name were inspired by `zod`: **Z**od + Pyth**on** = **Zon** !!!.

## Why

Expand Down Expand Up @@ -36,55 +38,107 @@ pip install .

In its essence, `zon` behaves much like `zod`. If you have used `zod`, you will feel right at home with `zon`.

> [!NOTE]
> There are some differences in the public API between `zon` and `zod`. Those mostly stem from the fact that Python does not have type inference like Typescript has. There are other slight deviations between `zon` and `zod`.
### Basic types

`zon` features most of `zod`'s basic types:

```python
zon.string()
zon.integer()
zon.floating_point()
zon.number()
zon.boolean()
zon.none()
zon.anything()
```
zon.literal()
zon.enum()

Besides this, theres also a `zon.optional()` type, which allows for a value to be either of the type passed as an argument or `None`.
zon.record()
zon.element_list()
zon.element_tuple()

```python
zon.optional(zon.string())
```
zon.union()
zon.intersection()
zon.optional()

### Integers and Floats
zon.never()
zon.anything()
```

`zon`'z integer and floating point types derive from a common `ZonNumber` class that defines some methods that can be applied to all numbers:
### Numbers

```python
validator = zon.integer() # (for floats, use zon.floating_point())
validator = zon.number()

validator.gt(5)
validator.gte(5)
validator.gte(5) # alias for .min(5)
validator.lt(5)
validator.lte(5)
validator.lte(5) # alias for .max(5)

validator.int()
validator.float()

validator.positive()
validator.negative()
validator.non_negative()
validator.non_positive()

validator.multiple_of(5) # alias for .step(5)

validator.finite()
```

### Strings

For strings, there are also some extra methods:

```python
zon.string().min(5)
zon.string().max(10)
zon.string().length(5)
zon.string().email()
zon.string().regex(r"^\d{3}-\d{3}-\d{4}$")
zon.string().uuid()
zon.string().ip()
validator = zon.string()

validator.min(5)
validator.max(10)
validator.length(5)

validator.email()
validator.url()
validator.uuid()
validator.regex(r"^\d{3}-\d{3}-\d{4}$")
validator.includes("needle")
validator.starts_with("prefix")
validator.ends_with("suffix")
validator.datetime()
validator.ip()

# transformers
validator.trim()
validator.to_lower_case()
validator.to_upper_case()
```

#### Datetime

`zon` accepts the same options as `zod` for datetime string validation:
```python
validator.datetime({"precision": 3})
validator.datetime({"local": True})
validator.datetime({"offset": True})
```

> [!NOTE]
> `zod` uses regex-based validation for datetimes, which must be valid [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) strings. However, due to an issue with most JavaScript engines' datetime validation, offsets cannot specify only hours, and `zod` reflects this in their API.
> While `zon` could reflect `zod`'s API in this matter, it is best to not constrain users to the problems of another platform, making this one of the aspects where `zon` deviates from `zod`.
#### IP addresses

`zon` accepts the same options as `zod` for ip address string validation:
```python
validator.ip({"version": "v4"})
validator.ip({"version": "v6"})
```


### List

Lists are defined by calling the `zon.list()` method, passing as an argument a `Zon` instance. All elements in this list must be of the same type.
Lists are defined by calling the `zon.element_list()` method, passing as an argument a `Zon` instance. All elements in this list must be of the same type.

```python
zon.element_list(zon.string())
Expand All @@ -100,24 +154,36 @@ validator.max(10)
validator.length(5)
```

There is also a method for validating that the list has at least one element, `nonempty`:
```py
validator.nonempty()
```

You can get the type of the list's elements by accessing the `element` property:
```py
zon.element_list(zon.string()).element is ZonString
```

### Union

`zon` supports unions of types, which are defined by calling the `zon.union()` method, passing as arguments the `Zon` instances that are part of the union.

```python
zod.union([zon.string(), zon.integer()])
zod.union([zon.string(), zon.number()])
```

To access the various options, access the `options` property:

```python
zod.union([zon.string(), zon.number()]).options = [ZonString, ZonNumber]
```

### Record

`zon` supports validating objects according to a specified schema, using the `zon.schema()` method. This method takes as an argument a dictionary, where the keys are the keys of the object to be validated and the values are the `Zon` instances that define the type of each key.

This method is probably the most useful in the library since it can be used to, for example, validate JSON data from a HTTP response, like such:

```python
import zon

schema = zon.record({
validator = zon.record({
"name": zon.string(),
"age": zon.number(),
"isAwesome": zon.boolean(),
Expand All @@ -130,6 +196,78 @@ schema = zon.record({
})
```

Useful methods for `ZonRecord` instances:
```py
validator.extend({...})
validator.merge(otherZon) == validator.extend(otherZon.shape)
validator.pick({"name": True}) == ZonRecord({"name": ...})
validator.omit({"name": True}) == ZonRecord({"<key that is different from name>": ..., ...})
validator.partial() # makes all attributes optional, shallowly
validator.partial({"name": True}) # makes only "name" optional (in this example)
validator.deepPartial() # makes all attributes optional, recursively
validator.required() # makes all attributes required, shallowly
```

You can access the shape of objects validated by any `ZonRecord` instance by accessing the `shape` property:
```py
shape = validator.shape
```

If you want to validate only the keys of the shape, use the `keyof` method:
```py
validator.keyof() == ZonEnum(["name", "age", "isAwesome", "friends", "address"])
```

#### Unknown Keys

As `zod`, `zon` normally strips unknown keys from records. This, however, can be configured:
```py
validator.strict() # presence of unknown keys makes validation fail
validator.passthrough() # add unknown keys to the resulting record
validator.strip() # the default behavior, strip unknown keys from the resulting record
```

#### Catchall

In case you want to validate unknown keys, you can use the `catchall` method to specify the validator that is used:
```py
validator.catchall(zon.string()) # unknown keys *must* be associated with string values
```

### Tuple

`zon` supports tuples out-of-the-box, which are fixed-size containers whose elements might not have the same type:
```py
validator = zon.tuple([...])
```

If you want to access the items of the tuples, use the `items` property:
```py
validator.items = [...]
```

Variadic items can be validated using the `rest` method:
```py
validator.rest(zon.string()) # after the defined items, everything must be a string
```

### Enums

`zon` supports enumerations, which allow validating that any given data is one of many values:

```py
validator = zon.enum([...])
```

If you want to access the possible valid values, use the `enum` property:
```py
validator.enum = [...]
```

## Examples

Example usage of `zon` can be found in the [`examples`](./examples/) directory.

## Documentation

Documentation is still not available, but it will be soon.
Expand All @@ -141,9 +279,11 @@ Tests can be found in the [tests](tests) folder. `zon` uses `pytest` for unit te
To run the tests, simply run:

```bash
pytest /test
pytest test
```

Coverage can be found on [Coveralls](https://coveralls.io/github/Naapperas/zon).

## Contributing

Contribution guidelines can be found in [CONTRIBUTING](CONTRIBUTING.md)
Expand Down
4 changes: 4 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# zon - Zod-like validator library for Python - Examples

This directory contains samples of validation schemas that can be built using `zon`.
Each different type of validation is separated into its own file
11 changes: 11 additions & 0 deletions examples/simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import zon

validator = zon.string().min(2).max(5).length(3)

for i in range(7):
print(f"Checking for length={i}")

try:
validator.validate("a" * i)
except zon.error.ZonError as e:
print(f"Failed: {e}")
Loading

0 comments on commit 5ae1392

Please sign in to comment.