hookly 0.1.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.
- hookly-0.1.0/.gitignore +8 -0
- hookly-0.1.0/.idea/.gitignore +5 -0
- hookly-0.1.0/.idea/hookly.iml +12 -0
- hookly-0.1.0/.idea/inspectionProfiles/profiles_settings.xml +6 -0
- hookly-0.1.0/.idea/misc.xml +6 -0
- hookly-0.1.0/.idea/workspace.xml +64 -0
- hookly-0.1.0/PKG-INFO +135 -0
- hookly-0.1.0/README.md +110 -0
- hookly-0.1.0/hookly/__init__.py +25 -0
- hookly-0.1.0/hookly/api/__init__.py +3 -0
- hookly-0.1.0/hookly/api/core.py +70 -0
- hookly-0.1.0/hookly/crm/__init__.py +3 -0
- hookly-0.1.0/hookly/crm/core.py +72 -0
- hookly-0.1.0/hookly/db/__init__.py +3 -0
- hookly-0.1.0/hookly/db/sqlite.py +72 -0
- hookly-0.1.0/main.py +15 -0
- hookly-0.1.0/pyproject.toml +37 -0
- hookly-0.1.0/tests/test_basic.py +20 -0
hookly-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="PYTHON_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$" />
|
|
5
|
+
<orderEntry type="inheritedJdk" />
|
|
6
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
7
|
+
</component>
|
|
8
|
+
<component name="PyDocumentationSettings">
|
|
9
|
+
<option name="format" value="PLAIN" />
|
|
10
|
+
<option name="myDocStringFormat" value="Plain" />
|
|
11
|
+
</component>
|
|
12
|
+
</module>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="AutoImportSettings">
|
|
4
|
+
<option name="autoReloadType" value="SELECTIVE" />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="ChangeListManager">
|
|
7
|
+
<list default="true" id="5347a489-26f6-4798-9b04-f4a3a2f63537" name="Changes" comment="" />
|
|
8
|
+
<option name="SHOW_DIALOG" value="false" />
|
|
9
|
+
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
10
|
+
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
11
|
+
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
12
|
+
</component>
|
|
13
|
+
<component name="FileTemplateManagerImpl">
|
|
14
|
+
<option name="RECENT_TEMPLATES">
|
|
15
|
+
<list>
|
|
16
|
+
<option value="Python Script" />
|
|
17
|
+
</list>
|
|
18
|
+
</option>
|
|
19
|
+
</component>
|
|
20
|
+
<component name="ProjectColorInfo">{
|
|
21
|
+
"associatedIndex": 2,
|
|
22
|
+
"fromUser": false
|
|
23
|
+
}</component>
|
|
24
|
+
<component name="ProjectId" id="3JjBxqpJ9mZ3SuDLXtBXK1wfoWF" />
|
|
25
|
+
<component name="ProjectViewState">
|
|
26
|
+
<option name="hideEmptyMiddlePackages" value="true" />
|
|
27
|
+
<option name="showLibraryContents" value="true" />
|
|
28
|
+
</component>
|
|
29
|
+
<component name="PropertiesComponent"><![CDATA[{
|
|
30
|
+
"keyToString": {
|
|
31
|
+
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
32
|
+
"Python.main.executor": "Run",
|
|
33
|
+
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
34
|
+
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
|
35
|
+
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
|
36
|
+
"codeWithMe.voiceChat.enabledByDefault": "false",
|
|
37
|
+
"node.js.detected.package.eslint": "true",
|
|
38
|
+
"node.js.detected.package.tslint": "true",
|
|
39
|
+
"node.js.selected.package.eslint": "(autodetect)",
|
|
40
|
+
"node.js.selected.package.tslint": "(autodetect)",
|
|
41
|
+
"nodejs_package_manager_path": "npm"
|
|
42
|
+
}
|
|
43
|
+
}]]></component>
|
|
44
|
+
<component name="SharedIndexes">
|
|
45
|
+
<attachedChunks>
|
|
46
|
+
<set>
|
|
47
|
+
<option value="bundled-python-sdk-b63d5a1f7c97-b61e75351b1f-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-261.22158.340" />
|
|
48
|
+
</set>
|
|
49
|
+
</attachedChunks>
|
|
50
|
+
</component>
|
|
51
|
+
<component name="TaskManager">
|
|
52
|
+
<task active="true" id="Default" summary="Default task">
|
|
53
|
+
<changelist id="5347a489-26f6-4798-9b04-f4a3a2f63537" name="Changes" comment="" />
|
|
54
|
+
<created>1790164852777</created>
|
|
55
|
+
<option name="number" value="Default" />
|
|
56
|
+
<option name="presentableId" value="Default" />
|
|
57
|
+
<updated>1790164852777</updated>
|
|
58
|
+
</task>
|
|
59
|
+
<servers />
|
|
60
|
+
</component>
|
|
61
|
+
<component name="TypeScriptGeneratedFilesManager">
|
|
62
|
+
<option name="version" value="3" />
|
|
63
|
+
</component>
|
|
64
|
+
</project>
|
hookly-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
32
|
+

|
|
33
|
+

