Metadata-Version: 2.4
Name: saython
Version: 0.1.2
Summary: A beginner-friendly Python web framework for CRUD apps with auth and ORM
Author: Sabbir
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: License :: OSI Approved :: MIT License
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Provides-Extra: jwt
Requires-Dist: PyJWT>=2.0; extra == "jwt"
Provides-Extra: bcrypt
Requires-Dist: bcrypt>=4.0; extra == "bcrypt"
Provides-Extra: postgres
Requires-Dist: psycopg2-binary; extra == "postgres"
Provides-Extra: mysql
Requires-Dist: mysql-connector-python; extra == "mysql"
Provides-Extra: all
Requires-Dist: PyJWT>=2.0; extra == "all"
Requires-Dist: bcrypt>=4.0; extra == "all"
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: provides-extra
Dynamic: requires-python
Dynamic: summary

# SAYTHON

> **A beginner-friendly Python web framework focused on CRUD apps — with built-in ORM and authentication.**  
> Zero hard dependencies. Pure Python standard library + optional extras.

---

## Install

```bash
pip install saython
# or from source:
git clone <repo>
pip install -e .
```

Optional extras for better performance:

```bash
pip install PyJWT bcrypt          # faster JWT + stronger password hashing
pip install psycopg2-binary       # PostgreSQL support
pip install gunicorn              # production WSGI server
```

---

## Quickstart

```python
from saython import Saython, Model, Field, auth_required, get_auth_router, Router, Response

app = Saython(__name__)

class Book(Model):
    title  = Field(str, required=True)
    author = Field(str, required=True)
    year   = Field(int, default=2024)

# Mount auth routes
app.include("/auth", get_auth_router())

# Write your own routes
book_router = Router()

@book_router.get("/")
def list_books(req):
    return Book.all_as_dicts()

@book_router.post("/")
@auth_required
def create_book(req):
    book = Book.create(**req.json)
    return Response(book.to_dict(), 201)

@book_router.get("/<id>")
def get_book(req):
    return Book.get(int(req.params["id"])).to_dict()

@book_router.delete("/<id>")
@auth_required
def delete_book(req):
    Book.get(int(req.params["id"])).delete()
    return {"message": "deleted"}

app.include("/books", book_router)

@app.get("/")
def index(req):
    return {"message": "Hello from SAYTHON!"}

app.run(debug=True)
```

| Method | Path              | Description          |
|--------|-------------------|----------------------|
| POST   | /auth/register    | Register user        |
| POST   | /auth/login       | Login → get token    |
| GET    | /auth/me          | Current user (token) |
| GET    | /books            | List all books       |
| POST   | /books            | Create (auth)        |
| GET    | /books/\<id\>     | Get one book         |
| DELETE | /books/\<id\>     | Delete (auth)        |
| GET    | /                 | Custom endpoint      |

---

## CLI

```bash
saython new my-blog      # scaffold a new project
cd my-blog
python main.py           # run it

saython run              # same, from CLI
saython routes           # print all registered routes
```

---

## ORM

```python
from saython import Model, Field

class Post(Model):
    title    = Field(str,   required=True)
    content  = Field(str,   required=True)
    views    = Field(int,   default=0)
    published = Field(bool, default=False)

# Every Model gets: id, created_at, updated_at automatically.
```

### CRUD operations

```python
# Create
post = Post.create(title="Hello", content="World")

# Read
post  = Post.get(1)                                 # by id (NotFound if missing)
posts = Post.all()                                  # list all
posts = Post.filter(published=True)                 # equality filter
page  = Post.paginate(page=1, limit=10)             # pagination dict
post  = Post.first(title="Hello")                   # first match or None
count = Post.count()
exists = Post.exists(title="Hello")

# Update
post.title = "Updated"
post.save()

# Delete
post.delete()

# Serialise
post.to_dict()                        # all fields as dict
post.to_dict(exclude=["content"])     # exclude fields
```

### Switch database

```python
# SQLite (default):
app.configure(DATABASE_URL="myapp.db")

# PostgreSQL:
app.configure(DATABASE_URL="postgresql://user:pass@localhost/dbname")

# MySQL:
app.configure(DATABASE_URL="mysql://user:pass@localhost/dbname")
```

---

## Authentication

### Register & Login

```http
POST /auth/register
{ "username": "alice", "email": "alice@example.com", "password": "secret123" }

POST /auth/login
{ "username": "alice", "password": "secret123" }
→ { "token": "eyJ...", "user": { "id": 1, "username": "alice", ... } }
```

### Protect endpoints

```python
from saython import auth_required, admin_required

@app.get("/dashboard")
@auth_required
def dashboard(req):
    return {"hello": req.user.username}

@app.delete("/admin/wipe")
@admin_required         # is_admin=True required
def wipe(req):
    return {"done": True}
```

Send the token in requests:

```
Authorization: Bearer eyJ...
```

### User model

```python
from saython.auth import User

user = User.register(username="bob", email="bob@x.com", password="pass123")
user = User.authenticate("bob", "pass123")
token = user.generate_token()
user.to_dict()   # password field automatically excluded
```

---

## Custom Routing

```python
@app.get("/items")
def list_items(req):
    page  = int(req.query.get("page", 1))
    limit = int(req.query.get("limit", 20))
    return Item.paginate(page=page, limit=limit)

@app.post("/items")
def create_item(req):
    data = req.json          # parsed JSON body
    item = Item.create(**data)
    return item.to_dict(), 201

@app.get("/items/<id>")
def get_item(req):
    return Item.get(int(req.params["id"])).to_dict()

@app.delete("/items/<id>")
@auth_required
def delete_item(req):
    Item.get(int(req.params["id"])).delete()
    return {"deleted": True}
```

---

## Sub-Routers

```python
from saython import Router

book_router = Router()

@book_router.get("/")
def list_books(req): ...

@book_router.post("/")
def create_book(req): ...

app.include("/books", book_router)
```

---

## Configuration

```python
app.configure(
    SECRET_KEY="very-secret",          # JWT signing key
    DATABASE_URL="myapp.db",           # DB connection
    DEBUG=True,                        # show tracebacks
    CORS=True,                         # enable CORS headers
    CORS_ORIGINS=["https://myapp.com"],
    TOKEN_EXPIRY_HOURS=48,             # JWT lifetime
    HOST="0.0.0.0",
    PORT=8080,
)
```

---

## Middleware & Hooks

```python
@app.before_request
def log_request(req):
    print(f"→ {req.method} {req.path}")

@app.after_request
def add_header(req, resp):
    resp.headers["X-Powered-By"] = "SAYTHON"

@app.error_handler(404)
def not_found(req, err):
    return {"error": "Page not found", "path": req.path}, 404
```

---

## Production

```bash
pip install gunicorn
gunicorn main:app --bind 0.0.0.0:8000 --workers 4
```

---

## Project Structure (generated by `saython new`)

```
my-project/
├── main.py          ← app + routes + model registration
├── models.py        ← extra model definitions
├── .env             ← secret key, DB URL
├── requirements.txt
└── README.md
```

---

## License

MIT
