guardly 0.2.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.
@@ -0,0 +1,24 @@
1
+ *.pyc
2
+ __pycache__/
3
+ .venv/
4
+ *.py[cod]
5
+ .env
6
+ node_modules/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ .DS_Store
11
+ whenly/
12
+ unbreak/
13
+ structmatch/
14
+ configly/
15
+ typely/
16
+ batchly/
17
+ trending-bloom/
18
+ openclaw_workspace/
19
+
20
+ retryly/
21
+ workspace-researcher/
22
+ git/
23
+ .openclaw/
24
+ trendsketch/
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to Guardly will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-03-24
9
+
10
+ ### Added
11
+ - Initial release of Guardly — schema-first validation for Python dicts
12
+ - Bare type validators: `str`, `int`, `float`, `bool`
13
+ - Constrained type validators: `Int()`, `Str()`, `Float()`, `Bool()`
14
+ - Specialized validators: `Email()`, `OneOf()`
15
+ - Collection validators: `List()`, `Dict()`
16
+ - `Optional()` wrapper for optional fields with default support
17
+ - Custom validator support (any callable as a schema node)
18
+ - Nested object validation (schemas within schemas)
19
+ - List shorthand syntax: `[str]`, `[int]`
20
+ - Type coercion: `str→int`, `str→float`, `str→bool`
21
+ - `validate()` — returns list of errors
22
+ - `check()` — raises `ValidationError`
23
+ - `ValidationIssue` with path and message
24
+ - `ValidationError` exception with all errors
25
+ - Zero external dependencies, pure Python 3.10+
26
+ - 106 tests with full coverage
@@ -0,0 +1,66 @@
1
+ # Contributing to Guardly
2
+
3
+ Thank you for your interest in contributing! Guardly is a lightweight library and contributions of all sizes are welcome.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ # Clone the repository
9
+ git clone https://github.com/yourusername/guardly.git
10
+ cd guardly
11
+
12
+ # Create a virtual environment (recommended)
13
+ python -m venv .venv
14
+ source .venv/bin/activate # or .venv\Scripts\activate on Windows
15
+
16
+ # Install in editable mode with dev dependencies
17
+ pip install -e ".[dev]"
18
+ ```
19
+
20
+ ## Running Tests
21
+
22
+ ```bash
23
+ # Run all tests
24
+ python -m pytest tests/ -v
25
+
26
+ # Run with coverage
27
+ python -m pytest tests/ -v --cov=guardly
28
+
29
+ # Run a specific test class
30
+ python -m pytest tests/test_guard.py::TestBasicStr -v
31
+ ```
32
+
33
+ ## Development Philosophy
34
+
35
+ Guardly has a few core principles that guide contributions:
36
+
37
+ 1. **Zero dependencies.** PRs that add external dependencies will not be accepted. If functionality requires a dependency, it belongs in an optional extra (`guardly[email]`, etc.).
38
+
39
+ 2. **Schema-as-data.** The schema is a dict, not a class. API design should reinforce this.
40
+
41
+ 3. **Clear error messages.** Every error should tell the user what went wrong, where it went wrong, and what was expected.
42
+
43
+ 4. **Coercion is opt-in.** Bare types (`str`, `int`) are strict. Use the validator constructors (`Str()`, `Int()`) for coercion behavior.
44
+
45
+ ## Pull Request Process
46
+
47
+ 1. Fork the repository
48
+ 2. Create a feature branch (`git checkout -b feature/my-feature`)
49
+ 3. Write tests for your changes
50
+ 4. Ensure all tests pass (`python -m pytest tests/ -v`)
51
+ 5. Commit with clear messages
52
+ 6. Open a pull request
53
+
54
+ ## Reporting Bugs
55
+
56
+ Please open a GitHub issue with:
57
+ - A minimal reproducible example
58
+ - The expected vs actual behavior
59
+ - Python version and OS
60
+
61
+ ## Code Style
62
+
63
+ - Follow PEP 8
64
+ - Type hints where helpful
65
+ - Docstrings on public functions/classes
66
+ - Keep it simple — this is a lightweight library
guardly-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,392 @@
1
+ Metadata-Version: 2.4
2
+ Name: guardly
3
+ Version: 0.2.0
4
+ Summary: Schema-first validation for dicts and configs. Zero dependencies.
5
+ Author: Ravi Teja Prabhala Venkata
6
+ License-Expression: MIT
7
+ Keywords: config,dict,schema,validation
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=7.0; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Guardly
16
+
17
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)
18
+ ![MIT License](https://img.shields.io/badge/license-MIT-green.svg)
19
+ ![Tests](https://img.shields.io/badge/tests-106%20passing-brightgreen.svg)
20
+ ![Zero Dependencies](https://img.shields.io/badge/dependencies-zero-orange.svg)
21
+
22
+ **Schema-first validation for Python dicts and configs. Zero dependencies.**
23
+
24
+ Guardly validates plain dictionaries against schemas you define as data — no models, no decorators, no ceremony. Define what you expect, get back clear errors with path info.
25
+
26
+ ```python
27
+ import guardly
28
+
29
+ schema = {
30
+ "name": str,
31
+ "age": guardly.Int(min=0, max=150),
32
+ "email": guardly.Email(),
33
+ "tags": [str],
34
+ "active": bool,
35
+ "score": float,
36
+ "address": {
37
+ "city": str,
38
+ "zip": guardly.Str(pattern=r"\d{5}"),
39
+ },
40
+ }
41
+
42
+ errors = guardly.validate(data, schema)
43
+ if errors:
44
+ for e in errors:
45
+ print(f"{'.'.join(e.path)}: {e.message}")
46
+
47
+ # Or raise on failure:
48
+ guardly.check(data, schema) # raises guardly.ValidationError
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Why Guardly?
54
+
55
+ Python's validation ecosystem has a gap. Here's the landscape:
56
+
57
+ | Library | Approach | Dependencies | Status |
58
+ |---------|----------|-------------|--------|
59
+ | **Pydantic** | Model-first (class-based) | pydantic-core, typing-extensions | Active, heavy |
60
+ | **Marshmallow** | Schema-based (class declarations) | marshmallow, packaging | Active, verbose |
61
+ | **Cerberus** | Schema-as-dict | None | Last release 2021, unmaintained |
62
+ | **Voluptuous** | Schema-as-dict | None | Last release 2020, unmaintained |
63
+ | **jsonschema** | JSON Schema spec | jsonschema, attrs, rpds-py | Active, spec-heavy |
64
+ | **Guardly** | Schema-as-dict | **None** | ✅ Active, lightweight |
65
+
66
+ **The problem:** If you're validating raw dicts — API payloads, config files, environment variables — you don't need models. Pydantic forces a class-based design that's overkill for simple validation. Cerberus and Voluptuous filled this niche but are now unmaintained. Marshmallow requires declaring classes.
67
+
68
+ **Guardly fills this gap:** Define schemas as plain Python dicts. Validate any dict against them. Get clear, path-annotated errors. Zero dependencies. 100% pure Python.
69
+
70
+ ---
71
+
72
+ ## Installation
73
+
74
+ ```bash
75
+ pip install guardly
76
+ ```
77
+
78
+ No other dependencies. Python 3.10+.
79
+
80
+ ---
81
+
82
+ ## Quick Start
83
+
84
+ ### Basic Types
85
+
86
+ ```python
87
+ import guardly
88
+
89
+ schema = {
90
+ "name": str,
91
+ "age": int,
92
+ "score": float,
93
+ "active": bool,
94
+ }
95
+
96
+ errors = guardly.validate({"name": "Alice", "age": 30, "score": 95.5, "active": True}, schema)
97
+ # errors == []
98
+ ```
99
+
100
+ ### Constrained Types
101
+
102
+ ```python
103
+ schema = {
104
+ "age": guardly.Int(min=0, max=150),
105
+ "email": guardly.Str(pattern=r"[^@]+@[^@]+\.[^@]+"),
106
+ "bio": guardly.Str(min_len=10, max_len=500),
107
+ "rating": guardly.Float(min=0.0, max=5.0),
108
+ "role": guardly.OneOf(["admin", "editor", "viewer"]),
109
+ "contact": guardly.Email(),
110
+ }
111
+ ```
112
+
113
+ ### Nested Schemas
114
+
115
+ ```python
116
+ schema = {
117
+ "user": {
118
+ "name": str,
119
+ "address": {
120
+ "street": str,
121
+ "city": str,
122
+ "zip": guardly.Str(pattern=r"\d{5}"),
123
+ },
124
+ },
125
+ }
126
+ ```
127
+
128
+ ### Lists
129
+
130
+ ```python
131
+ # Shorthand: all elements must be strings
132
+ schema = {"tags": [str]}
133
+
134
+ # Full control with List()
135
+ schema = {"scores": guardly.List(guardly.Float(min=0, max=100), min_len=1, max_len=10)}
136
+ ```
137
+
138
+ ### Dict (untyped keys, typed values)
139
+
140
+ ```python
141
+ schema = {"metadata": guardly.Dict(int)}
142
+ # validates {"metadata": {"views": 100, "likes": 42}}
143
+ ```
144
+
145
+ ### Optional Fields
146
+
147
+ ```python
148
+ schema = {
149
+ "name": str,
150
+ "nickname": guardly.Optional(str), # can be missing or None
151
+ "role": guardly.Optional(guardly.Str(), default="viewer"),
152
+ }
153
+ ```
154
+
155
+ ### Custom Validators
156
+
157
+ ```python
158
+ schema = {
159
+ "password": lambda x: len(x) >= 8 or "password must be at least 8 characters",
160
+ "age": lambda x: x >= 0 or "age must be non-negative",
161
+ }
162
+ ```
163
+
164
+ ### Error Handling
165
+
166
+ ```python
167
+ # Collect all errors:
168
+ errors = guardly.validate(data, schema)
169
+ for e in errors:
170
+ print(f"[{'.'.join(e.path) if e.path else 'root'}] {e.message}")
171
+
172
+ # Or raise immediately:
173
+ try:
174
+ guardly.check(data, schema)
175
+ except guardly.ValidationError as e:
176
+ for e in e.errors:
177
+ print(e)
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Type System
183
+
184
+ ### Primitive Types
185
+
186
+ | Schema | Validates | Coercions |
187
+ |--------|-----------|-----------|
188
+ | `str` | strings only | — |
189
+ | `int` | integers | `"42"` → `42` |
190
+ | `float` | floats | `42` → `42.0`, `"3.14"` → `3.14` |
191
+ | `bool` | booleans | `"true"`/`"false"`/`"yes"`/`"no"`/`0`/`1` |
192
+
193
+ ### Constrained Types
194
+
195
+ | Type | Parameters | Example |
196
+ |------|-----------|---------|
197
+ | `Int(min, max)` | min/max bounds | `Int(min=0, max=150)` |
198
+ | `Str(pattern, min_len, max_len)` | regex, length bounds | `Str(pattern=r"\d{5}")` |
199
+ | `Float(min, max)` | min/max bounds | `Float(min=0.0, max=1.0)` |
200
+ | `Email()` | built-in regex | `Email()` |
201
+ | `OneOf(choices)` | allowed values | `OneOf(["a", "b", "c"])` |
202
+ | `List(element_type, min_len, max_len)` | element type, size bounds | `List(int, min_len=1)` |
203
+ | `Dict(value_type)` | value type (keys unrestricted) | `Dict(int)` |
204
+ | `Optional(type, default)` | wraps any type as optional | `Optional(str, default="N/A")` |
205
+
206
+ ### Custom Validators
207
+
208
+ Any callable works as a schema node:
209
+
210
+ ```python
211
+ # Return truthy for pass, falsy for fail
212
+ schema = {"x": lambda x: x > 0}
213
+
214
+ # Return error message string on failure
215
+ schema = {"x": lambda x: x > 0 or "must be positive"}
216
+
217
+ # Raise an exception
218
+ schema = {"x": lambda x: assert x > 0}
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Design Philosophy
224
+
225
+ 1. **Schema-as-data.** Your schema is a Python dict, not a class. It's serializable, composable, and trivially dynamic. You can load it from JSON, generate it programmatically, or compose it from pieces.
226
+
227
+ 2. **Zero dependencies.** Guardly is ~250 lines of pure Python. No pydantic-core, no attrs, no typing extensions. Install it, use it, ship it — nothing else to track.
228
+
229
+ 3. **Clear errors with paths.** Every validation error tells you exactly where it is (`address.zip`), what went wrong, and what was expected. No hunting through nested exceptions.
230
+
231
+ 4. **Coercion where sensible, strict where it matters.** `"42"` coerces to `42` for `Int()` because that's what most APIs need. But `True` never coerces to `1` — that's a bug waiting to happen.
232
+
233
+ 5. **Extra fields ignored by default.** Your schema declares what you need. Additional keys in the data are silently ignored. This makes forward-compatible APIs natural.
234
+
235
+ ---
236
+
237
+ ## Use Cases
238
+
239
+ ### API Input Validation
240
+
241
+ ```python
242
+ def create_user(request_json):
243
+ schema = {
244
+ "username": Str(min_len=3, max_len=32, pattern=r"[a-zA-Z0-9_]+"),
245
+ "email": Email(),
246
+ "age": Optional(Int(min=13, max=120)),
247
+ "role": Optional(OneOf(["user", "admin"]), default="user"),
248
+ }
249
+ errors = guardly.validate(request_json, schema)
250
+ if errors:
251
+ return {"errors": [{"field": ".".join(e.path), "msg": e.message} for e in errors]}, 400
252
+ # proceed with validated data...
253
+ ```
254
+
255
+ ### Config File Validation
256
+
257
+ ```python
258
+ import json
259
+
260
+ CONFIG_SCHEMA = {
261
+ "database": {
262
+ "host": str,
263
+ "port": Int(min=1, max=65535),
264
+ "name": str,
265
+ "pool_size": Optional(Int(min=1, max=100), default=10),
266
+ },
267
+ "server": {
268
+ "host": str,
269
+ "port": Int(min=1, max=65535),
270
+ "debug": bool,
271
+ },
272
+ "logging": {
273
+ "level": OneOf(["DEBUG", "INFO", "WARNING", "ERROR"]),
274
+ "file": Optional(str),
275
+ },
276
+ }
277
+
278
+ with open("config.json") as f:
279
+ config = json.load(f)
280
+ guardly.check(config, CONFIG_SCHEMA)
281
+ ```
282
+
283
+ ### Environment Variable Validation
284
+
285
+ ```python
286
+ import os
287
+
288
+ env_schema = {
289
+ "DATABASE_URL": Str(pattern=r"postgres://.+"),
290
+ "PORT": Int(min=1, max=65535),
291
+ "DEBUG": bool,
292
+ "SECRET_KEY": Str(min_len=32),
293
+ }
294
+
295
+ env_data = {k: os.environ.get(k) for k in env_schema}
296
+ errors = guardly.validate(env_data, env_schema)
297
+ if errors:
298
+ raise RuntimeError(f"Invalid environment: {errors}")
299
+ ```
300
+
301
+ ### Form Data Validation
302
+
303
+ ```python
304
+ form_schema = {
305
+ "email": Email(),
306
+ "password": Str(min_len=8),
307
+ "confirm_password": str,
308
+ "terms_accepted": bool,
309
+ }
310
+
311
+ # Custom cross-field validation
312
+ errors = guardly.validate(form_data, form_schema)
313
+ if form_data.get("password") != form_data.get("confirm_password"):
314
+ errors.append(guardly.errors.ValidationIssue(
315
+ ("confirm_password",), "passwords do not match"
316
+ ))
317
+ ```
318
+
319
+ ---
320
+
321
+ ## API Reference
322
+
323
+ ### `guardly.validate(data, schema) -> list[ValidationIssue]`
324
+
325
+ Validates `data` (a dict) against `schema`. Returns a list of errors. Empty list means valid.
326
+
327
+ ### `guardly.check(data, schema)`
328
+
329
+ Validates and raises `ValidationError` if any errors are found.
330
+
331
+ ### `guardly.ValidationError`
332
+
333
+ Exception raised by `check()`. Contains `.errors` — a list of `ValidationIssue` objects.
334
+
335
+ ### `guardly.errors.ValidationIssue`
336
+
337
+ - `.path` — tuple of path segments, e.g., `("address", "zip")`
338
+ - `.message` — human-readable error description
339
+
340
+ ### Types
341
+
342
+ - `guardly.Int(min=None, max=None)`
343
+ - `guardly.Str(pattern=None, min_len=None, max_len=None)`
344
+ - `guardly.Float(min=None, max=None)`
345
+ - `guardly.Bool()`
346
+ - `guardly.Email()`
347
+ - `guardly.OneOf(choices)`
348
+ - `guardly.List(element_type, min_len=None, max_len=None)`
349
+ - `guardly.Dict(value_type)`
350
+ - `guardly.Optional(type, default=None)`
351
+
352
+ ---
353
+
354
+ ## Comparison: Guardly vs Alternatives
355
+
356
+ | Feature | Guardly | Pydantic | Cerberus | Voluptuous | Marshmallow | jsonschema |
357
+ |---------|---------|----------|----------|------------|-------------|------------|
358
+ | Schema-as-dict | ✅ | ❌ (class-based) | ✅ | ✅ | ❌ (class-based) | ✅ (JSON Schema) |
359
+ | Zero dependencies | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
360
+ | Type coercion | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
361
+ | Nested validation | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
362
+ | Custom validators | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
363
+ | Error path info | ✅ | ✅ | ✅ | Partial | ✅ | ✅ |
364
+ | Optional fields | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
365
+ | Maintained (2024+) | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
366
+ | Lines of code | ~250 | ~20,000 | ~5,000 | ~2,500 | ~10,000 | ~8,000 |
367
+ | Install size | ~10 KB | ~5 MB | ~200 KB | ~150 KB | ~500 KB | ~1 MB |
368
+
369
+ ---
370
+
371
+ ## Development
372
+
373
+ ```bash
374
+ # Clone and set up
375
+ git clone https://github.com/yourusername/guardly.git
376
+ cd guardly
377
+ pip install -e ".[dev]"
378
+
379
+ # Run tests
380
+ python -m pytest tests/ -v
381
+
382
+ # Run with coverage
383
+ python -m pytest tests/ -v --cov=guardly
384
+ ```
385
+
386
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
387
+
388
+ ---
389
+
390
+ ## License
391
+
392
+ MIT © Ravi Teja Prabhala Venkata