Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for structured scaffold in create command #970

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9828c99
feat: added simple/structured scaffolding
ashupednekar Sep 25, 2024
56e174a
chore: adding auto discover routes
ashupednekar Sep 25, 2024
03918ac
chore: added discover routes
ashupednekar Sep 25, 2024
10d3c83
chore: updated server
ashupednekar Sep 25, 2024
0baab2b
feat: finished no db scaffolding
ashupednekar Sep 25, 2024
481bed8
chore: copied over no db stuff to db
ashupednekar Sep 25, 2024
c0c5dd4
chore: added db connection pool and envs to sqlalchemy structured sca…
ashupednekar Sep 25, 2024
a4c2730
feat: added sqlalchemy and pydantic example models to scaffold
ashupednekar Sep 25, 2024
e98ce50
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 25, 2024
cdcb947
feat: added alembic migration support
ashupednekar Sep 26, 2024
6005967
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 26, 2024
095916f
Merge branch 'sparckles:main' into feat_structured_scaffold
ashupednekar Sep 26, 2024
832bd8f
feat: added alembic migration setup to sqlalchemy structured scaffold
ashupednekar Sep 26, 2024
09b9262
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 26, 2024
2a16aff
style: formatted scaffold and helper code
ashupednekar Sep 26, 2024
a4b49b1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 26, 2024
25840fa
dep: added sqlalchemy dep to scaffolding
ashupednekar Sep 26, 2024
6f2e58c
dep: added alembic to req in scaffold
ashupednekar Sep 26, 2024
4922b1a
dep: added psycopg2 binary to dep in scaffolding
ashupednekar Sep 26, 2024
f2ae1f7
chore: updated sqlalchemy scaffold structure
ashupednekar Sep 26, 2024
c964335
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 26, 2024
4391a39
fix: updated import in models init in sqlalchemy scaffold
ashupednekar Sep 26, 2024
9d66596
chore: reverted accidental change to simple sqlalchemy scaffold
ashupednekar Sep 26, 2024
43726fb
chore: added mutator/selector abc classes to sqlalchemy structured sc…
ashupednekar Sep 26, 2024
9159a71
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 26, 2024
b5895dd
chore: updated test for create_app, added additional test
ashupednekar Sep 28, 2024
f155900
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 28, 2024
182f9cf
chore: ignoring errors in case of docker N, if devops directory doesn…
ashupednekar Sep 28, 2024
054570b
chore: udpated ruff suggestions
ashupednekar Sep 28, 2024
3a93374
Merge branch 'sparckles:main' into feat_structured_scaffold
ashupednekar Sep 28, 2024
155acd7
fix: updated subrouter initializations in scaffold
ashupednekar Oct 1, 2024
d2ddba8
chore: upadated scaffold
ashupednekar Oct 1, 2024
d282b36
chore: updated handler to include pool from a global dependency
ashupednekar Oct 1, 2024
9d7fcf0
chore: added missing arg
ashupednekar Oct 1, 2024
8a3c2ab
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 1, 2024
8ce0753
style: ruff formatting on structured directory
ashupednekar Oct 2, 2024
0a9cbb6
chore: added sample selector and used "session" in sample handler in …
ashupednekar Oct 2, 2024
42c61fc
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 2, 2024
ffe6629
feat: switched to sqlalchemy async
ashupednekar Oct 5, 2024
138e530
fix: updated handler to await async selectors appropriately
ashupednekar Oct 5, 2024
646b58b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 5, 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
18 changes: 17 additions & 1 deletion integration_tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,27 @@


# Unit tests
def test_create_robyn_app():
def test_create_robyn_app_simple():
with patch("robyn.cli.prompt") as mock_prompt:
mock_prompt.return_value = {
"directory": "test_dir",
"docker": "N",
"scaffold_type": "simple",
"project_type": "no-db",
}
with patch("robyn.cli.os.makedirs") as mock_makedirs:
with patch("robyn.cli.shutil.copytree") as mock_copytree, patch("robyn.os.remove") as _mock_remove:
create_robyn_app()
mock_makedirs.assert_called_once()
mock_copytree.assert_called_once()


