Metadata-Version: 2.4
Name: onesite
Version: 0.1.16
Summary: A CLI tool to generate full-stack web projects from SQLModel
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: cookiecutter
Requires-Dist: fastapi
Requires-Dist: jinja2
Requires-Dist: pydantic
Requires-Dist: rich
Requires-Dist: sqlmodel
Requires-Dist: typer[all]
Requires-Dist: uvicorn
Description-Content-Type: text/markdown

# OneSite

[中文文档](README.zh-CN.md) · [Training guide](docs/onesite-training-guide.md)

OneSite is a model-driven CLI that generates an admin-style full-stack application from SQLModel definitions. `models/` is the source of truth; `site sync` generates FastAPI schemas, CRUD, services and REST endpoints, plus React/Vite pages, API clients, navigation, translations and theme assets.

## Requirements and installation

- Python 3.10+
- Node.js and npm for generated frontends
- Docker or Podman only when building containers

```bash
pip install onesite
site --help
```

For development in this repository:

```bash
python -m pip install -e .
```

## Quick start

```bash
site create inventory
cd inventory

# Add or edit SQLModel classes in models/.
site sync --install
site run
```

The frontend is available at `http://localhost:5173`; FastAPI documentation is at `http://localhost:8000/docs`. A new project seeds `admin@example.com` / `admin`; change this credential before deploying.

The normal edit/generate loop is:

```text
edit models/*.py or site_config.json → site sync → test /docs and the frontend
```

Do not treat `backend/app/models/` as the model source of truth: it is synced from `models/`. Schemas, endpoints, CRUD/services and generated frontend pages can be regenerated by `site sync`; keep durable business customizations in deliberate extension points, not solely in generated files.

## CLI

| Command | Purpose |
| --- | --- |
| `site init` | Initialize `site_config.json`, base models and icon reference in the current directory. |
| `site create <project_name>` | Create a new full-stack project. |
| `site sync` | Copy models and regenerate backend/frontend code. |
| `site sync --install` / `-i` | Regenerate, then install backend and frontend dependencies. |
| `site run` | Run backend and frontend. |
| `site run --component backend` | Run only FastAPI. |
| `site run --component frontend` | Run only Vite. |
| `site run <project_path> --component all` | Run a project from another directory. |
| `site build [-c backend\|frontend\|all] [-e docker\|podman] [-t TAG] [-p PORT]` | Build images and generate `docker-compose.yml`. |
| `site compose [--engine docker\|podman] up -d` | Forward a Compose command to Docker Compose or Podman Compose. |

`component` is an option: use `site run --component backend`, not `site run backend`.

## Project configuration

`site_config.json` is created with sensible defaults. Common settings are:

```json
{
  "project_name": "Inventory",
  "database_url": "sqlite:///./app.db",
  "upload_dir": "uploads",
  "secret_key": "replace-with-a-random-production-secret",
  "access_token_expire_minutes": 11520,
  "allowed_origins": ["http://localhost:5173", "http://localhost:3000"],
  "style": "normal",
  "radius": 1.0,
  "nav_order": ["user", "category", "product"]
}
```

Supported themes are `normal`, `industrial`, `anime` and `cute`. Use a production database URL and a strong, private `secret_key` outside local development.

## Model basics

```python
from typing import Optional
from sqlmodel import Field, SQLModel

class Product(SQLModel, table=True):
    __onesite__ = {
        "translations": {
            "zh": {
                "name": "商品",
                "fields": {"name": "名称", "price": "价格"},
            },
        },
    }

    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(
        unique=True,
        sa_column_kwargs={"info": {"site_props": {"is_search_field": True}}},
    )
    price: float = 0
    is_active: bool = True
```

Field types, requiredness, defaults, enums and unique constraints become API validation and form controls. Fields named like `image`, `avatar`, `photo`, `*_image` use the image uploader; `file`, `attachment`, `*_file` use the file uploader. Override detection with `site_props.component`: `image`, `file`, `textarea` or `json`.

### Business primary keys

Integer IDs with `default=None` are database-generated and are not accepted by the create API by default. A business key can be passed on creation by making the `id` field required and explicitly granting it create permission:

```python
class Device(SQLModel, table=True):
    id: str = Field(
        primary_key=True,
        nullable=False,
        sa_column_kwargs={"info": {"site_props": {"permissions": "rcu"}}},
    )
    name: str
```

OneSite generates `id: str` consistently in its create/read schemas, endpoint paths, CRUD/service functions and frontend client for this model. Primary keys are immutable through the normal update API.

### Relations

Foreign keys are inferred from a `{target}_id` field with `foreign_key="target.id"`:

