├── .gitignore
├── LICENSE
├── README.md
├── dori_orm
├── __init__.py
├── columns.py
├── db.py
└── operators.py
├── examples.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.db
2 | test.py
3 | __pycache__
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Mohammad Dori
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # My ORM
2 |
3 | It's a simple ORM built with Python and sqlite.
This is not a real ORM to use in your project.
4 |
5 | #
6 |
7 | # Use My ORM
8 |
9 | You can see sample codes in [examples.py](examples.py).
10 | I will be glad if you cooperate in the development of this project 😊
11 |
12 | #
13 |
14 | ## Install My ORM
15 |
16 | ```
17 | pip install dori-orm
18 | ```
19 |
20 | #
21 |
22 | ## Imports
23 |
24 | Import DB, ResultConfig, operators and columns from the _dori-orm_ library.
25 |
26 | **DB**: For create classes and database table.
27 | **ResultConfig**: Change the result, e.g. ordering and limit.
28 | **operators**: SQL operators like AND, OR, NOT
29 | **columns**: To create table columns with specific _data type_ and _constraints_. For example `age = columns.Integer(nullable=False)` means `age INTEGER NOT NULL`
30 |
31 | ```python
32 | from dori_orm import DB, ResultConfig
33 | from dori_orm.operators import AND, OR, NOT
34 | from dori_orm import columns
35 | ```
36 |
37 | #
38 |
39 | ## Create Table
40 |
41 | Create class and inheritance from `DB`, tables created automatically in database(set db name with file name) when you inheritance from DB, you can define class variable and use `columns` to create table column.
42 |
43 | ```python
44 | class Person(DB):
45 | name = columns.Text(nullable=False)
46 | family = columns.Text(nullable=False)
47 | age = columns.Integer()
48 | phone = columns.Integer()
49 | salary = columns.Real(default=100_000)
50 |
51 |
52 | class School(DB):
53 | name = columns.VarChar(nullable=False, unique=True)
54 | created_at = columns.Date()
55 | address = columns.Text()
56 | students_count = columns.SmallInt(default=300)
57 |
58 |
59 | class Student(DB):
60 | person = columns.ForeignKey(Person)
61 | school = columns.ForeignKey(School)
62 | class_name = columns.VarChar()
63 | ```
64 |
65 | #
66 |
67 | ## Insert Data
68 |
69 | Insert data to table with create instance from class. this code create two row in database with this arguments.
70 |
71 | ```python
72 | person1 = Person(
73 | name='Mohammad',
74 | family='Dori',
75 | age=20,
76 | phone=1234567890,
77 | salary=110_000,
78 | )
79 | print(person1.age) # 20
80 |
81 | person2 = Person(
82 | name='John',
83 | family='Gits',
84 | age=30,
85 | phone=1234567890,
86 | )
87 | ```
88 |
89 | #
90 |
91 | ## Select Data
92 |
93 | Select rows from table.
94 | `all` method select all rows from table.
95 | `get` method, select all rows with defined columns.
96 | `filter` method select all rows that match the given conditions.
97 |
98 | ```python
99 | print(Person.all())
100 | ```
101 |
102 | ```python
103 | # Result:
104 | [
105 | <'id':1, 'name':'Mohammad', 'family':'Dori', 'age':20, 'phone':1234567890, 'salary':110000.0>,
106 | <'id':2, 'name':'John', 'family':'Gits', 'age':30, 'phone':1234567890, 'salary':100000.0>
107 | ]
108 | ```
109 |
110 | ```python
111 | print(Person.get('id', 'name', 'family'))
112 | ```
113 |
114 | ```python
115 | # Result:
116 | [
117 | <'id':1, 'name':'Mohammad', 'family':'Dori'>,
118 | <'id':2, 'name':'John', 'family':'Gits'>
119 | ]
120 | ```
121 |
122 | ```python
123 | print(Person.filter(id=1, name='Mohammad'))
124 | ```
125 |
126 | ```python
127 | # Result:
128 | [
129 | <'id':1, 'name':'Mohammad', 'family':'Dori', 'age':20, 'phone':1234567890, 'salary':110000.0>
130 | ]
131 | ```
132 |
133 | #
134 |
135 | ## Advance Filtering
136 |
137 | You can use operators in filter method. like AND, OR, NOT, BETWEEN, LIKE, IN, =, !=, <, <=, >, >=.
138 |
139 | **AND**: e.g. `AND(x='', y=123)` that means `x='' AND y=123`
140 |
141 | ```python
142 | rows = Person.filter(
143 | AND(id=2, name='Ali')
144 | )
145 | ```
146 |
147 | **OR**: e.g. `OR(x='', y=123)` that means `x='' OR y=123`
148 |
149 | ```python
150 | rows = Person.filter(
151 | OR(id=2, name='Ali')
152 | )
153 | ```
154 |
155 | **NOT**: e.g. `NOT(OR(x='', y=123))` that means `NOT (x='' OR y=123)`
156 |
157 | ```python
158 | rows = Person.filter(
159 | NOT(AND(id=2, name='Ali'))
160 | )
161 | ```
162 |
163 | You can use another operator in operator.
164 |
165 | ```python
166 | rows = Person.filter(
167 | OR(OR(name='Ali', id=2), OR(salary=10, age=20))
168 | )
169 | ```
170 |
171 | **BETWEEN**: Return row, if it value between x and y.
172 |
173 | ```python
174 | print(Person.filter(id__between=(2, 8)))
175 | ```
176 |
177 | **LIKE**: Use pattern with % and \_ to filter rows.
178 |
179 | ```python
180 | print(Person.filter(
181 | name__like='Mo%',
182 | config=ResultConfig(
183 | limit=2,
184 | order_by='age',
185 | )
186 | ))
187 | ```
188 |
189 | **lt**: less than, means `<`
190 |
191 | ```python
192 | print(Person.filter(id__lt=5))
193 | ```
194 |
195 | **lte**: less than or equal, means `<=`
196 |
197 | ```python
198 | print(Person.filter(id__lte=5))
199 | ```
200 |
201 | **gt**: greater than, means `>`
202 |
203 | ```python
204 | print(Person.filter(id__gt=5))
205 | ```
206 |
207 | **gte**: greater than or equal, means `>=`
208 |
209 | ```python
210 | print(Person.filter(id__gte=5))
211 | ```
212 |
213 | **not**: not equal, means `!=`
214 |
215 | ```python
216 | print(Person.filter(id__n=5))
217 | ```
218 |
219 | You can use any filter together.
220 |
221 | ```python
222 | print(Person.filter(
223 | OR(
224 | id__n=5,
225 | name__in=('Mohammad', 'Salar'),
226 | age__gte=8
227 | )
228 | ))
229 | ```
230 |
231 | #
232 |
233 | ## Result Methods
234 |
235 | `result.count()` return count of results.
236 | `result.first()` return first row in result.
237 | `result.last()` return last row in result.
238 |
239 | ```python
240 | not_mohammad = Person.filter(name__n='Mohammad')
241 | print(not_mohammad.count())
242 | print(not_mohammad.first())
243 | print(not_mohammad.last())
244 | ```
245 |
246 | Iterate on result.
247 |
248 | ```python
249 | for row in not_mohammad:
250 | print(row.name)
251 | row.remove()
252 | # row.update(...)
253 | ```
254 |
255 | #
256 |
257 | ## Update Row
258 |
259 | ```python
260 | person1 = Person(
261 | name='Mohammad',
262 | family='Dori',
263 | age=20,
264 | phone=1234567890,
265 | salary=110_000,
266 | )
267 |
268 | print(person1)
269 | person1.update(name='Salar')
270 | print(person1)
271 | ```
272 |
273 | #
274 |
275 | ## Table Class Method
276 |
277 | **max**: Return maximum value of column.
278 |
279 | ```python
280 | print(Person.max('salary'))
281 | ```
282 |
283 | **min**: Return minimum value of column.
284 |
285 | ```python
286 | print(Person.min('salary'))
287 | ```
288 |
289 | **sum**: Return sum of column values.
290 |
291 | ```python
292 | print(Person.sum('salary'))
293 | ```
294 |
295 | **avg**: Return average of column values.
296 |
297 | ```python
298 | print(Person.avg('salary'))
299 | ```
300 |
301 | **count**: Return count of rows in table.
302 |
303 | ```python
304 | print(Person.count())
305 | ```
306 |
307 | **first**: Return first row of table.
308 |
309 | ```python
310 | print(Person.first())
311 | ```
312 |
313 | **last**: Return last row of table.
314 |
315 | ```python
316 | print(Person.last())
317 | ```
318 |
319 | #
320 |
321 | ## Result configuration
322 |
323 | `limit` Limit the number of result rows.
324 | `order_by` Order result by columns.
325 | `reverse` Use with order_by, False means sort ASC and True means sort DESC.
326 |
327 | ```python
328 | print(Person.all(
329 | config=ResultConfig(
330 | order_by='id',
331 | reverse=True
332 | )
333 | ))
334 | print(Person.get(
335 | config=ResultConfig(
336 | limit=5
337 | )
338 | ))
339 | ```
340 |
341 | #
342 |
343 | ## Foreign Key
344 |
345 | ```python
346 | person1 = Person(
347 | name='Mohammad',
348 | family='Dori',
349 | age=20,
350 | phone=1234567890,
351 | salary=110_000,
352 | )
353 | school1 = School(
354 | name='The Sample School',
355 | created_at='2002-01-04',
356 | address='1600 Amphitheatre Parkway in Mountain View, California',
357 | )
358 |
359 | print(school1)
360 |
361 | student = Student(
362 | person=person1,
363 | school=school1,
364 | class_name='A3',
365 | )
366 |
367 | print(school1.id)
368 | print(person1.id)
369 |
370 | print(student)
371 | ```
372 |
373 | #
374 |
375 | ## Change Easy
376 |
377 | Remove `class_name` column and add gpa column. now add a row to table.
378 |
379 | ```python
380 | class Student(DB):
381 | person = columns.ForeignKey(Person)
382 | school = columns.ForeignKey(School)
383 | gpa = columns.TinyInt(default=20)
384 |
385 | person1 = Person(
386 | name='Mohammad',
387 | family='Dori',
388 | age=20,
389 | phone=1234567890,
390 | salary=110_000,
391 | )
392 | school1 = School(
393 | name='The Sample School',
394 | created_at='2002-01-04',
395 | address='1600 Amphitheatre Parkway in Mountain View, California',
396 | )
397 |
398 | print(school1)
399 |
400 | student = Student(
401 | person=person1,
402 | school=school1,
403 | gpa=10,
404 | )
405 |
406 | print(school1.id)
407 | print(person1.id)
408 |
409 | print(student)
410 | ```
411 |
412 | #
413 |
414 | ## See All Query Usage
415 |
416 | ```python
417 | print(Person.queries())
418 | ```
419 |
420 | #
421 |
422 | # Links
423 |
424 | Download Source Code: [Click Here](https://github.com/dori-dev/my-orm/archive/refs/heads/main.zip)
425 |
426 | My Github Account: [Click Here](https://github.com/dori-dev/)
427 |
--------------------------------------------------------------------------------
/dori_orm/__init__.py:
--------------------------------------------------------------------------------
1 | from .db import DB, ResultConfig
2 | from . import columns
3 | from . import operators
4 |
--------------------------------------------------------------------------------
/dori_orm/columns.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 |
3 |
4 | class Column:
5 | def __init__(self, unique: bool = False,
6 | nullable: bool = True, default: str = None) -> None:
7 | self.type = None
8 | self.unique = unique
9 | self.nullable = nullable
10 | self.default = default
11 |
12 | def __repr__(self):
13 | constraints = ''
14 | if self.nullable:
15 | if self.default is not None:
16 | constraints += f'DEFAULT {self.default}'
17 | else:
18 | constraints += 'NOT NULL'
19 | if self.unique:
20 | constraints += ' UNIQUE'
21 | return f'{self.type} {constraints.strip()}'
22 |
23 |
24 | class Int(Column):
25 | def __init__(self, unique: bool = False,
26 | nullable: bool = True, default: str = None) -> None:
27 | super().__init__(unique, nullable, default)
28 | self.type = 'INT'
29 |
30 |
31 | class Integer(Column):
32 | def __init__(self, unique: bool = False,
33 | nullable: bool = True, default: str = None) -> None:
34 | super().__init__(unique, nullable, default)
35 | self.type = 'INTEGER'
36 |
37 |
38 | class TinyInt(Column):
39 | def __init__(self, unique: bool = False,
40 | nullable: bool = True, default: str = None) -> None:
41 | super().__init__(unique, nullable, default)
42 | self.type = 'TINYINT'
43 |
44 |
45 | class SmallInt(Column):
46 | def __init__(self, unique: bool = False,
47 | nullable: bool = True, default: str = None) -> None:
48 | super().__init__(unique, nullable, default)
49 | self.type = 'SMALLINT'
50 |
51 |
52 | class MediumInt(Column):
53 | def __init__(self, unique: bool = False,
54 | nullable: bool = True, default: str = None) -> None:
55 | super().__init__(unique, nullable, default)
56 | self.type = 'MEDIUMINT'
57 |
58 |
59 | class Text(Column):
60 | def __init__(self, unique: bool = False,
61 | nullable: bool = True, default: str = None) -> None:
62 | super().__init__(unique, nullable, default)
63 | self.type = 'TEXT'
64 |
65 |
66 | class VarChar(Column):
67 | def __init__(self, unique: bool = False,
68 | nullable: bool = True, default: str = None) -> None:
69 | super().__init__(unique, nullable, default)
70 | self.type = 'VARCHAR(255)'
71 |
72 |
73 | class Blob(Column):
74 | def __init__(self, unique: bool = False,
75 | nullable: bool = True, default: str = None) -> None:
76 | super().__init__(unique, nullable, default)
77 | self.type = 'BLOB'
78 |
79 |
80 | class Real(Column):
81 | def __init__(self, unique: bool = False,
82 | nullable: bool = True, default: str = None) -> None:
83 | super().__init__(unique, nullable, default)
84 | self.type = 'REAL'
85 |
86 |
87 | class Double(Column):
88 | def __init__(self, unique: bool = False,
89 | nullable: bool = True, default: str = None) -> None:
90 | super().__init__(unique, nullable, default)
91 | self.type = 'DOUBLE'
92 |
93 |
94 | class Float(Column):
95 | def __init__(self, unique: bool = False,
96 | nullable: bool = True, default: str = None) -> None:
97 | super().__init__(unique, nullable, default)
98 | self.type = 'FLOAT'
99 |
100 |
101 | class Numeric(Column):
102 | def __init__(self, unique: bool = False,
103 | nullable: bool = True, default: str = None) -> None:
104 | super().__init__(unique, nullable, default)
105 | self.type = 'NUMERIC'
106 |
107 |
108 | class Decimal(Column):
109 | def __init__(self, unique: bool = False,
110 | nullable: bool = True, default: str = None) -> None:
111 | super().__init__(unique, nullable, default)
112 | self.type = 'DECIMAL(10,5)'
113 |
114 |
115 | class Boolean(Column):
116 | def __init__(self, unique: bool = False,
117 | nullable: bool = True, default: str = None) -> None:
118 | super().__init__(unique, nullable, default)
119 | self.type = 'BOOLEAN'
120 |
121 |
122 | class Date(Column):
123 | def __init__(self, unique: bool = False,
124 | nullable: bool = True, default: str = None) -> None:
125 | super().__init__(unique, nullable, default)
126 | self.type = 'DATE'
127 |
128 |
129 | class DateTime(Column):
130 | def __init__(self, unique: bool = False,
131 | nullable: bool = True, default: str = None) -> None:
132 | super().__init__(unique, nullable, default)
133 | self.type = 'DATETIME'
134 |
135 |
136 | class ForeignKey(Column):
137 | def __set_name__(self, owner, name):
138 | self.column_name_ = name
139 |
140 | def __init__(self, reference: Union[object, str], unique: bool = False,
141 | nullable: bool = True, default: str = None) -> None:
142 | super().__init__(unique, nullable, default)
143 | self.type = 'INTEGER'
144 | if isinstance(reference, object):
145 | reference = reference.__name__
146 | self.reference = reference.lower()
147 |
148 | def get_foreign_key(self):
149 | name = self.column_name_
150 | return f'FOREIGN KEY ({name}) REFERENCES {self.reference} (id)'
151 |
--------------------------------------------------------------------------------
/dori_orm/db.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | import os
3 | import sqlite3
4 | import inspect
5 | from typing import Dict, List, NamedTuple, Tuple, Union
6 | from dori_orm.operators import OPERATORS
7 | from dori_orm.columns import ForeignKey
8 |
9 |
10 | class GenerateTableName:
11 | def __get__(self, instance, owner) -> str:
12 | return owner.__name__.lower()
13 |
14 |
15 | class GenerateDBName:
16 | def __get__(self, instance, owner) -> str:
17 | file_address = inspect.getfile(owner)
18 | db_name = os.path.basename(file_address)[:-3]
19 | return f"{db_name}.db"
20 |
21 |
22 | class GetColumns:
23 | def get_columns(self, owner) -> Tuple[str, str]:
24 | class_variables: Dict[str, str] = owner.__dict__
25 | return (
26 | (key, value)
27 | for key, value in class_variables.items()
28 | # remove python magic method from class variables
29 | if not key.startswith('_')
30 | )
31 |
32 | def __get__(self, instance, owner) -> Dict[str, str]:
33 | columns = {
34 | name.lower(): f"{name.lower()} {value}"
35 | for name, value in self.get_columns(owner)
36 | }
37 | return {
38 | 'id': 'id INTEGER PRIMARY KEY UNIQUE NOT NULL',
39 | **columns,
40 | }
41 |
42 |
43 | class GetForeignKeys:
44 | def get_foreign_key_columns(self, owner) -> Tuple[str, str]:
45 | class_variables: Dict[str, str] = owner.__dict__
46 | return (
47 | (key, value.get_foreign_key())
48 | for key, value in class_variables.items()
49 | if isinstance(value, ForeignKey)
50 | )
51 |
52 | def __get__(self, instance, owner) -> Dict[str, str]:
53 | return {
54 | key: value
55 | for key, value in self.get_foreign_key_columns(owner)
56 | }
57 |
58 |
59 | class Row:
60 | def __init__(self, db_name_: str, table_name_: str, **data):
61 | self.data = data
62 | for key, value in data.items():
63 | self.__setattr__(key, value)
64 | Row.db_name = db_name_
65 | Row.table_name = table_name_
66 |
67 | def remove(self):
68 | where = ' AND '.join([
69 | f'{key} = {repr(value)}'
70 | for key, value in self.data.items()
71 | ])
72 | query = f'DELETE FROM {self.table_name} WHERE {where}'
73 | self._execute(query)
74 |
75 | def update(self, **kwargs):
76 | where = ' AND '.join([
77 | f'{key} = {repr(value)}'
78 | for key, value in self.data.items()
79 | ])
80 | new_data = ', '.join([
81 | f'{key} = {repr(value)}'
82 | for key, value in kwargs.items()
83 | ])
84 | if new_data.strip():
85 | query = (
86 | f'UPDATE {self.table_name} SET {new_data} WHERE {where}'
87 | )
88 | self._execute(query)
89 |
90 | @ classmethod
91 | def _execute(cls, query: str):
92 | conn = sqlite3.connect(cls.db_name)
93 | conn.execute(query)
94 | conn.commit()
95 | conn.close()
96 |
97 | def __repr__(self) -> str:
98 | result = ', '.join([
99 | f'{repr(attr)}:{repr(value)}'
100 | for attr, value in self.data.items()
101 | ])
102 | return f"<{result}>"
103 |
104 |
105 | class Rows:
106 | def __init__(self, rows: List[Row]):
107 | self.rows = rows
108 |
109 | def count(self) -> int:
110 | return len(self.rows)
111 |
112 | def first(self) -> Union[Row, None]:
113 | if self.rows:
114 | return self.rows[0]
115 | return None
116 |
117 | def last(self) -> Union[Row, None]:
118 | if self.rows:
119 | return self.rows[-1]
120 | return None
121 |
122 | def __repr__(self) -> str:
123 | return repr(self.rows)
124 |
125 | def __iter__(self) -> List[Row]:
126 | return iter(self.rows)
127 |
128 |
129 | class ResultConfig(NamedTuple):
130 | limit: Union[int, None] = None
131 | order_by: Union[str, None] = None
132 | reverse: bool = False
133 |
134 |
135 | class DB:
136 | db_name = GenerateDBName()
137 | table_name = GenerateTableName()
138 | columns = GetColumns()
139 | foreign_keys = GetForeignKeys()
140 | _query = ''
141 |
142 | def __init__(self, **data):
143 | data = {
144 | 'id': self._get_max_id() + 1,
145 | **data,
146 | }
147 | for key, value in data.items():
148 | if key in self.foreign_keys.keys():
149 | data[key] = value.id
150 | self.data = data
151 | for key, value in data.items():
152 | self.__setattr__(key, value)
153 | self.id = self.id # just for type hinting
154 | self.insert(**data)
155 |
156 | def __init_subclass__(cls, **kwargs):
157 | cls._manage_table()
158 |
159 | @classmethod
160 | def insert(cls, **data: dict):
161 | fields = ', '.join(data.keys())
162 | values = ', '.join(
163 | map(repr, data.values())
164 | )
165 | query = (f'INSERT OR IGNORE INTO {cls.table_name} '
166 | f'({fields}) VALUES ({values});')
167 | cls._execute(query)
168 |
169 | @ classmethod
170 | def all(cls, config: Union[ResultConfig, None] = None) -> List[Row]:
171 | configs = cls._set_config(config)
172 | query = f'SELECT * FROM {cls.table_name}{configs};'
173 | return cls._fetchall(query)
174 |
175 | @ classmethod
176 | def get(cls, *fields: dict,
177 | config: Union[ResultConfig, None] = None) -> List[Row]:
178 | fields = [
179 | field
180 | for field in fields
181 | if field in cls.columns
182 | ]
183 | fields_string = ', '.join(fields) or '*'
184 | configs = cls._set_config(config)
185 | query = f'SELECT {fields_string} FROM {cls.table_name}{configs};'
186 | return cls._fetchall(query)
187 |
188 | @ classmethod
189 | def filter(cls, *args, config: Union[ResultConfig, None] = None,
190 | **kwargs) -> List[Row]:
191 | conditions = []
192 | for key, value in kwargs.items():
193 | if '__' in key:
194 | condition = cls._set_operator_filter(key, value)
195 | else:
196 | condition = f'{key} = {repr(value)}'
197 | if condition is not None:
198 | conditions.append(condition)
199 | condition = None
200 | conditions.extend(list(map(repr, args)))
201 | statements = ' AND '.join(conditions) or 'true'
202 | configs = cls._set_config(config)
203 | query = (
204 | f'SELECT * FROM {cls.table_name} WHERE {statements}{configs};'
205 | )
206 | return cls._fetchall(query)
207 |
208 | @ classmethod
209 | def max(cls, column_name: str):
210 | query = f'SELECT MAX({column_name}) FROM {cls.table_name};'
211 | result = cls._fetch_result(query)
212 | if result:
213 | return {column_name: result[0]}
214 |
215 | @ classmethod
216 | def min(cls, column_name: str):
217 | query = f'SELECT MIN({column_name}) FROM {cls.table_name};'
218 | result = cls._fetch_result(query)
219 | if result:
220 | return {column_name: result[0]}
221 |
222 | @ classmethod
223 | def avg(cls, column_name: str):
224 | query = f'SELECT AVG({column_name}) FROM {cls.table_name};'
225 | result = cls._fetch_result(query)
226 | if result:
227 | return {column_name: result[0]}
228 |
229 | @ classmethod
230 | def sum(cls, column_name: str):
231 | query = f'SELECT SUM({column_name}) FROM {cls.table_name};'
232 | result = cls._fetch_result(query)
233 | if result:
234 | return {column_name: result[0]}
235 |
236 | @ classmethod
237 | def count(cls):
238 | query = f'SELECT COUNT(1) FROM {cls.table_name};'
239 | result = cls._fetch_result(query)
240 | if result:
241 | return {'count': result[0]}
242 |
243 | @classmethod
244 | def first(cls) -> DB:
245 | query = f'SELECT * FROM {cls.table_name} WHERE id=1;'
246 | result = cls._fetch_result(query)
247 | if result is None:
248 | return None
249 | row = dict(zip(cls.columns.keys(), result))
250 | return Row(cls.db_name, cls.table_name, **row)
251 |
252 | @classmethod
253 | def last(cls) -> DB:
254 | max_id = cls._get_max_id()
255 | query = f'SELECT * FROM {cls.table_name} WHERE id={max_id};'
256 | result = cls._fetch_result(query)
257 | if result is None:
258 | return None
259 | row = dict(zip(cls.columns.keys(), result))
260 | return Row(cls.db_name, cls.table_name, **row)
261 |
262 | def remove(self):
263 | where = ' AND '.join([
264 | f'{key} = {repr(value)}'
265 | for key, value in self.data.items()
266 | ])
267 | query = f'DELETE FROM {self.table_name} WHERE {where};'
268 | self._execute(query)
269 |
270 | def update(self, **kwargs):
271 | where = ' AND '.join([
272 | f'{key} = {repr(value)}'
273 | for key, value in self.data.items()
274 | ])
275 | new_data = ', '.join([
276 | f'{key} = {repr(value)}'
277 | for key, value in kwargs.items()
278 | ])
279 | if new_data.strip():
280 | query = (
281 | f'UPDATE {self.table_name} SET {new_data} WHERE {where};'
282 | )
283 | self._execute(query)
284 | query = f'SELECT * FROM {self.table_name} WHERE id={self.id};'
285 | result = self._fetch_result(query)
286 | data = dict(zip(self.columns.keys(), result))
287 | self.data = data
288 | self.__dict__.update(data)
289 |
290 | @ classmethod
291 | def remove_table(cls):
292 | query = f'DROP TABLE {cls.table_name}'
293 | cls._execute(query)
294 |
295 | @ classmethod
296 | def queries(cls):
297 | return cls._query.strip()
298 |
299 | @ classmethod
300 | def _manage_table(cls) -> str:
301 | try:
302 | current_columns = cls._get_current_table_columns()
303 | if current_columns != list(cls.columns.keys()):
304 | cls._alter_columns(current_columns)
305 | cls._drop_columns(current_columns)
306 | except sqlite3.OperationalError:
307 | cls._create_table()
308 |
309 | @ classmethod
310 | def _create_table(cls) -> str:
311 | columns = list(cls.columns.values()).copy()
312 | foreign_keys = ', '.join(cls.foreign_keys.values())
313 | if foreign_keys:
314 | columns.append(foreign_keys)
315 | string_columns = ', '.join(columns)
316 | query = (f'CREATE TABLE IF NOT EXISTS {cls.table_name}'
317 | f'({string_columns});')
318 | cls._execute(query)
319 |
320 | @classmethod
321 | def _alter_columns(cls, current_columns):
322 | new_columns = [
323 | value
324 | for column, value in cls.columns.items()
325 | if column not in current_columns
326 | ]
327 | for field in new_columns:
328 | query = f'ALTER TABLE {cls.table_name} ADD {field};'
329 | cls._execute(query)
330 |
331 | @classmethod
332 | def _drop_columns(cls, current_columns):
333 | removed_columns = [
334 | column
335 | for column in current_columns
336 | if column not in cls.columns.keys()
337 | ]
338 | for field in removed_columns:
339 | query = f'ALTER TABLE {cls.table_name} DROP {field};'
340 | cls._execute(query)
341 |
342 | @ staticmethod
343 | def _set_config(config: Union[ResultConfig, None]) -> str:
344 | if config is None:
345 | return ''
346 | limit, order_by, reverse = config
347 | if limit is None:
348 | limit = ''
349 | else:
350 | limit = f' LIMIT {limit}'
351 | if order_by is None:
352 | order_by = ''
353 | sorting = ''
354 | else:
355 | order_by = F'ORDER BY {order_by}'
356 | if reverse is False:
357 | sorting = ' ASC'
358 | else:
359 | sorting = ' DESC'
360 | return f' {order_by}{sorting}{limit}'
361 |
362 | @ staticmethod
363 | def _set_operator_filter(key: str, value: str):
364 | filter = key.split('__')
365 | if len(filter) != 2:
366 | return None
367 | key, operator = filter
368 | key, operator = key.lower(), operator.lower()
369 | if operator in OPERATORS:
370 | return (
371 | f'{key} {OPERATORS[operator]} {repr(value)}'
372 | )
373 | elif operator == 'between':
374 | start, *_, end = value
375 | return (
376 | f'{key} BETWEEN {repr(start)} AND {repr(end)}'
377 | )
378 | else:
379 | return None
380 |
381 | @ classmethod
382 | def _get_max_id(cls):
383 | query = f'SELECT MAX(id) FROM {cls.table_name}'
384 | result = cls._fetch_result(query)
385 | return result[0] or 0
386 |
387 | @ classmethod
388 | def _execute(cls, query: str):
389 | cls._query += f"{query}\n\n"
390 | conn = sqlite3.connect(cls.db_name)
391 | conn.execute(query)
392 | conn.commit()
393 | conn.close()
394 |
395 | @ classmethod
396 | def _fetchall(cls, query: str) -> Rows:
397 | cls._query += f"{query}\n\n"
398 | conn = sqlite3.connect(cls.db_name)
399 | cur = conn.cursor()
400 | cur.execute(query)
401 | rows = cur.fetchall()
402 | result = []
403 | for row in rows:
404 | row = dict(zip(
405 | cls.columns.keys(),
406 | row
407 | ))
408 | result.append(
409 | Row(cls.db_name, cls.table_name, **row)
410 | )
411 | conn.close()
412 | return Rows(result)
413 |
414 | @ classmethod
415 | def _fetch_result(cls, query: str):
416 | cls._query += f"{query}\n\n"
417 | conn = sqlite3.connect(cls.db_name)
418 | cur = conn.cursor()
419 | cur.execute(query)
420 | result = cur.fetchone()
421 | conn.close()
422 | return result
423 |
424 | @classmethod
425 | def _get_current_table_columns(cls):
426 | query = f'SELECT * FROM {cls.table_name}'
427 | conn = sqlite3.connect(cls.db_name)
428 | cur = conn.cursor()
429 | cur.execute(query)
430 | columns = [description[0] for description in cur.description]
431 | conn.close()
432 | return columns
433 |
434 | def __repr__(self) -> str:
435 | result = ', '.join([
436 | f'{repr(attr)}:{repr(value)}'
437 | for attr, value in self.data.items()
438 | ])
439 | return f"<{result}>"
440 |
--------------------------------------------------------------------------------
/dori_orm/operators.py:
--------------------------------------------------------------------------------
1 | OPERATORS = {
2 | 'lt': '<',
3 | 'lte': '<=',
4 | 'gt': '>',
5 | 'gte': '>=',
6 | 'n': '!=',
7 | 'in': 'IN',
8 | 'like': 'LIKE',
9 | }
10 |
11 |
12 | class Operator:
13 | def __init__(self, *args, **kwargs):
14 | self.fields = kwargs
15 | self.args = args
16 | self.operator = self.__class__.__name__
17 |
18 | def generate_statements(self):
19 | operator = f' {self.operator} '
20 | statements = [
21 | f'{key}={repr(value)}'
22 | for key, value in self.fields.items()
23 | ]
24 | args_statements = operator.join(
25 | map(str, self.args))
26 | if args_statements:
27 | statements.append(args_statements)
28 | return f"({operator.join(statements)})"
29 |
30 | def __repr__(self) -> str:
31 | return self.generate_statements()
32 |
33 |
34 | class AND(Operator):
35 | pass
36 |
37 |
38 | class OR(Operator):
39 | pass
40 |
41 |
42 | class NOT(Operator):
43 | def __init__(self, *args, **kwargs):
44 | super().__init__(*args, **kwargs)
45 | self.operator = 'AND'
46 |
47 | def __repr__(self) -> str:
48 | return f"NOT {super().__repr__()}"
49 |
--------------------------------------------------------------------------------
/examples.py:
--------------------------------------------------------------------------------
1 | # pip install dori-orm
2 | from dori_orm import DB, ResultConfig
3 | from dori_orm.operators import AND, OR, NOT
4 | from dori_orm import columns
5 |
6 |
7 | class Person(DB):
8 | name = columns.Text(nullable=False)
9 | family = columns.Text(nullable=False)
10 | age = columns.Integer()
11 | phone = columns.Integer()
12 | salary = columns.Real(default=100_000)
13 |
14 |
15 | class School(DB):
16 | name = columns.VarChar(nullable=False)
17 | created_at = columns.Date()
18 | address = columns.Text()
19 | students_count = columns.SmallInt(default=300)
20 |
21 |
22 | class Student(DB):
23 | person = columns.ForeignKey(Person)
24 | school = columns.ForeignKey(School)
25 | # class_name = columns.VarChar()
26 | gpa = columns.TinyInt(default=20)
27 |
28 |
29 | p1 = Person(
30 | name='Mohammad',
31 | family='Dori',
32 | age=20,
33 | phone=1234567890,
34 | salary=110_000,
35 | )
36 |
37 | p2 = Person(
38 | name='John',
39 | family='Gits',
40 | age=30,
41 | phone=1234567890,
42 | )
43 |
44 | print(p1.age)
45 |
46 | print(Person.all())
47 |
48 | print(Person.get('id', 'name', 'family'))
49 |
50 | print(Person.filter(id=1, name='Mohammad'))
51 |
52 | not_mohammad = Person.filter(name__n='Mohammad')
53 | print(not_mohammad)
54 | print(not_mohammad.count())
55 | print(not_mohammad.first())
56 | print(not_mohammad.last())
57 | for row in not_mohammad:
58 | print(row.name)
59 | # row.remove()
60 |
61 | print(p1)
62 | p1.update(name='Salar')
63 | print(p1)
64 |
65 | print(Person.count())
66 |
67 | print(Person.first())
68 |
69 | print(Person.last())
70 |
71 | print(Person.max('salary'))
72 | print(Person.min('salary'))
73 | print(Person.sum('salary'))
74 | print(Person.avg('salary'))
75 |
76 | print(Person.all(config=ResultConfig(order_by='id', reverse=True)))
77 | print(Person.get(
78 | config=ResultConfig(
79 | limit=5
80 | )
81 | ))
82 |
83 | # id__lt, id__lte, id__gt, id__gte, id__n
84 | print(Person.filter(id__lte=5))
85 | print(Person.filter(id__between=(2, 8)))
86 | print(Person.filter(
87 | name__like='Mo%',
88 | config=ResultConfig(
89 | limit=2,
90 | order_by='age',
91 | )
92 | ))
93 |
94 |
95 | rows = Person.filter(
96 | OR(id=2, name='Ali')
97 | )
98 | print()
99 | print(rows)
100 |
101 | rows = Person.filter(
102 | NOT(AND(id=2, name='Ali'))
103 | )
104 | print()
105 | print(rows.count())
106 |
107 | rows = Person.filter(
108 | OR(OR(name='Ali', id=2), OR(salary=10, age=20))
109 | )
110 | print()
111 | print(rows.count())
112 |
113 |
114 | print(Person.queries())
115 |
116 |
117 | school1 = School(
118 | name='The Sample School',
119 | created_at='2002-01-04',
120 | address='1600 Amphitheatre Parkway in Mountain View, California',
121 | )
122 |
123 | print(school1)
124 |
125 | student = Student(
126 | person=p1,
127 | school=school1,
128 | # class_name='A3',
129 | gpa=10,
130 | )
131 |
132 | print(school1.id)
133 | print(p1.id)
134 |
135 | print(student)
136 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """
2 | python3 setup.py sdist bdist_wheel
3 | python -m twine upload dist/*
4 | """
5 | import setuptools
6 |
7 | with open("README.md", "r", encoding="utf-8") as fh:
8 | long_description = fh.read()
9 |
10 | setuptools.setup(
11 | name="dori-orm",
12 | version="4.6.1",
13 | author="Mohammad Dori",
14 | author_email="mr.dori.dev@gmail.com",
15 | description="simple orm, to manage your database.",
16 | long_description=long_description,
17 | long_description_content_type="text/markdown",
18 | url="https://github.com/dori-dev/my-orm",
19 | classifiers=[
20 | "Programming Language :: Python :: 3",
21 | "License :: OSI Approved :: MIT License",
22 | "Operating System :: OS Independent",
23 | ],
24 | packages=setuptools.find_packages(),
25 | install_requires=[],
26 | python_requires=">=3.6",
27 | )
28 |
--------------------------------------------------------------------------------