def test_create_robyn_app_structured():
with patch("robyn.cli.prompt") as mock_prompt:
mock_prompt.return_value = {
"directory": "test_dir",
"docker": "N",
"scaffold_type": "structured",
"project_type": "no-db",
}
with patch("robyn.cli.os.makedirs") as mock_makedirs:
Expand Down
62 changes: 47 additions & 15 deletions robyn/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,53 @@ def create_robyn_app():
},
{
"type": "list",
"message": "Please select project type (Mongo/Postgres/Sqlalchemy/Prisma): ",
"choices": [
Choice("no-db", name="No DB"),
Choice("sqlite", name="Sqlite"),
Choice("postgres", name="Postgres"),
Choice("mongo", name="MongoDB"),
Choice("sqlalchemy", name="SqlAlchemy"),
Choice("prisma", name="Prisma"),
Choice("sqlmodel", name="SQLModel"),
],
"default": Choice("no-db", name="No DB"),
"name": "project_type",
"name": "scaffold_type",
"choices": [Choice("simple", name="Simple"), Choice("structured", name="Structured")],
"message": "Please choose if you'd like the scaffold to be a simple starter kit or an opinionated structure",
},
]
result = prompt(questions=questions)
project_dir_path = Path(str(result["directory"])).resolve()
docker = result["docker"]
project_type = str(result["project_type"])
scaffold_type: str = str(result["scaffold_type"])
if scaffold_type == "simple":
scaffold_type: str = "simple"
result = prompt(
questions=[
{
"type": "list",
"message": "Please select project type (Mongo/Postgres/Sqlalchemy/Prisma): ",
"choices": [
Choice("no-db", name="No DB"),
Choice("sqlite", name="Sqlite"),
Choice("postgres", name="Postgres"),
Choice("mongo", name="MongoDB"),
Choice("sqlalchemy", name="SqlAlchemy"),
Choice("prisma", name="Prisma"),
Choice("sqlmodel", name="SQLModel"),
],
"default": Choice("no-db", name="No DB"),
"name": "project_type",
}
]
)
project_type = str(result["project_type"])
else:
result = prompt(
questions=[
{
"type": "list",
"message": "Please select project type (Mongo/Postgres/Sqlalchemy/Prisma): ",
"choices": [
Choice("no-db", name="No DB"),
Choice("sqlalchemy", name="SqlAlchemy"),
],
"default": Choice("no-db", name="No DB"),
"name": "project_type",
}
]
)
project_type = str(result["project_type"])

final_project_dir_path = (CURRENT_WORKING_DIR / project_dir_path).resolve()

Expand All @@ -62,12 +91,15 @@ def create_robyn_app():
# Create a new directory for the project
os.makedirs(final_project_dir_path, exist_ok=True)

selected_project_template = (SCAFFOLD_DIR / Path(project_type)).resolve()
selected_project_template = (SCAFFOLD_DIR / Path(scaffold_type) / Path(project_type)).resolve()
shutil.copytree(str(selected_project_template), str(final_project_dir_path), dirs_exist_ok=True)

# If docker is not needed, delete the docker file
if docker == "N":
os.remove(f"{final_project_dir_path}/Dockerfile")
if scaffold_type == "simple":
os.remove(f"{final_project_dir_path}/Dockerfile")
else:
shutil.rmtree(f"{final_project_dir_path}/devops", ignore_errors=True)

print(f"New Robyn project created in '{final_project_dir_path}' ")

Expand Down
47 changes: 47 additions & 0 deletions robyn/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import Any, Type, Tuple
from pydantic_settings import (
BaseSettings,
EnvSettingsSource,
PydanticBaseSettingsSource,
)
from pydantic import ConfigDict

import importlib
import pkgutil
import logging

from robyn import Robyn

logger = logging.getLogger(__name__)


def discover_routes(handler_path: str = "api.handlers") -> Robyn:
mux: Robyn = Robyn(__file__)
package = importlib.import_module(handler_path)
for _, module_name, _ in pkgutil.iter_modules(package.__path__, package.__name__ + "."):
module = importlib.import_module(module_name)
logger.info(f"member: {module}")
mux.include_router(module.router)
return mux


class AcceptArrayEnvsSource(EnvSettingsSource):
def prepare_field_value(self, field_name: str, field: Any, value: Any, value_is_complex: bool) -> Any:
if isinstance(field.annotation, type) and issubclass(field.annotation, list) and isinstance(value, str):
return [x.strip() for x in value.split(",") if x]
return value


