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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Plinx
3
- Version: 0.0.1
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
  ![purpose](https://img.shields.io/badge/purpose-learning-green.svg)
23
- ![PyPI](https://img.shields.io/pypi/v/bumbo.svg)
25
+ ![PyPI](https://img.shields.io/pypi/v/Plinx.svg)
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
- - 🛣️ Intuitive routing system
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, world!"
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 (example, assuming you have an ASGI server like `uvicorn`):
83
+ Run your app using a WSGI server (like `gunicorn`):
65
84
 
66
85
  ```bash
67
- uvicorn myapp:app
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
- Plinx-0.0.1.dist-info/LICENSE,sha256=MljMjTJD6oOY41eZWLDZ4x8FrTUcl7ElKxWIXJXUwLM,1066
9
- Plinx-0.0.1.dist-info/METADATA,sha256=N-vDBHtG4pV2LKumS4yh40_IYzKKh8PgRf1G3Umw_Q0,2568
10
- Plinx-0.0.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
11
- Plinx-0.0.1.dist-info/top_level.txt,sha256=U_4P3aFsTEhhvfiE3sVbJKAG9Fp4IPC5d6zpttayAZw,6
12
- Plinx-0.0.1.dist-info/RECORD,,
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
@@ -0,0 +1,7 @@
1
+ SQLITE_TYPE_MAP = {
2
+ int: "INTEGER",
3
+ float: "REAL",
4
+ str: "TEXT",
5
+ bytes: "BLOB",
6
+ bool: "INTEGER"
7
+ }
File without changes
File without changes