Plinx 0.0.1__py3-none-any.whl → 1.0.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.
- {Plinx-0.0.1.dist-info → Plinx-1.0.0.dist-info}/METADATA +28 -21
- {Plinx-0.0.1.dist-info → Plinx-1.0.0.dist-info}/RECORD +8 -5
- plinx/orm/__init__.py +1 -0
- plinx/orm/orm.py +240 -0
- plinx/orm/utils.py +7 -0
- {Plinx-0.0.1.dist-info → Plinx-1.0.0.dist-info}/LICENSE +0 -0
- {Plinx-0.0.1.dist-info → Plinx-1.0.0.dist-info}/WHEEL +0 -0
- {Plinx-0.0.1.dist-info → Plinx-1.0.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: Plinx
|
3
|
-
Version: 0.0
|
3
|
+
Version: 1.0.0
|
4
4
|
Summary: Plinx is an experimental, minimalistic, and extensible web framework and ORM written in Python.
|
5
5
|
Home-page: https://github.com/dhavalsavalia/plinx
|
6
6
|
Author: Dhaval Savalia
|
@@ -15,12 +15,14 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
15
15
|
Requires-Python: >=3.11.0
|
16
16
|
Description-Content-Type: text/markdown
|
17
17
|
License-File: LICENSE
|
18
|
+
Requires-Dist: webob
|
19
|
+
Requires-Dist: parse
|
18
20
|
|
19
21
|
|
20
22
|
# Plinx
|
21
23
|
|
22
24
|

|
23
|
-

|
24
26
|
|
25
27
|
**Plinx** is an experimental, minimalistic, and extensible WSGI-based web framework and ORM written in Python.
|
26
28
|
It is designed to be simple, fast, and easy to extend, making it ideal for rapid prototyping and educational purposes.
|
@@ -29,8 +31,9 @@ It is designed to be simple, fast, and easy to extend, making it ideal for rapid
|
|
29
31
|
|
30
32
|
## Features
|
31
33
|
|
32
|
-
- 🚀 Minimal and fast web framework
|
33
|
-
-
|
34
|
+
- 🚀 Minimal and fast WSGI web framework
|
35
|
+
- 💾 Integrated Object-Relational Mapper (ORM)
|
36
|
+
- 🛣️ Intuitive routing system (including parameterized and class-based routes)
|
34
37
|
- 🧩 Extensible middleware support
|
35
38
|
- 🧪 Simple, readable codebase for learning and hacking
|
36
39
|
- 📝 Type hints and modern Python best practices
|
@@ -39,6 +42,12 @@ It is designed to be simple, fast, and easy to extend, making it ideal for rapid
|
|
39
42
|
|
40
43
|
## Installation
|
41
44
|
|
45
|
+
Install from PyPI:
|
46
|
+
|
47
|
+
```bash
|
48
|
+
pip install Plinx
|
49
|
+
```
|
50
|
+
|
42
51
|
Install directly from the git source:
|
43
52
|
|
44
53
|
```bash
|
@@ -52,19 +61,30 @@ pip install git+https://github.com/dhavalsavalia/plinx.git
|
|
52
61
|
Create a simple web application in seconds:
|
53
62
|
|
54
63
|
```python
|
64
|
+
# myapp.py
|
55
65
|
from plinx import Plinx
|
56
66
|
|
57
67
|
app = Plinx()
|
58
68
|
|
59
69
|
@app.route("/")
|
60
70
|
def index(request, response):
|
61
|
-
response.text = "Hello,
|
71
|
+
response.text = "Hello, Plinx 1.0.0!"
|
72
|
+
|
73
|
+
# Example using the ORM (requires database setup)
|
74
|
+
# from plinx.orm import Database, Table, Column
|
75
|
+
# db = Database("my_database.db")
|
76
|
+
# class Item(Table):
|
77
|
+
# name = Column(str)
|
78
|
+
# count = Column(int)
|
79
|
+
# db.create(Item)
|
80
|
+
# db.save(Item(name="Example", count=1))
|
62
81
|
```
|
63
82
|
|
64
|
-
Run your app
|
83
|
+
Run your app using a WSGI server (like `gunicorn`):
|
65
84
|
|
66
85
|
```bash
|
67
|
-
|
86
|
+
pip install gunicorn
|
87
|
+
gunicorn myapp:app
|
68
88
|
```
|
69
89
|
|
70
90
|
## Testing
|
@@ -77,19 +97,6 @@ pytest --cov=.
|
|
77
97
|
|
78
98
|
---
|
79
99
|
|
80
|
-
## Roadmap
|
81
|
-
|
82
|
-
- [x] Web Framework
|
83
|
-
- [x] Routing
|
84
|
-
- [x] Explicit Routing Methods (GET, POST, etc.)
|
85
|
-
- [x] Parameterized Routes
|
86
|
-
- [x] Class Based Routes
|
87
|
-
- [x] Django-like Routes
|
88
|
-
- [x] Middleware Support
|
89
|
-
- [ ] ORM
|
90
|
-
|
91
|
-
---
|
92
|
-
|
93
100
|
## Contributing
|
94
101
|
|
95
102
|
Contributions are welcome! Please open issues or submit pull requests for improvements, bug fixes, or new features.
|
@@ -104,7 +111,7 @@ This project is licensed under the MIT License. See [LICENSE](LICENSE) for detai
|
|
104
111
|
|
105
112
|
## Author & Contact
|
106
113
|
|
107
|
-
Created and maintained by [Dhaval Savalia](https://github.com/dhavalsavalia).
|
114
|
+
Created and maintained by [Dhaval Savalia](https://github.com/dhavalsavalia).
|
108
115
|
For questions or opportunities, feel free to reach out via [LinkedIn](https://www.linkedin.com/in/dhavalsavalia/) or open an issue.
|
109
116
|
|
110
117
|
---
|
@@ -5,8 +5,11 @@ plinx/middleware.py,sha256=OiNDhKWpmpKkAIbSWxRzP_tQXSyfDToos-MQMDPSUWM,2161
|
|
5
5
|
plinx/response.py,sha256=oDZPF9V76HhqdUZLzSmoLw_q-GG9hc5krfCBgNm3OfU,1460
|
6
6
|
plinx/status_codes.py,sha256=V7vX7_ujYIaVjO3I3eVUnuNKiTXMSov56wJt8tvMbAM,700
|
7
7
|
plinx/utils.py,sha256=WaU_j7YXsdlZh3M33B2vXjRPAU26lBeQ5sV2ZasXVwE,352
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
Plinx-0.0.
|
12
|
-
Plinx-0.0.
|
8
|
+
plinx/orm/__init__.py,sha256=ryDdzvF6Eh6RIHyEgym6UKCaFMnMitwucazpiWuvICQ,61
|
9
|
+
plinx/orm/orm.py,sha256=FdwLdT-4I5rnhfTCu7v9iBZZ5xlZHZ36equqPAHYvhk,7335
|
10
|
+
plinx/orm/utils.py,sha256=uXT2JV9cVgLTnm_EkguqJglsbIfSPXcnjMQBNFJ8M08,116
|
11
|
+
Plinx-1.0.0.dist-info/LICENSE,sha256=MljMjTJD6oOY41eZWLDZ4x8FrTUcl7ElKxWIXJXUwLM,1066
|
12
|
+
Plinx-1.0.0.dist-info/METADATA,sha256=cDxPb9HkViq-kG_JWVI3YFWI6APojOUiPSZSotzBfFY,2819
|
13
|
+
Plinx-1.0.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
14
|
+
Plinx-1.0.0.dist-info/top_level.txt,sha256=U_4P3aFsTEhhvfiE3sVbJKAG9Fp4IPC5d6zpttayAZw,6
|
15
|
+
Plinx-1.0.0.dist-info/RECORD,,
|
plinx/orm/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
from .orm import Column, Database, ForeignKey, Table # noqa
|
plinx/orm/orm.py
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
import inspect
|
2
|
+
import sqlite3
|
3
|
+
from typing import Generic, TypeVar
|
4
|
+
|
5
|
+
from .utils import SQLITE_TYPE_MAP
|
6
|
+
|
7
|
+
T = TypeVar("T")
|
8
|
+
|
9
|
+
|
10
|
+
class Database:
|
11
|
+
def __init__(self, path: str):
|
12
|
+
self.connection = sqlite3.Connection(path)
|
13
|
+
|
14
|
+
def create(self, table: "Table"):
|
15
|
+
self.connection.execute(table._get_create_sql())
|
16
|
+
|
17
|
+
def save(self, instance: "Table"):
|
18
|
+
sql, values = instance._get_insert_sql()
|
19
|
+
cursor = self.connection.execute(sql, values)
|
20
|
+
instance._data["id"] = cursor.lastrowid
|
21
|
+
self.connection.commit()
|
22
|
+
|
23
|
+
def all(self, table: "Table"):
|
24
|
+
sql, fields = table._get_select_all_sql()
|
25
|
+
rows = self.connection.execute(sql).fetchall()
|
26
|
+
|
27
|
+
result = []
|
28
|
+
|
29
|
+
for row in rows:
|
30
|
+
properties = {}
|
31
|
+
for field, value in zip(fields, row):
|
32
|
+
if field.endswith("_id"):
|
33
|
+
foreign_key = field[:-3]
|
34
|
+
foreign_table = getattr(table, foreign_key).table
|
35
|
+
properties[foreign_key] = self.get(foreign_table, id=value)
|
36
|
+
else:
|
37
|
+
properties[field] = value
|
38
|
+
result.append(table(**properties))
|
39
|
+
|
40
|
+
return result
|
41
|
+
|
42
|
+
def get(self, table: "Table", **kwargs):
|
43
|
+
sql, fields, params = table._get_select_where_sql(**kwargs)
|
44
|
+
row = self.connection.execute(sql, params).fetchone()
|
45
|
+
|
46
|
+
if row is None:
|
47
|
+
raise Exception(f"{table.__name__} instance with {kwargs} does not exist")
|
48
|
+
|
49
|
+
properties = {}
|
50
|
+
|
51
|
+
for field, value in zip(fields, row):
|
52
|
+
if field.endswith("_id"):
|
53
|
+
foreign_key = field[:-3]
|
54
|
+
foreign_table = getattr(table, foreign_key).table
|
55
|
+
properties[foreign_key] = self.get(foreign_table, id=value)
|
56
|
+
else:
|
57
|
+
properties[field] = value
|
58
|
+
|
59
|
+
return table(**properties)
|
60
|
+
|
61
|
+
def update(self, instance: "Table"):
|
62
|
+
sql, values = instance._get_update_sql()
|
63
|
+
self.connection.execute(sql, values)
|
64
|
+
self.connection.commit()
|
65
|
+
|
66
|
+
def delete(self, instance: "Table"):
|
67
|
+
sql, values = instance._get_delete_sql()
|
68
|
+
self.connection.execute(sql, values)
|
69
|
+
self.connection.commit()
|
70
|
+
|
71
|
+
def close(self):
|
72
|
+
if self.connection:
|
73
|
+
self.connection.close()
|
74
|
+
self.connection = None
|
75
|
+
|
76
|
+
@property
|
77
|
+
def tables(self):
|
78
|
+
SELECT_TABLES_SQL = "SELECT name FROM sqlite_master WHERE type = 'table';"
|
79
|
+
return [x[0] for x in self.connection.execute(SELECT_TABLES_SQL).fetchall()]
|
80
|
+
|
81
|
+
|
82
|
+
class Column:
|
83
|
+
def __init__(self, type: Generic[T]):
|
84
|
+
self.type = type
|
85
|
+
|
86
|
+
@property
|
87
|
+
def sql_type(self):
|
88
|
+
return SQLITE_TYPE_MAP[self.type]
|
89
|
+
|
90
|
+
|
91
|
+
class ForeignKey:
|
92
|
+
def __init__(self, table):
|
93
|
+
self.table = table
|
94
|
+
|
95
|
+
|
96
|
+
class Table:
|
97
|
+
def __init__(self, **kwargs):
|
98
|
+
self._data = {"id": None}
|
99
|
+
|
100
|
+
for key, value in kwargs.items():
|
101
|
+
self._data[key] = value
|
102
|
+
|
103
|
+
def __getattribute__(self, key):
|
104
|
+
"""
|
105
|
+
Values to be access are in `self._data`
|
106
|
+
Accessing without __getattribute__ will return Column or ForeignKey and not the actual value
|
107
|
+
"""
|
108
|
+
# Why use super().__getattribute__ instead of self._data[key]?
|
109
|
+
# Because otherwise it will create an infinite loop since __getattribute__ will call itself
|
110
|
+
# and will never return the value
|
111
|
+
_data = super().__getattribute__("_data")
|
112
|
+
if key in _data:
|
113
|
+
return _data[key]
|
114
|
+
return super().__getattribute__(key)
|
115
|
+
|
116
|
+
def __setattr__(self, key, value):
|
117
|
+
"""
|
118
|
+
Values to be set are in `self._data`
|
119
|
+
"""
|
120
|
+
super().__setattr__(key, value)
|
121
|
+
if key in self._data:
|
122
|
+
self._data[key] = value
|
123
|
+
|
124
|
+
@classmethod
|
125
|
+
def _get_create_sql(cls):
|
126
|
+
CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS {name} ({fields});"
|
127
|
+
fields = [
|
128
|
+
"id INTEGER PRIMARY KEY AUTOINCREMENT",
|
129
|
+
]
|
130
|
+
|
131
|
+
for name, field in inspect.getmembers(cls):
|
132
|
+
if isinstance(field, Column):
|
133
|
+
fields.append(f"{name} {field.sql_type}")
|
134
|
+
elif isinstance(field, ForeignKey):
|
135
|
+
fields.append(f"{name}_id INTEGER")
|
136
|
+
|
137
|
+
fields = ", ".join(fields)
|
138
|
+
name = cls.__name__.lower()
|
139
|
+
return CREATE_TABLE_SQL.format(name=name, fields=fields)
|
140
|
+
|
141
|
+
def _get_insert_sql(self):
|
142
|
+
INSERT_SQL = "INSERT INTO {name} ({fields}) VALUES ({placeholders});"
|
143
|
+
|
144
|
+
cls = self.__class__
|
145
|
+
fields = []
|
146
|
+
placeholders = []
|
147
|
+
values = []
|
148
|
+
|
149
|
+
for name, field in inspect.getmembers(cls):
|
150
|
+
if isinstance(field, Column):
|
151
|
+
fields.append(name)
|
152
|
+
values.append(getattr(self, name))
|
153
|
+
placeholders.append("?")
|
154
|
+
elif isinstance(field, ForeignKey):
|
155
|
+
fields.append(name + "_id")
|
156
|
+
values.append(getattr(self, name).id)
|
157
|
+
placeholders.append("?")
|
158
|
+
|
159
|
+
fields = ", ".join(fields)
|
160
|
+
placeholders = ", ".join(placeholders)
|
161
|
+
|
162
|
+
sql = INSERT_SQL.format(
|
163
|
+
name=cls.__name__.lower(), fields=fields, placeholders=placeholders
|
164
|
+
)
|
165
|
+
|
166
|
+
return sql, values
|
167
|
+
|
168
|
+
@classmethod
|
169
|
+
def _get_select_all_sql(cls):
|
170
|
+
SELECT_ALL_SQL = "SELECT {fields} FROM {name};"
|
171
|
+
|
172
|
+
fields = ["id"]
|
173
|
+
|
174
|
+
for name, field in inspect.getmembers(cls):
|
175
|
+
if isinstance(field, Column):
|
176
|
+
fields.append(name)
|
177
|
+
elif isinstance(field, ForeignKey):
|
178
|
+
fields.append(name + "_id")
|
179
|
+
|
180
|
+
return SELECT_ALL_SQL.format(
|
181
|
+
fields=", ".join(fields),
|
182
|
+
name=cls.__name__.lower(),
|
183
|
+
), fields
|
184
|
+
|
185
|
+
@classmethod
|
186
|
+
def _get_select_where_sql(cls, **kwargs):
|
187
|
+
SELECT_WHERE_SQL = "SELECT {fields} FROM {name} WHERE {query};"
|
188
|
+
|
189
|
+
fields = ["id"]
|
190
|
+
query = []
|
191
|
+
values = []
|
192
|
+
|
193
|
+
for name, field in inspect.getmembers(cls):
|
194
|
+
if isinstance(field, Column):
|
195
|
+
fields.append(name)
|
196
|
+
elif isinstance(field, ForeignKey):
|
197
|
+
fields.append(name + "_id")
|
198
|
+
|
199
|
+
for key, value in kwargs.items():
|
200
|
+
query.append(f"{key} = ?")
|
201
|
+
values.append(value)
|
202
|
+
|
203
|
+
return (
|
204
|
+
SELECT_WHERE_SQL.format(
|
205
|
+
fields=", ".join(fields),
|
206
|
+
name=cls.__name__.lower(),
|
207
|
+
query=", ".join(query),
|
208
|
+
),
|
209
|
+
fields,
|
210
|
+
values,
|
211
|
+
)
|
212
|
+
|
213
|
+
def _get_update_sql(self):
|
214
|
+
UPDATE_SQL = "UPDATE {name} SET {fields} WHERE id = ?;"
|
215
|
+
|
216
|
+
cls = self.__class__
|
217
|
+
fields = []
|
218
|
+
values = []
|
219
|
+
|
220
|
+
for name, field in inspect.getmembers(cls):
|
221
|
+
if isinstance(field, Column):
|
222
|
+
fields.append(name)
|
223
|
+
values.append(getattr(self, name))
|
224
|
+
elif isinstance(field, ForeignKey):
|
225
|
+
fields.append(name + "_id")
|
226
|
+
values.append(getattr(self, name).id)
|
227
|
+
|
228
|
+
values.append(getattr(self, "id"))
|
229
|
+
|
230
|
+
return UPDATE_SQL.format(
|
231
|
+
name=cls.__name__.lower(),
|
232
|
+
fields=", ".join([f"{field} = ?" for field in fields]),
|
233
|
+
), values
|
234
|
+
|
235
|
+
def _get_delete_sql(self):
|
236
|
+
DELETE_SQL = "DELETE FROM {name} WHERE id = ?;"
|
237
|
+
|
238
|
+
return DELETE_SQL.format(
|
239
|
+
name=self.__class__.__name__.lower()
|
240
|
+
), [getattr(self, "id")]
|
plinx/orm/utils.py
ADDED
File without changes
|
File without changes
|
File without changes
|