class BaseConfig(BaseSettings):
@classmethod
def settings_customise_sources(
cls,
settings_cls: Type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> Tuple[PydanticBaseSettingsSource, ...]:
return (AcceptArrayEnvsSource(settings_cls),)

model_config = ConfigDict(extra="ignore") # Ignore extra environment variables
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Empty file.
14 changes: 14 additions & 0 deletions robyn/scaffold/structured/no-db/api/handlers/probes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from robyn import SubRouter


router = SubRouter(__name__, prefix="/")


@router.get("/livez/")
def livez() -> str:
return "live"


@router.get("/healthz/")
def healthz() -> str:
return "healthy"
13 changes: 13 additions & 0 deletions robyn/scaffold/structured/no-db/api/handlers/sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from robyn import SubRouter

router = SubRouter(__name__, "/sample")


class SampleHandlers:
@router.post("/one")
@staticmethod
def one(): ...

@router.get("/two")
@staticmethod
def two(): ...
Comment on lines +6 to +13
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we confining the two methods in a namespace?

Empty file.
8 changes: 8 additions & 0 deletions robyn/scaffold/structured/no-db/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from robyn.helpers import BaseConfig


class Settings(BaseConfig):
service_port: int


settings = Settings()
1 change: 1 addition & 0 deletions robyn/scaffold/structured/no-db/config.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SERVICE_PORT=3000
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is this being used

14 changes: 14 additions & 0 deletions robyn/scaffold/structured/no-db/devops/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM python:3.11-bookworm AS builder

WORKDIR /workspace

COPY . .
RUN pip install --no-cache-dir --upgrade -r requirements.txt --target=/workspace/deps

FROM gcr.io/distroless/python3-debian11

WORKDIR /workspace
COPY --from=builder /workspace /workspace
ENV PYTHONPATH=/workspace/deps

CMD ["server.py", "--log-level=DEBUG"]
Empty file.
11 changes: 11 additions & 0 deletions robyn/scaffold/structured/no-db/devops/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: '3'
services:

service:
image: service # build an image with this tag
container_name: "service"
env_file:
- ../config.env
network_mode: host


3 changes: 3 additions & 0 deletions robyn/scaffold/structured/no-db/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
robyn
pydantic~=2.7.4
pydantic-settings~=2.2.1
11 changes: 11 additions & 0 deletions robyn/scaffold/structured/no-db/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from robyn.helpers import discover_routes
from robyn import Robyn

from conf import settings

app: Robyn = discover_routes("api.handlers")
# note: if you prefer to manuall refine routes, use your build_routes function instead


if __name__ == "__main__":
app.start(host="0.0.0.0", port=settings.service_port)
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .user import Base as user

metadata = [user.metadata]
36 changes: 36 additions & 0 deletions robyn/scaffold/structured/sqlalchemy/adaptors/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from sqlalchemy import String, Integer, Boolean
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from pydantic import BaseModel


class Base(DeclarativeBase):
pass


class User(Base):
__tablename__ = "users"

id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
username: Mapped[str] = mapped_column(String, unique=True, index=True)
hashed_password: Mapped[str] = mapped_column(String)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False)


class UserRead(BaseModel):
id: int
username: str
is_active: bool
is_superuser: bool

class Config:
orm_mode = True


# Pydantic model for creating/updating a user
class UserCreate(BaseModel):
username: str
password: str

class Config:
orm_mode = True
14 changes: 14 additions & 0 deletions robyn/scaffold/structured/sqlalchemy/adaptors/mutators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from abc import ABC, abstractmethod

from pydantic import BaseModel


class Mutator(ABC):
@abstractmethod
def create(self, model: BaseModel): ...

@abstractmethod
def update(self, **kwargs): ...

@abstractmethod
def delete(self, **kwargs): ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from abc import ABC, abstractmethod


class Mutator(ABC):
@abstractmethod
def retrieve(self, **kwargs): ...

@abstractmethod
def list(self, **kwargs): ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from sqlalchemy import text


async def sample_selector(conn):
await conn.execute(text("select * from sample;"))
Loading
Loading