├── .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 | --------------------------------------------------------------------------------