|
|
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
|
hookly-0.1.0/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# ⚡ Hookly
|
|
2
|
+
|
|
3
|
+
**Batteries-included backend toolkit for Python.**
|
|
4
|
+
Stop wiring up an API, a database layer and a CRM by hand — import Hookly and call it.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why Hookly
|
|
13
|
+
|
|
14
|
+
Every backend needs the same handful of things: an API layer, a place to store
|
|
15
|
+
data, and something to track contacts, leads or tickets. Hookly ships all
|
|
16
|
+
three as ready-made modules that work together out of the box — you call
|
|
17
|
+
them, you don't rebuild them.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from hookly import Database, CRM, API
|
|
21
|
+
|
|
22
|
+
db = Database("app.db")
|
|
23
|
+
crm = CRM(db)
|
|
24
|
+
api = API("my-service")
|
|
25
|
+
|
|
26
|
+
@api.get("/contacts")
|
|
27
|
+
def list_contacts():
|
|
28
|
+
return crm.list_contacts()
|
|
29
|
+
|
|
30
|
+
@api.post("/contacts")
|
|
31
|
+
def new_contact():
|
|
32
|
+
return crm.add_contact("Ali", phone="+998901234567").__dict__
|
|
33
|
+
|
|
34
|
+
api.run(port=8000)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That's a working API + database + CRM. No boilerplate.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install hookly
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
(local dev: `pip install -e ".[dev]"` from the project root)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Modules
|
|
52
|
+
|
|
53
|
+
### `hookly.db` — SQLite without the ceremony
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from hookly import Database
|
|
57
|
+
|
|
58
|
+
db = Database("app.db")
|
|
59
|
+
db.create_table("users", {"id": "INTEGER PRIMARY KEY", "name": "TEXT"})
|
|
60
|
+
db.insert("users", name="Ali")
|
|
61
|
+
db.fetchall("SELECT * FROM users")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### `hookly.crm` — contacts and leads, ready to go
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from hookly import CRM
|
|
68
|
+
|
|
69
|
+
crm = CRM()
|
|
70
|
+
contact = crm.add_contact("Ali", email="ali@example.com")
|
|
71
|
+
crm.update_status(contact.id, "in_progress")
|
|
72
|
+
crm.list_contacts(status="in_progress")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `hookly.api` — a tiny decorator-based router
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from hookly import API
|
|
79
|
+
|
|
80
|
+
api = API("my-service")
|
|
81
|
+
|
|
82
|
+
@api.get("/ping")
|
|
83
|
+
def ping():
|
|
84
|
+
return {"status": "ok"}
|
|
85
|
+
|
|
86
|
+
api.run(port=8000)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Roadmap
|
|
92
|
+
|
|
93
|
+
- [ ] Async support (`AsyncDatabase`, `AsyncAPI`)
|
|
94
|
+
- [ ] Auth module (`hookly.auth`)
|
|
95
|
+
- [ ] PostgreSQL backend for `hookly.db`
|
|
96
|
+
- [ ] CLI: `hookly new my-project`
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Philosophy
|
|
101
|
+
|
|
102
|
+
Hookly isn't trying to replace FastAPI or Django — it's for the moment
|
|
103
|
+
before that, when you just need something working *now*: a table, an
|
|
104
|
+
endpoint, a contact list. Grow out of it, swap in the big tools later.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -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__"]
|
|
@@ -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()
|
|
@@ -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,))
|
|
@@ -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()
|
hookly-0.1.0/main.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from hookly import Database, CRM, API
|
|
2
|
+
|
|
3
|
+
db = Database("app.db")
|
|
4
|
+
crm = CRM(db)
|
|
5
|
+
api = API("my-service")
|
|
6
|
+
|
|
7
|
+
@api.get("/contacts")
|
|
8
|
+
def list_contacts():
|
|
9
|
+
return crm.list_contacts()
|
|
10
|
+
|
|
11
|
+
@api.post("/contacts")
|
|
12
|
+
def new_contact():
|
|
13
|
+
return crm.add_contact("Ali", phone="+998901234567").__dict__
|
|
14
|
+
|
|
15
|
+
api.run(port=8000)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hookly"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Batteries-included backend toolkit for Python — API, DB and CRM, ready to call."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Your Name", email = "you@example.com" }]
|
|
13
|
+
keywords = ["backend", "api", "crm", "sqlite", "toolkit", "framework"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = ["pytest>=7.0"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/yourusername/hookly"
|
|
33
|
+
Repository = "https://github.com/yourusername/hookly"
|
|
34
|
+
Issues = "https://github.com/yourusername/hookly/issues"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["hookly"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from hookly import Database, CRM
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_db_and_crm(tmp_path):
|
|
5
|
+
db_path = tmp_path / "test.db"
|
|
6
|
+
db = Database(str(db_path))
|
|
7
|
+
crm = CRM(db)
|
|
8
|
+
|
|
9
|
+
contact = crm.add_contact("Ali", phone="+998901234567", email="ali@example.com")
|
|
10
|
+
assert contact.id is not None
|
|
11
|
+
|
|
12
|
+
contacts = crm.list_contacts()
|
|
13
|
+
assert len(contacts) == 1
|
|
14
|
+
assert contacts[0]["name"] == "Ali"
|
|
15
|
+
|
|
16
|
+
crm.update_status(contact.id, "in_progress")
|
|
17
|
+
updated = crm.get_contact(contact.id)
|
|
18
|
+
assert updated["status"] == "in_progress"
|
|
19
|
+
|
|
20
|
+
db.close()
|