```python
category_id: Optional[int] = Field(default=None, foreign_key="category.id")
```

The generated UI selects and displays related labels. Make a useful target label field `unique=True` and/or `is_search_field=True`.

For many-to-many relations, mark the link table:

```python
class ProductTagLink(SQLModel, table=True):
    __onesite__ = {"is_link_table": True}
    product_id: Optional[int] = Field(default=None, primary_key=True, foreign_key="product.id")
    tag_id: Optional[int] = Field(default=None, primary_key=True, foreign_key="tag.id")
```

Link tables with only relation keys become multi-select fields; extra fields keep standalone CRUD support. Self-referencing FKs can generate a tree view.

## `__onesite__` model configuration

Place model-level options in `__onesite__` (or table `info.site_props`). Key options include:

| Option | Effect |
| --- | --- |
| `translations` | Model, field and enum labels for `zh`/`en`. |
| `icon` | Lucide icon name for navigation. |
| `permissions` | Model CRUD permissions by role. |
| `visible` | Navigation visibility by role. |
| `owner_field` | User-owned resource filtering, e.g. `"owner_id"`. |
| `is_link_table` | Marks a many-to-many link table. |
| `is_singleton` | Creates a single configuration-like record UI/API. |
| `frontend_only` | Excludes a model from backend persistence. |
| `page_edit` | Use a full page rather than dialogs for create/edit. |
| `actions` | Adds permission-controlled custom action buttons. |
| `importable` / `exportable` | Enables CSV import/export flows. |
| `import_key` | Field used for import upsert matching. |
| `refresh_interval` | Enables periodic list refresh. |
| `visualize` | Adds generated dashboard statistics/charts. |
| `is_notification_table` | Enables notification-center behavior and realtime push. |
| `time_series_table` | Configures TimescaleDB/time-series generation. |

Useful field-level `site_props` are `permissions`, `is_search_field`, `component`, `create_optional`, `update_optional`, `is_foreign_key`, `reverse_display`, `allow_download`, `group`, `fixed_keys` and `lock_keys`.

## Permissions and visibility

Permissions are independent at three layers:

1. Model CRUD controls endpoint and button access: `c`, `r`, `u`, `d`.
2. Field CRU controls a field's presence in create, read and update schemas/forms: `c`, `r`, `u`.
3. `visible` controls whether the model appears in navigation.

```python
class PremiumContent(SQLModel, table=True):
    __onesite__ = {
        "permissions": {"user": "", "admin": "crud", "developer": "crud"},
        "visible": ["admin", "developer"],
    }
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str = Field(sa_column_kwargs={"info": {"site_props": {
        "permissions": {"user": "r", "admin": "cru", "developer": "cru"},
    }}})
```

Roles are `user`, `admin`, `developer` (in ascending hierarchy). A model permission not specified for a role is no access; an unset model permission defaults to full CRUD for all roles. Field permissions inherit the model permissions without `d`. The implicit defaults are `id`: hidden, `created_at`: read, `updated_at`: read/update; explicitly set field permissions override them.

`owner_field` adds server-side ownership protection: non-admin users only list, read, update and delete their own records, and creation assigns their own user ID.

## Generated capabilities

- JWT authentication and role-based access control
- REST CRUD, unique-constraint validation, list search and filters
- Image/file upload and download controls
- JSON fields, including structured Pydantic-model editors and lockable keys
- Foreign-key label display, M2M selectors, tree views and owner-scoped resources
- Custom actions with role permissions and conditional field updates
- CSV import/export with import-key upserts
- Singleton/configuration models
- Notification center and WebSocket real-time updates
- Dashboard statistics/charts and configurable auto-refresh
- Scheduled jobs via APScheduler with UI management
- EN/ZH locale generation and four built-in themes
- Docker/Podman images and generated Compose configuration
- Optional time-series/TimescaleDB flows

## Container deployment

After syncing a project, build and start it:

```bash
site build --engine docker --tag v1 --port 3000
site compose up -d
site compose logs -f
```

`site build` writes `docker-compose.yml`. PostgreSQL is included when `database_url` starts with `postgresql`; configure production credentials and API origins before exposing the application.

## Generated project layout

```text
project/
├── site_config.json
├── models/                   # SQLModel source of truth
├── backend/app/
│   ├── api/endpoints/         # generated REST routes
│   ├── schemas/ cruds/ services/
│   └── models/                # synced model copy
└── frontend/src/
    ├── pages/ services/ stores/
    └── components/
```

See the [Chinese training guide](docs/onesite-training-guide.md) and `examples/` for end-to-end model examples, including permissions and IoT/time-series scenarios.
