Plinx 0.0.1__py3-none-any.whl → 1.0.1__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/__init__.py +2 -0
- plinx/applications.py +212 -28
- plinx/methods.py +61 -2
- plinx/middleware.py +99 -22
- plinx/orm/__init__.py +1 -0
- plinx/orm/orm.py +550 -0
- plinx/orm/utils.py +7 -0
- plinx/response.py +78 -6
- plinx/status_codes.py +26 -5
- plinx/utils.py +18 -3
- {Plinx-0.0.1.dist-info → plinx-1.0.1.dist-info}/METADATA +42 -22
- plinx-1.0.1.dist-info/RECORD +15 -0
- {Plinx-0.0.1.dist-info → plinx-1.0.1.dist-info}/WHEEL +1 -1
- Plinx-0.0.1.dist-info/RECORD +0 -12
- {Plinx-0.0.1.dist-info → plinx-1.0.1.dist-info/licenses}/LICENSE +0 -0
- {Plinx-0.0.1.dist-info → plinx-1.0.1.dist-info}/top_level.txt +0 -0
plinx/orm/orm.py
ADDED
@@ -0,0 +1,550 @@
|
|
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
|
+
"""
|
12
|
+
SQLite database wrapper that provides a simple ORM interface.
|
13
|
+
|
14
|
+
The Database class is the main entry point for ORM operations in Plinx.
|
15
|
+
It handles database connections, table creation, and provides methods for
|
16
|
+
basic CRUD operations (Create, Read, Update, Delete) on Table objects.
|
17
|
+
|
18
|
+
This class uses SQLite as the underlying database engine and provides
|
19
|
+
a simplified interface that avoids writing raw SQL in most cases.
|
20
|
+
|
21
|
+
Examples:
|
22
|
+
Creating a database and defining models:
|
23
|
+
|
24
|
+
```python
|
25
|
+
from plinx.orm import Database, Table, Column, ForeignKey
|
26
|
+
|
27
|
+
db = Database("app.db")
|
28
|
+
|
29
|
+
class User(Table):
|
30
|
+
name = Column(str)
|
31
|
+
age = Column(int)
|
32
|
+
|
33
|
+
db.create(User)
|
34
|
+
|
35
|
+
# Create a new user
|
36
|
+
john = User(name="John Doe", age=30)
|
37
|
+
db.save(john)
|
38
|
+
|
39
|
+
# Query users
|
40
|
+
all_users = db.all(User)
|
41
|
+
john = db.get(User, id=1)
|
42
|
+
|
43
|
+
# Update a user
|
44
|
+
john.age = 31
|
45
|
+
db.update(john)
|
46
|
+
|
47
|
+
# Delete a user
|
48
|
+
db.delete(john)
|
49
|
+
```
|
50
|
+
"""
|
51
|
+
|
52
|
+
def __init__(self, path: str):
|
53
|
+
"""
|
54
|
+
Initialize a new database connection.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
path: Path to the SQLite database file. If the file doesn't exist,
|
58
|
+
it will be created.
|
59
|
+
"""
|
60
|
+
self.connection = sqlite3.Connection(path)
|
61
|
+
|
62
|
+
def create(self, table: "Table"):
|
63
|
+
"""
|
64
|
+
Create a database table based on a Table subclass definition.
|
65
|
+
|
66
|
+
This method creates a table in the database with columns corresponding
|
67
|
+
to the Column and ForeignKey attributes defined on the Table subclass.
|
68
|
+
If the table already exists, this method does nothing.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
table: A Table subclass with Column and/or ForeignKey attributes
|
72
|
+
|
73
|
+
Example:
|
74
|
+
```python
|
75
|
+
class User(Table):
|
76
|
+
name = Column(str)
|
77
|
+
age = Column(int)
|
78
|
+
|
79
|
+
db.create(User)
|
80
|
+
```
|
81
|
+
"""
|
82
|
+
self.connection.execute(table._get_create_sql())
|
83
|
+
|
84
|
+
def save(self, instance: "Table"):
|
85
|
+
"""
|
86
|
+
Save a Table instance to the database.
|
87
|
+
|
88
|
+
This method inserts a new row into the corresponding database table.
|
89
|
+
It automatically sets the instance's id attribute to the new row's ID.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
instance: A Table instance to save
|
93
|
+
|
94
|
+
Example:
|
95
|
+
```python
|
96
|
+
user = User(name="John Doe", age=30)
|
97
|
+
db.save(user) # user.id is now set to the new row's ID
|
98
|
+
```
|
99
|
+
"""
|
100
|
+
sql, values = instance._get_insert_sql()
|
101
|
+
cursor = self.connection.execute(sql, values)
|
102
|
+
instance._data["id"] = cursor.lastrowid
|
103
|
+
self.connection.commit()
|
104
|
+
|
105
|
+
def all(self, table: "Table"):
|
106
|
+
"""
|
107
|
+
Retrieve all rows from a table.
|
108
|
+
|
109
|
+
This method selects all rows from the table corresponding to the given
|
110
|
+
Table subclass. It returns a list of instances of that class, with
|
111
|
+
attributes set to the values from the database.
|
112
|
+
|
113
|
+
Args:
|
114
|
+
table: A Table subclass to query
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
List of Table instances, one for each row in the table
|
118
|
+
|
119
|
+
Example:
|
120
|
+
```python
|
121
|
+
all_users = db.all(User)
|
122
|
+
for user in all_users:
|
123
|
+
print(f"{user.name} is {user.age} years old")
|
124
|
+
```
|
125
|
+
"""
|
126
|
+
sql, fields = table._get_select_all_sql()
|
127
|
+
rows = self.connection.execute(sql).fetchall()
|
128
|
+
|
129
|
+
result = []
|
130
|
+
|
131
|
+
for row in rows:
|
132
|
+
properties = {}
|
133
|
+
for field, value in zip(fields, row):
|
134
|
+
if field.endswith("_id"):
|
135
|
+
foreign_key = field[:-3]
|
136
|
+
foreign_table = getattr(table, foreign_key).table
|
137
|
+
properties[foreign_key] = self.get(foreign_table, id=value)
|
138
|
+
else:
|
139
|
+
properties[field] = value
|
140
|
+
result.append(table(**properties))
|
141
|
+
|
142
|
+
return result
|
143
|
+
|
144
|
+
def get(self, table: "Table", **kwargs):
|
145
|
+
"""
|
146
|
+
Retrieve a single row from a table by specified criteria.
|
147
|
+
|
148
|
+
This method selects a row from the database where the specified columns
|
149
|
+
match the given values. It returns an instance of the Table subclass with
|
150
|
+
attributes set to the values from the database.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
table: A Table subclass to query
|
154
|
+
**kwargs: Column-value pairs to filter by
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
A Table instance corresponding to the matched row
|
158
|
+
|
159
|
+
Raises:
|
160
|
+
Exception: If no row matches the criteria
|
161
|
+
|
162
|
+
Example:
|
163
|
+
```python
|
164
|
+
# Get user by ID
|
165
|
+
user = db.get(User, id=1)
|
166
|
+
|
167
|
+
# Get user by name
|
168
|
+
user = db.get(User, name="John Doe")
|
169
|
+
```
|
170
|
+
"""
|
171
|
+
sql, fields, params = table._get_select_where_sql(**kwargs)
|
172
|
+
row = self.connection.execute(sql, params).fetchone()
|
173
|
+
|
174
|
+
if row is None:
|
175
|
+
raise Exception(f"{table.__name__} instance with {kwargs} does not exist")
|
176
|
+
|
177
|
+
properties = {}
|
178
|
+
|
179
|
+
for field, value in zip(fields, row):
|
180
|
+
if field.endswith("_id"):
|
181
|
+
foreign_key = field[:-3]
|
182
|
+
foreign_table = getattr(table, foreign_key).table
|
183
|
+
properties[foreign_key] = self.get(foreign_table, id=value)
|
184
|
+
else:
|
185
|
+
properties[field] = value
|
186
|
+
|
187
|
+
return table(**properties)
|
188
|
+
|
189
|
+
def update(self, instance: "Table"):
|
190
|
+
"""
|
191
|
+
Update an existing row in the database.
|
192
|
+
|
193
|
+
This method updates the row corresponding to the given instance with the
|
194
|
+
current values of the instance's attributes.
|
195
|
+
|
196
|
+
Args:
|
197
|
+
instance: A Table instance to update. Must have an id attribute.
|
198
|
+
|
199
|
+
Example:
|
200
|
+
```python
|
201
|
+
user = db.get(User, id=1)
|
202
|
+
user.name = "Jane Doe"
|
203
|
+
db.update(user)
|
204
|
+
```
|
205
|
+
"""
|
206
|
+
sql, values = instance._get_update_sql()
|
207
|
+
self.connection.execute(sql, values)
|
208
|
+
self.connection.commit()
|
209
|
+
|
210
|
+
def delete(self, instance: "Table"):
|
211
|
+
"""
|
212
|
+
Delete a row from the database.
|
213
|
+
|
214
|
+
This method deletes the row corresponding to the given instance.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
instance: A Table instance to delete. Must have an id attribute.
|
218
|
+
|
219
|
+
Example:
|
220
|
+
```python
|
221
|
+
user = db.get(User, id=1)
|
222
|
+
db.delete(user)
|
223
|
+
```
|
224
|
+
"""
|
225
|
+
sql, values = instance._get_delete_sql()
|
226
|
+
self.connection.execute(sql, values)
|
227
|
+
self.connection.commit()
|
228
|
+
|
229
|
+
def close(self):
|
230
|
+
"""
|
231
|
+
Close the database connection.
|
232
|
+
|
233
|
+
This method closes the SQLite connection when the database is no longer
|
234
|
+
needed. It's good practice to call this method when you're done using
|
235
|
+
the database, especially in longer-running applications.
|
236
|
+
"""
|
237
|
+
if self.connection:
|
238
|
+
self.connection.close()
|
239
|
+
self.connection = None
|
240
|
+
|
241
|
+
@property
|
242
|
+
def tables(self):
|
243
|
+
"""
|
244
|
+
Get a list of all tables in the database.
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
List of table names as strings
|
248
|
+
"""
|
249
|
+
SELECT_TABLES_SQL = "SELECT name FROM sqlite_master WHERE type = 'table';"
|
250
|
+
return [x[0] for x in self.connection.execute(SELECT_TABLES_SQL).fetchall()]
|
251
|
+
|
252
|
+
|
253
|
+
class Column:
|
254
|
+
"""
|
255
|
+
Define a column in a database table.
|
256
|
+
|
257
|
+
This class represents a column definition for a Table class. It stores
|
258
|
+
the column's type and can generate the corresponding SQL type.
|
259
|
+
|
260
|
+
Examples:
|
261
|
+
```python
|
262
|
+
class User(Table):
|
263
|
+
name = Column(str) # TEXT column
|
264
|
+
age = Column(int) # INTEGER column
|
265
|
+
active = Column(bool) # INTEGER column (0=False, 1=True)
|
266
|
+
```
|
267
|
+
"""
|
268
|
+
|
269
|
+
def __init__(self, type: Generic[T]):
|
270
|
+
"""
|
271
|
+
Initialize a new column.
|
272
|
+
|
273
|
+
Args:
|
274
|
+
type: Python type for the column (str, int, float, bool, bytes)
|
275
|
+
"""
|
276
|
+
self.type = type
|
277
|
+
|
278
|
+
@property
|
279
|
+
def sql_type(self):
|
280
|
+
"""
|
281
|
+
Get the SQL type corresponding to this column's Python type.
|
282
|
+
|
283
|
+
Returns:
|
284
|
+
SQL type string (e.g., "TEXT", "INTEGER", "REAL")
|
285
|
+
"""
|
286
|
+
return SQLITE_TYPE_MAP[self.type]
|
287
|
+
|
288
|
+
|
289
|
+
class ForeignKey:
|
290
|
+
"""
|
291
|
+
Define a foreign key relationship between tables.
|
292
|
+
|
293
|
+
This class represents a foreign key constraint in a database schema,
|
294
|
+
linking one Table class to another.
|
295
|
+
|
296
|
+
Examples:
|
297
|
+
```python
|
298
|
+
class Author(Table):
|
299
|
+
name = Column(str)
|
300
|
+
|
301
|
+
class Book(Table):
|
302
|
+
title = Column(str)
|
303
|
+
author = ForeignKey(Author) # Creates author_id column
|
304
|
+
```
|
305
|
+
"""
|
306
|
+
|
307
|
+
def __init__(self, table):
|
308
|
+
"""
|
309
|
+
Initialize a new foreign key.
|
310
|
+
|
311
|
+
Args:
|
312
|
+
table: The Table subclass that this foreign key references
|
313
|
+
"""
|
314
|
+
self.table = table
|
315
|
+
|
316
|
+
|
317
|
+
class Table:
|
318
|
+
"""
|
319
|
+
Base class for ORM models in Plinx.
|
320
|
+
|
321
|
+
This class is used as a base class for defining database tables.
|
322
|
+
Subclasses should define class attributes using Column and ForeignKey
|
323
|
+
to describe the table schema.
|
324
|
+
|
325
|
+
The Table class provides methods for generating SQL statements for
|
326
|
+
CRUD operations, which are used by the Database class.
|
327
|
+
|
328
|
+
Examples:
|
329
|
+
```python
|
330
|
+
class User(Table):
|
331
|
+
name = Column(str)
|
332
|
+
age = Column(int)
|
333
|
+
|
334
|
+
class Post(Table):
|
335
|
+
title = Column(str)
|
336
|
+
content = Column(str)
|
337
|
+
author = ForeignKey(User)
|
338
|
+
```
|
339
|
+
"""
|
340
|
+
|
341
|
+
def __init__(self, **kwargs):
|
342
|
+
"""
|
343
|
+
Initialize a new record.
|
344
|
+
|
345
|
+
Args:
|
346
|
+
**kwargs: Column values to initialize with
|
347
|
+
"""
|
348
|
+
self._data = {"id": None}
|
349
|
+
|
350
|
+
for key, value in kwargs.items():
|
351
|
+
self._data[key] = value
|
352
|
+
|
353
|
+
def __getattribute__(self, key):
|
354
|
+
"""
|
355
|
+
Custom attribute access for Table instances.
|
356
|
+
|
357
|
+
This method allows Table instances to access column values as attributes,
|
358
|
+
rather than accessing self._data directly.
|
359
|
+
|
360
|
+
Args:
|
361
|
+
key: Attribute name to access
|
362
|
+
|
363
|
+
Returns:
|
364
|
+
The attribute value
|
365
|
+
"""
|
366
|
+
# Why use super().__getattribute__ instead of self._data[key]?
|
367
|
+
# Because otherwise it will create an infinite loop since __getattribute__ will call itself
|
368
|
+
# and will never return the value
|
369
|
+
_data = super().__getattribute__("_data")
|
370
|
+
if key in _data:
|
371
|
+
return _data[key]
|
372
|
+
return super().__getattribute__(key)
|
373
|
+
|
374
|
+
def __setattr__(self, key, value):
|
375
|
+
"""
|
376
|
+
Custom attribute assignment for Table instances.
|
377
|
+
|
378
|
+
This method ensures that when setting an attribute that corresponds to
|
379
|
+
a column, the value is stored in self._data.
|
380
|
+
|
381
|
+
Args:
|
382
|
+
key: Attribute name to set
|
383
|
+
value: Value to assign
|
384
|
+
"""
|
385
|
+
super().__setattr__(key, value)
|
386
|
+
if key in self._data:
|
387
|
+
self._data[key] = value
|
388
|
+
|
389
|
+
@classmethod
|
390
|
+
def _get_create_sql(cls):
|
391
|
+
"""
|
392
|
+
Generate SQL for creating the table.
|
393
|
+
|
394
|
+
Returns:
|
395
|
+
SQL string for creating the table
|
396
|
+
"""
|
397
|
+
CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS {name} ({fields});"
|
398
|
+
fields = [
|
399
|
+
"id INTEGER PRIMARY KEY AUTOINCREMENT",
|
400
|
+
]
|
401
|
+
|
402
|
+
for name, field in inspect.getmembers(cls):
|
403
|
+
if isinstance(field, Column):
|
404
|
+
fields.append(f"{name} {field.sql_type}")
|
405
|
+
elif isinstance(field, ForeignKey):
|
406
|
+
fields.append(f"{name}_id INTEGER")
|
407
|
+
|
408
|
+
fields = ", ".join(fields)
|
409
|
+
name = cls.__name__.lower()
|
410
|
+
return CREATE_TABLE_SQL.format(name=name, fields=fields)
|
411
|
+
|
412
|
+
def _get_insert_sql(self):
|
413
|
+
"""
|
414
|
+
Generate SQL for inserting a record.
|
415
|
+
|
416
|
+
Returns:
|
417
|
+
Tuple of (SQL string, parameter values list)
|
418
|
+
"""
|
419
|
+
INSERT_SQL = "INSERT INTO {name} ({fields}) VALUES ({placeholders});"
|
420
|
+
|
421
|
+
cls = self.__class__
|
422
|
+
fields = []
|
423
|
+
placeholders = []
|
424
|
+
values = []
|
425
|
+
|
426
|
+
for name, field in inspect.getmembers(cls):
|
427
|
+
if isinstance(field, Column):
|
428
|
+
fields.append(name)
|
429
|
+
values.append(getattr(self, name))
|
430
|
+
placeholders.append("?")
|
431
|
+
elif isinstance(field, ForeignKey):
|
432
|
+
fields.append(name + "_id")
|
433
|
+
values.append(getattr(self, name).id)
|
434
|
+
placeholders.append("?")
|
435
|
+
|
436
|
+
fields = ", ".join(fields)
|
437
|
+
placeholders = ", ".join(placeholders)
|
438
|
+
|
439
|
+
sql = INSERT_SQL.format(
|
440
|
+
name=cls.__name__.lower(), fields=fields, placeholders=placeholders
|
441
|
+
)
|
442
|
+
|
443
|
+
return sql, values
|
444
|
+
|
445
|
+
@classmethod
|
446
|
+
def _get_select_all_sql(cls):
|
447
|
+
"""
|
448
|
+
Generate SQL for selecting all records.
|
449
|
+
|
450
|
+
Returns:
|
451
|
+
Tuple of (SQL string, field names list)
|
452
|
+
"""
|
453
|
+
SELECT_ALL_SQL = "SELECT {fields} FROM {name};"
|
454
|
+
|
455
|
+
fields = ["id"]
|
456
|
+
|
457
|
+
for name, field in inspect.getmembers(cls):
|
458
|
+
if isinstance(field, Column):
|
459
|
+
fields.append(name)
|
460
|
+
elif isinstance(field, ForeignKey):
|
461
|
+
fields.append(name + "_id")
|
462
|
+
|
463
|
+
return (
|
464
|
+
SELECT_ALL_SQL.format(
|
465
|
+
fields=", ".join(fields),
|
466
|
+
name=cls.__name__.lower(),
|
467
|
+
),
|
468
|
+
fields,
|
469
|
+
)
|
470
|
+
|
471
|
+
@classmethod
|
472
|
+
def _get_select_where_sql(cls, **kwargs):
|
473
|
+
"""
|
474
|
+
Generate SQL for selecting records by criteria.
|
475
|
+
|
476
|
+
Args:
|
477
|
+
**kwargs: Column-value pairs to filter by
|
478
|
+
|
479
|
+
Returns:
|
480
|
+
Tuple of (SQL string, field names list, parameter values list)
|
481
|
+
"""
|
482
|
+
SELECT_WHERE_SQL = "SELECT {fields} FROM {name} WHERE {query};"
|
483
|
+
|
484
|
+
fields = ["id"]
|
485
|
+
query = []
|
486
|
+
values = []
|
487
|
+
|
488
|
+
for name, field in inspect.getmembers(cls):
|
489
|
+
if isinstance(field, Column):
|
490
|
+
fields.append(name)
|
491
|
+
elif isinstance(field, ForeignKey):
|
492
|
+
fields.append(name + "_id")
|
493
|
+
|
494
|
+
for key, value in kwargs.items():
|
495
|
+
query.append(f"{key} = ?")
|
496
|
+
values.append(value)
|
497
|
+
|
498
|
+
return (
|
499
|
+
SELECT_WHERE_SQL.format(
|
500
|
+
fields=", ".join(fields),
|
501
|
+
name=cls.__name__.lower(),
|
502
|
+
query=", ".join(query),
|
503
|
+
),
|
504
|
+
fields,
|
505
|
+
values,
|
506
|
+
)
|
507
|
+
|
508
|
+
def _get_update_sql(self):
|
509
|
+
"""
|
510
|
+
Generate SQL for updating a record.
|
511
|
+
|
512
|
+
Returns:
|
513
|
+
Tuple of (SQL string, parameter values list)
|
514
|
+
"""
|
515
|
+
UPDATE_SQL = "UPDATE {name} SET {fields} WHERE id = ?;"
|
516
|
+
|
517
|
+
cls = self.__class__
|
518
|
+
fields = []
|
519
|
+
values = []
|
520
|
+
|
521
|
+
for name, field in inspect.getmembers(cls):
|
522
|
+
if isinstance(field, Column):
|
523
|
+
fields.append(name)
|
524
|
+
values.append(getattr(self, name))
|
525
|
+
elif isinstance(field, ForeignKey):
|
526
|
+
fields.append(name + "_id")
|
527
|
+
values.append(getattr(self, name).id)
|
528
|
+
|
529
|
+
values.append(getattr(self, "id"))
|
530
|
+
|
531
|
+
return (
|
532
|
+
UPDATE_SQL.format(
|
533
|
+
name=cls.__name__.lower(),
|
534
|
+
fields=", ".join([f"{field} = ?" for field in fields]),
|
535
|
+
),
|
536
|
+
values,
|
537
|
+
)
|
538
|
+
|
539
|
+
def _get_delete_sql(self):
|
540
|
+
"""
|
541
|
+
Generate SQL for deleting a record.
|
542
|
+
|
543
|
+
Returns:
|
544
|
+
Tuple of (SQL string, parameter values list)
|
545
|
+
"""
|
546
|
+
DELETE_SQL = "DELETE FROM {name} WHERE id = ?;"
|
547
|
+
|
548
|
+
return DELETE_SQL.format(name=self.__class__.__name__.lower()), [
|
549
|
+
getattr(self, "id")
|
550
|
+
]
|
plinx/orm/utils.py
ADDED
plinx/response.py
CHANGED
@@ -6,7 +6,57 @@ from webob import Response as WebObResponse
|
|
6
6
|
|
7
7
|
|
8
8
|
class PlinxResponse:
|
9
|
+
"""
|
10
|
+
Response class for the Plinx web framework.
|
11
|
+
|
12
|
+
This class provides a simple interface for constructing HTTP responses,
|
13
|
+
with high-level helpers for common response types like JSON and plain text.
|
14
|
+
It wraps WebOb's Response for actual WSGI compliance and output generation.
|
15
|
+
|
16
|
+
The class provides multiple ways to set response content:
|
17
|
+
|
18
|
+
1. Set the `text` attribute for plain text responses
|
19
|
+
2. Set the `json` attribute for JSON responses
|
20
|
+
3. Set the `body` attribute directly for binary data
|
21
|
+
|
22
|
+
It also allows setting status codes, content types, and custom headers.
|
23
|
+
|
24
|
+
Examples:
|
25
|
+
Plain text response:
|
26
|
+
```python
|
27
|
+
def handler(request, response):
|
28
|
+
response.text = "Hello, World!"
|
29
|
+
response.status_code = 200 # Optional, defaults to 200
|
30
|
+
```
|
31
|
+
|
32
|
+
JSON response:
|
33
|
+
```python
|
34
|
+
def handler(request, response):
|
35
|
+
response.json = {"message": "Hello, World!"}
|
36
|
+
# Content-Type will automatically be set to application/json
|
37
|
+
```
|
38
|
+
|
39
|
+
Custom headers:
|
40
|
+
```python
|
41
|
+
def handler(request, response):
|
42
|
+
response.text = "Not Found"
|
43
|
+
response.status_code = 404
|
44
|
+
response.headers["X-Custom-Header"] = "Value"
|
45
|
+
```
|
46
|
+
"""
|
47
|
+
|
9
48
|
def __init__(self):
|
49
|
+
"""
|
50
|
+
Initialize a new response object.
|
51
|
+
|
52
|
+
Sets up default values for the response attributes:
|
53
|
+
- json: None (will be serialized to JSON if set)
|
54
|
+
- text: None (will be encoded to UTF-8 if set)
|
55
|
+
- content_type: None (will be set based on response type)
|
56
|
+
- body: Empty bytes (raw response body)
|
57
|
+
- status_code: 200 (OK)
|
58
|
+
- headers: Empty dict (custom HTTP headers)
|
59
|
+
"""
|
10
60
|
self.json = None
|
11
61
|
self.text = None
|
12
62
|
self.content_type = None
|
@@ -20,10 +70,18 @@ class PlinxResponse:
|
|
20
70
|
start_response: StartResponse,
|
21
71
|
) -> Iterable[bytes]:
|
22
72
|
"""
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
73
|
+
WSGI callable interface for the response.
|
74
|
+
|
75
|
+
This makes the response object act as a WSGI application,
|
76
|
+
which is required for compatibility with WSGI servers.
|
77
|
+
It delegates the actual WSGI handling to WebOb's Response.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
environ: The WSGI environment dictionary
|
81
|
+
start_response: The WSGI start_response callable
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
An iterable of bytes representing the response body
|
27
85
|
"""
|
28
86
|
|
29
87
|
self.set_body_and_content_type()
|
@@ -37,12 +95,26 @@ class PlinxResponse:
|
|
37
95
|
return response(environ, start_response)
|
38
96
|
|
39
97
|
def set_body_and_content_type(self):
|
98
|
+
"""
|
99
|
+
Prepare the response body and content type based on the response attributes.
|
100
|
+
|
101
|
+
This method is called automatically before the response is returned.
|
102
|
+
It handles the conversion of high-level response attributes (`json`, `text`)
|
103
|
+
into the raw response body and appropriate content type.
|
104
|
+
|
105
|
+
The priority order is:
|
106
|
+
1. If `json` is set, encode it as JSON and set content_type to application/json
|
107
|
+
2. If `text` is set, encode it as UTF-8 and set content_type to text/plain
|
108
|
+
3. Otherwise, use the existing `body` and `content_type`
|
109
|
+
"""
|
40
110
|
if self.json is not None:
|
41
111
|
self.body = json.dumps(self.json).encode("UTF-8")
|
42
112
|
self.content_type = "application/json"
|
43
113
|
elif self.text is not None:
|
44
|
-
self.body =
|
114
|
+
self.body = (
|
115
|
+
self.text.encode("utf-8") if isinstance(self.text, str) else self.text
|
116
|
+
)
|
45
117
|
self.content_type = "text/plain"
|
46
118
|
|
47
119
|
if self.content_type is not None:
|
48
|
-
self.headers["Content-Type"] = self.content_type
|
120
|
+
self.headers["Content-Type"] = self.content_type
|