hookly 0.1.0__py3-none-any.whl

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.
hookly/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """
2
+ Hookly — batteries-included backend toolkit for Python.
3
+
4
+ Instead of wiring up a DB layer, a CRM module and an API router by hand,
5
+ you import Hookly and call it.
6
+
7
+ from hookly import Database, CRM, API
8
+
9
+ db = Database("app.db")
10
+ crm = CRM(db)
11
+ api = API("my-service")
12
+
13
+ @api.get("/contacts")
14
+ def list_contacts():
15
+ return crm.list_contacts()
16
+
17
+ api.run(port=8000)
18
+ """
19
+
20
+ from .db import Database
21
+ from .crm import CRM
22
+ from .api import API
23
+
24
+ __version__ = "0.1.0"
25
+ __all__ = ["Database", "CRM", "API", "__version__"]
hookly/api/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .core import API
2
+
3
+ __all__ = ["API"]
hookly/api/core.py ADDED
@@ -0,0 +1,70 @@
1
+ """
2
+ hookly.api — a tiny decorator-based JSON API router.
3
+
4
+ No dependencies, no config files: decorate a function, run the server.
5
+ Good enough for internal tools and prototypes; swap in ASGI later for
6
+ production if you outgrow it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from http.server import BaseHTTPRequestHandler, HTTPServer
13
+ from typing import Callable
14
+
15
+
16
+ class API:
17
+ """Call it, don't build it: a minimal HTTP router."""
18
+
19
+ def __init__(self, name: str = "hookly-api") -> None:
20
+ self.name = name
21
+ self._routes: dict[tuple[str, str], Callable] = {}
22
+
23
+ def route(self, path: str, method: str = "GET") -> Callable:
24
+ def decorator(func: Callable) -> Callable:
25
+ self._routes[(method.upper(), path)] = func
26
+ return func
27
+
28
+ return decorator
29
+
30
+ def get(self, path: str) -> Callable:
31
+ return self.route(path, "GET")
32
+
33
+ def post(self, path: str) -> Callable:
34
+ return self.route(path, "POST")
35
+
36
+ def run(self, host: str = "127.0.0.1", port: int = 8000) -> None:
37
+ routes = self._routes
38
+
39
+ class Handler(BaseHTTPRequestHandler):
40
+ def _handle(self, method: str) -> None:
41
+ handler = routes.get((method, self.path))
42
+ if handler is None:
43
+ self.send_response(404)
44
+ self.send_header("Content-Type", "application/json")
45
+ self.end_headers()
46
+ self.wfile.write(b'{"error": "not found"}')
47
+ return
48
+ try:
49
+ result = handler()
50
+ body = json.dumps(result).encode()
51
+ status = 200
52
+ except Exception as exc: # noqa: BLE001 - surface errors as JSON
53
+ body = json.dumps({"error": str(exc)}).encode()
54
+ status = 500
55
+ self.send_response(status)
56
+ self.send_header("Content-Type", "application/json")
57
+ self.end_headers()
58
+ self.wfile.write(body)
59
+
60
+ def do_GET(self) -> None: # noqa: N802 - stdlib naming
61
+ self._handle("GET")
62
+
63
+ def do_POST(self) -> None: # noqa: N802 - stdlib naming
64
+ self._handle("POST")
65
+
66
+ def log_message(self, format: str, *args) -> None: # noqa: A002
67
+ pass # keep the console quiet
68
+
69
+ print(f"⚡ {self.name} running at http://{host}:{port}")
70
+ HTTPServer((host, port), Handler).serve_forever()
hookly/crm/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .core import CRM, Contact
2
+
3
+ __all__ = ["CRM", "Contact"]
hookly/crm/core.py ADDED
@@ -0,0 +1,72 @@
1
+ """
2
+ hookly.crm — a ready-made contacts/leads module.
3
+
4
+ Drop it on top of any Database instance and you get a contacts table,
5
+ lead statuses and simple queries for free.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timezone
12
+ from typing import Optional
13
+
14
+ from ..db import Database
15
+
16
+
17
+ @dataclass
18
+ class Contact:
19
+ id: Optional[int]
20
+ name: str
21
+ phone: str = ""
22
+ email: str = ""
23
+ status: str = "new" # new -> in_progress -> closed
24
+ created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
25
+
26
+
27
+ class CRM:
28
+ """A small CRM: contacts in, statuses tracked, no setup required."""
29
+
30
+ STATUSES = ("new", "in_progress", "closed")
31
+
32
+ def __init__(self, db: Optional[Database] = None) -> None:
33
+ self.db = db or Database("hookly_crm.db")
34
+ self.db.create_table(
35
+ "contacts",
36
+ {
37
+ "id": "INTEGER PRIMARY KEY AUTOINCREMENT",
38
+ "name": "TEXT NOT NULL",
39
+ "phone": "TEXT",
40
+ "email": "TEXT",
41
+ "status": "TEXT DEFAULT 'new'",
42
+ "created_at": "TEXT",
43
+ },
44
+ )
45
+
46
+ def add_contact(self, name: str, phone: str = "", email: str = "") -> Contact:
47
+ contact = Contact(id=None, name=name, phone=phone, email=email)
48
+ contact.id = self.db.insert(
49
+ "contacts",
50
+ name=contact.name,
51
+ phone=contact.phone,
52
+ email=contact.email,
53
+ status=contact.status,
54
+ created_at=contact.created_at,
55
+ )
56
+ return contact
57
+
58
+ def list_contacts(self, status: Optional[str] = None) -> list[dict]:
59
+ if status:
60
+ return self.db.fetchall("SELECT * FROM contacts WHERE status = ?", (status,))
61
+ return self.db.fetchall("SELECT * FROM contacts ORDER BY id DESC")
62
+
63
+ def get_contact(self, contact_id: int) -> Optional[dict]:
64
+ return self.db.fetchone("SELECT * FROM contacts WHERE id = ?", (contact_id,))
65
+
66
+ def update_status(self, contact_id: int, status: str) -> None:
67
+ if status not in self.STATUSES:
68
+ raise ValueError(f"status must be one of {self.STATUSES}")
69
+ self.db.update("contacts", "id = ?", (contact_id,), status=status)
70
+
71
+ def delete_contact(self, contact_id: int) -> None:
72
+ self.db.delete("contacts", "id = ?", (contact_id,))
hookly/db/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .sqlite import Database
2
+
3
+ __all__ = ["Database"]
hookly/db/sqlite.py ADDED
@@ -0,0 +1,72 @@
1
+ """
2
+ hookly.db — a SQLite layer with no ceremony.
3
+
4
+ No models to declare, no migrations to write for simple cases: describe a
5
+ table once, then insert/fetch with plain Python calls.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sqlite3
11
+ from typing import Any, Iterable, Optional
12
+
13
+
14
+ class Database:
15
+ """Thin, friendly wrapper around sqlite3."""
16
+
17
+ def __init__(self, path: str = "hookly.db") -> None:
18
+ self.path = path
19
+ self._conn = sqlite3.connect(self.path)
20
+ self._conn.row_factory = sqlite3.Row
21
+
22
+ # -- low level -----------------------------------------------------
23
+
24
+ def execute(self, query: str, params: Iterable[Any] = ()) -> sqlite3.Cursor:
25
+ cur = self._conn.execute(query, tuple(params))
26
+ self._conn.commit()
27
+ return cur
28
+
29
+ def fetchall(self, query: str, params: Iterable[Any] = ()) -> list[dict]:
30
+ cur = self._conn.execute(query, tuple(params))
31
+ return [dict(row) for row in cur.fetchall()]
32
+
33
+ def fetchone(self, query: str, params: Iterable[Any] = ()) -> Optional[dict]:
34
+ cur = self._conn.execute(query, tuple(params))
35
+ row = cur.fetchone()
36
+ return dict(row) if row else None
37
+
38
+ # -- convenience -----------------------------------------------------
39
+
40
+ def create_table(self, name: str, columns: dict[str, str]) -> None:
41
+ """create_table('users', {'id': 'INTEGER PRIMARY KEY', 'name': 'TEXT'})"""
42
+ cols_sql = ", ".join(f"{col} {ctype}" for col, ctype in columns.items())
43
+ self.execute(f"CREATE TABLE IF NOT EXISTS {name} ({cols_sql})")
44
+
45
+ def insert(self, table: str, **fields: Any) -> int:
46
+ """insert('users', name='Ali', email='ali@example.com') -> new row id"""
47
+ keys = ", ".join(fields.keys())
48
+ placeholders = ", ".join("?" for _ in fields)
49
+ cur = self.execute(
50
+ f"INSERT INTO {table} ({keys}) VALUES ({placeholders})",
51
+ tuple(fields.values()),
52
+ )
53
+ return cur.lastrowid
54
+
55
+ def update(self, table: str, where: str, where_params: Iterable[Any], **fields: Any) -> None:
56
+ set_sql = ", ".join(f"{k} = ?" for k in fields)
57
+ self.execute(
58
+ f"UPDATE {table} SET {set_sql} WHERE {where}",
59
+ tuple(fields.values()) + tuple(where_params),
60
+ )
61
+
62
+ def delete(self, table: str, where: str, where_params: Iterable[Any] = ()) -> None:
63
+ self.execute(f"DELETE FROM {table} WHERE {where}", where_params)
64
+
65
+ def close(self) -> None:
66
+ self._conn.close()
67
+
68
+ def __enter__(self) -> "Database":
69
+ return self
70
+
71
+ def __exit__(self, *exc) -> None:
72
+ self.close()
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.5
2
+ Name: hookly
3
+ Version: 0.1.0
4
+ Summary: Batteries-included backend toolkit for Python — API, DB and CRM, ready to call.
5
+ Project-URL: Homepage, https://github.com/yourusername/hookly
6
+ Project-URL: Repository, https://github.com/yourusername/hookly
7
+ Project-URL: Issues, https://github.com/yourusername/hookly/issues
8
+ Author-email: Your Name <you@example.com>
9
+ License: MIT
10
+ Keywords: api,backend,crm,framework,sqlite,toolkit
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.10
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # ⚡ Hookly
27
+
28
+ **Batteries-included backend toolkit for Python.**
29
+ Stop wiring up an API, a database layer and a CRM by hand — import Hookly and call it.
30
+
31
+ ![PyPI](https://img.shields.io/badge/pypi-hookly-blue)
32
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
33
+ ![License](https://img.shields.io/badge/license-MIT-green)
34
+
35
+ ---
36
+
37
+ ## Why Hookly
38
+
39
+ Every backend needs the same handful of things: an API layer, a place to store
40
+ data, and something to track contacts, leads or tickets. Hookly ships all
41
+ three as ready-made modules that work together out of the box — you call
42
+ them, you don't rebuild them.
43
+
44
+ ```python
45
+ from hookly import Database, CRM, API
46
+
47
+ db = Database("app.db")
48
+ crm = CRM(db)
49
+ api = API("my-service")
50
+
51
+ @api.get("/contacts")
52
+ def list_contacts():
53
+ return crm.list_contacts()
54
+
55
+ @api.post("/contacts")
56
+ def new_contact():
57
+ return crm.add_contact("Ali", phone="+998901234567").__dict__
58
+
59
+ api.run(port=8000)
60
+ ```
61
+
62
+ That's a working API + database + CRM. No boilerplate.
63
+
64
+ ---
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ pip install hookly
70
+ ```
71
+
72
+ (local dev: `pip install -e ".[dev]"` from the project root)
73
+
74
+ ---
75
+
76
+ ## Modules
77
+
78
+ ### `hookly.db` — SQLite without the ceremony
79
+
80
+ ```python
81
+ from hookly import Database
82
+
83
+ db = Database("app.db")
84
+ db.create_table("users", {"id": "INTEGER PRIMARY KEY", "name": "TEXT"})
85
+ db.insert("users", name="Ali")
86
+ db.fetchall("SELECT * FROM users")
87
+ ```
88
+
89
+ ### `hookly.crm` — contacts and leads, ready to go
90
+
91
+ ```python
92
+ from hookly import CRM
93
+
94
+ crm = CRM()
95
+ contact = crm.add_contact("Ali", email="ali@example.com")
96
+ crm.update_status(contact.id, "in_progress")
97
+ crm.list_contacts(status="in_progress")
98
+ ```
99
+
100
+ ### `hookly.api` — a tiny decorator-based router
101
+
102
+ ```python
103
+ from hookly import API
104
+
105
+ api = API("my-service")
106
+
107
+ @api.get("/ping")
108
+ def ping():
109
+ return {"status": "ok"}
110
+
111
+ api.run(port=8000)
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Roadmap
117
+
118
+ - [ ] Async support (`AsyncDatabase`, `AsyncAPI`)
119
+ - [ ] Auth module (`hookly.auth`)
120
+ - [ ] PostgreSQL backend for `hookly.db`
121
+ - [ ] CLI: `hookly new my-project`
122
+
123
+ ---
124
+
125
+ ## Philosophy
126
+
127
+ Hookly isn't trying to replace FastAPI or Django — it's for the moment
128
+ before that, when you just need something working *now*: a table, an
129
+ endpoint, a contact list. Grow out of it, swap in the big tools later.
130
+
131
+ ---
132
+
133
+ ## License
134
+
135
+ MIT
@@ -0,0 +1,10 @@
1
+ hookly/__init__.py,sha256=WF4DIh-JaLs90kriePQPkz8EKLkESqXgKZKtOU4SO4k,543
2
+ hookly/api/__init__.py,sha256=QZO1J9kqpYidnj1MiYtILAUWGmKLDwW2TpvO9gJcczI,41
3
+ hookly/api/core.py,sha256=7yoEmd9cw8IWPVX7G8dH8CLB0EJGQoxn2a0Ohaz8f0k,2495
4
+ hookly/crm/__init__.py,sha256=hnpSDw9g4TvtFD9Gu9RTubzWzbLbUwvJAGZ3TuFjQZU,61
5
+ hookly/crm/core.py,sha256=nDgissnNKZOVtdpJTBk_qhFlP2B_c-5_wiWAKa8GQGA,2394
6
+ hookly/db/__init__.py,sha256=-OGjZDIpfVw1A8HsIQ-ls_RcKmTprul2zM_aBh98M3g,53
7
+ hookly/db/sqlite.py,sha256=PwFCmViqSZUJUpwHZ6eQhWCR6UHb0tnUT6a455qZXIU,2609
8
+ hookly-0.1.0.dist-info/METADATA,sha256=hp1whSKCJR6XmfDfnezbnb5Fd_QnbkUC1-P9QI8aYFc,3336
9
+ hookly-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
10
+ hookly-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any