Skip to content

Commit adb094b

Browse files
committed
Merge branch 'dataclass-use'
# Conflicts: # pythonicMySQL/__init__.py # pythonicMySQL/mysqlclient/response.py
2 parents 42bfe06 + 77cd331 commit adb094b

15 files changed

Lines changed: 649 additions & 0 deletions

File tree

pythonicMySQL/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from pythonicMySQL.mysqlclient import CLIENT
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from pythonicMySQL.constructor.table import Table
2+
from pythonicMySQL.constructor.columnflags import *
3+
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from __future__ import annotations
2+
from typing import overload, Union, List, Optional
3+
from dataclasses import dataclass, is_dataclass, field, fields
4+
from pythonicMySQL.datatypes.mysqltypes import MySQLType, INT
5+
6+
7+
ID_COLUMN = field(default=None, init=True, metadata={"name": "id", "mysql_type": INT(11), "unsigned": True,
8+
"primary": True, "auto_increment": True})
9+
10+
11+
@dataclass(frozen=True)
12+
class Column:
13+
14+
name: str
15+
mysql_type: MySQLType
16+
default: type = None
17+
unsigned: bool = False
18+
primary: bool = False
19+
null: bool = False
20+
unique: bool = False
21+
auto_increment: bool = False
22+
23+
@property
24+
def description(self) -> str:
25+
query_str = f"`{self.name}` {self.mysql_type.description}"
26+
if self.unsigned: query_str += " unsigned"
27+
if not self.null: query_str += " NOT NULL"
28+
if self.default is not None: query_str += f" Default {self.default}"
29+
if self.auto_increment: query_str += " AUTO_INCREMENT"
30+
if self.unique:
31+
query_str += " UNIQUE"
32+
if self.primary:
33+
query_str += " PRIMARY KEY"
34+
return query_str
35+
36+
37+
def column(mysql_type: Union[MySQLType, MySQLType.__class__], *flags: dict, default=None, unsigned: bool = False,
38+
null: bool = False, unique: bool = False):
39+
if isinstance(mysql_type, type):
40+
mysql_type = mysql_type()
41+
metadata = {
42+
"mysql_type": mysql_type,
43+
"default": default,
44+
"unsigned": unsigned,
45+
"null": null,
46+
"unique": unique
47+
}
48+
for flag in flags:
49+
metadata = {**metadata, **flag}
50+
if default is not None:
51+
return field(default=default, metadata=metadata)
52+
else:
53+
return field(metadata=metadata)
54+
55+
56+
@overload
57+
def columns(mysql_object: type, attribute: str) -> Optional[Column]: ...
58+
59+
60+
@overload
61+
def columns(mysql_object: type) -> List[Column]: ...
62+
63+
64+
def columns(mysql_object: type, attribute: Optional[str] = None) -> Union[List[Column], Column, None]:
65+
if not is_dataclass(mysql_object):
66+
raise ValueError("MySQL objects need to be a dataclass")
67+
columns_ = []
68+
if attribute is None:
69+
fields_ = fields(mysql_object)
70+
else:
71+
fields_ = [item for item in fields(mysql_object) if item.name == attribute]
72+
for field_ in fields_:
73+
if "mysql_type" in dict(field_.metadata).keys():
74+
metadata = {**{"name": field_.name}, **field_.metadata}
75+
columns_.append(Column(**metadata))
76+
if attribute is not None and len(columns_) == 1:
77+
return columns_[0]
78+
elif attribute is not None and len(columns_) == 0:
79+
return None
80+
elif attribute is None:
81+
return sorted(columns_, key=lambda i: i.primary, reverse=True)
82+
else:
83+
raise KeyError
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
UNIQUE = {"unique": True}
2+
PRIMARY = {"primary": True}
3+
NULL = {"null": True}
4+
UNSIGNED = {"unsigned": True}
5+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from dataclasses import dataclass, field
2+
from pythonicMySQL.constructor.table import Table
3+
4+
5+
@dataclass
6+
class LinkedObject:
7+
8+
id: str = "wew"

pythonicMySQL/constructor/table.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
from __future__ import annotations
2+
from typing import Union, TypeVar, TYPE_CHECKING, overload
3+
from pythonicMySQL.mysqlclient import CLIENT
4+
from pythonicMySQL.constructor.column import columns
5+
from dataclasses import is_dataclass
6+
7+
8+
T = TypeVar("T")
9+
10+
11+
class Table:
12+
13+
def __init__(self, database: str, table: str, python_type: T):
14+
self._validate_dataclass(python_type)
15+
self.python_type = python_type
16+
self.database = database
17+
self.table = table
18+
19+
@staticmethod
20+
def _validate_dataclass(obj):
21+
if not is_dataclass(obj):
22+
raise ValueError("Object must be a dataclass")
23+
24+
def to_mysql_dict(self, obj, remove_id: bool = False) -> dict:
25+
self._validate_dataclass(obj)
26+
columns_ = columns(obj)
27+
mysql_dict = {}
28+
for key, value in obj.__dict__.items():
29+
matching = [column for column in columns_ if column.name == key]
30+
if matching:
31+
mysql_dict[key] = matching[0].mysql_type.convert_to_mysql(value)
32+
if remove_id:
33+
id_column = [column for column in columns_ if column.primary]
34+
mysql_dict.pop(id_column[0].name, '')
35+
return mysql_dict
36+
37+
def insert(self, obj: T, update_if_exists=False, commit: bool = True):
38+
if update_if_exists and len([column for column in columns(obj) if column.unique]) == 0:
39+
raise AssertionError("You cannot use update_if_exists when there are no unique keys")
40+
return CLIENT.insert(self.database, self.table, self.to_mysql_dict(obj),
41+
update_if_duplicate_key=update_if_exists, commit=commit)
42+
43+
def update(self, mysql_id: int, obj: T, commit=True):
44+
return CLIENT.update(self.database, self.table, mysql_id=mysql_id, dictionary=self.to_mysql_dict(obj, True),
45+
commit=commit)
46+
47+
def select(self, ascending: bool = True, limit: int = None, offset: int = 0, where: Union[dict, str] = " "):
48+
response = CLIENT.select(self.database, self.table,
49+
ascending=ascending, limit=limit, offset=offset, where=where)
50+
data = response.data
51+
result = []
52+
for row in data:
53+
insertion_object = {}
54+
for key, value in row.items():
55+
matching = [column for column in columns(self.python_type) if column.name == key]
56+
if matching:
57+
insertion_object[key] = matching[0].mysql_type.convert_to_python(value)
58+
result.append(self.python_type(**insertion_object))
59+
return result
60+
61+
@overload
62+
def get_item(self, _id: int):
63+
...
64+
65+
@overload
66+
def get_item(self, column: str, value: object):
67+
...
68+
69+
def get_item(self, *args):
70+
"""Gets first row corresponding to the search. Raises error if there are none."""
71+
if isinstance(args[0], int):
72+
return self.select(limit=1, where={"id": args[0]})[0]
73+
else:
74+
return self.select(limit=1, where={args[0]: args[1]})[0]
75+
76+
def __iter__(self):
77+
return iter(self.select())
78+
79+
def __len__(self):
80+
return CLIENT.get_number_of_rows(self.database, self.table)
81+
82+
83+
84+
#
85+
86+
# @overload
87+
# def table(_database: str, _table: str) -> object: ...
88+
#
89+
# def table(*, cls: Optional = None, mysql_connection: mysql.connector.connection.MySQLConnection = None, database: str = None,
90+
# table: str = None, object_type = None,
91+
# ascending: bool = True, limit: int = None, offset: int = 0, where: Union[dict, str] = " "):
92+
#
93+
# def wrap(cls):
94+
#
95+
# setattr(cls, "teuuw", 4)
96+
# cls.hi = lambda : print("hello")
97+
# return cls
98+
#
99+
# return wrap
100+
#
101+
#
102+
# @table
103+
# class Songs:
104+
#
105+
# def __init__(self):
106+
# self.h = 3
107+
#
108+
#
109+
# song = Songs()
110+
111+
#
112+
#
113+
# class Table:
114+
#
115+
# object_type = None
116+
# client = None
117+
#
118+
# def __init__(self, mysql_connection: mysql.connector.connection.MySQLConnection,
119+
# database: str, table: str, object_type: MySQLObject.__class__, ascending: bool = True, limit: int = None, offset: int = 0,
120+
# where: Union[dict, str] = " "):
121+
# self.ascending = ascending
122+
# self.limit = limit
123+
# self.offset = offset
124+
# self.where = where
125+
# self.__class__.client = Client(mysql_connection)
126+
# self.client = self.__class__.client
127+
# self.__class__.object_type = object_type
128+
# self.table = table
129+
# self.database = database
130+
#
131+
# def validate_columns(self):
132+
# mysql_description = self.client.describe_columns(self.database, self.table).fetchall()
133+
# assigned_columns = self.__class__.object_type.COLUMNS + Column.id_column()
134+
# for desc in mysql_description:
135+
# if desc["Field"] not in [column.name for column in assigned_columns]:
136+
# raise ValueError("Columns do not match")
137+
#
138+
# def create(self):
139+
# columns = [Column.id_column()] + self.object_type.COLUMNS
140+
# descriptions = [column.description for column in columns]
141+
# return self.client.create_table(self.database, self.table, descriptions)
142+
#
143+
# def update_columns(self):
144+
# columns = [Column.id_column()] + self.object_type.COLUMNS
145+
# descriptions = [column.description for column in columns]
146+
# self.__class__.client.update_table_columns(self.database, self.table, descriptions)
147+
#
148+
# def get(self) -> List[MySQLObject]:
149+
# result = self.client.select(self.database, self.table,
150+
# ascending=self.ascending, limit=self.limit, offset=self.offset, where=self.where)
151+
# response = SQLResponse(result)
152+
# data = response.data
153+
# result = []
154+
# for item in data:
155+
# id_ = item.pop(Column.ID_COLUMN_NAME)
156+
# obj = self.__class__.object_type(**item)
157+
# obj.mysql_row_id = id_
158+
# result.append(obj)
159+
# return result
160+
#
161+
# def insert(self, obj: MySQLObject, update_if_duplicate_key: bool = True, commit=True) -> OneDMLSQLResponse:
162+
# cursor = self.client.insert(self.database, self.table, obj.as_mysql_dict(),
163+
# update_if_duplicate_key=update_if_duplicate_key, commit=commit)
164+
# return OneDMLSQLResponse(cursor)
165+
#
166+
# def update(self, obj: MySQLObject, commit=True) -> OneDMLSQLResponse:
167+
# cursor = self.client.update(self.database, self.table, obj.mysql_row_id, obj.as_mysql_dict(), commit=commit)
168+
# response = OneDMLSQLResponse(cursor)
169+
# return response
170+
#
171+
# def __getitem__(self, item: Union[tuple, int]) -> MySQLObject:
172+
# if isinstance(item, tuple) and len(item) == 2:
173+
# result = self.client.select(self.database, self.table, where={item[0]: item[1]})
174+
# elif isinstance(item, int):
175+
# result = self.client.select(self.database, self.table, where={"id": item})
176+
# else:
177+
# raise KeyError
178+
# if len(result) == 1:
179+
# return result[0]
180+
# else:
181+
# raise KeyError
182+
#
183+
# def __iter__(self):
184+
# return iter(self.get())
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from pythonicMySQL.mysqlclient import CLIENT
2+
from pythonicMySQL.constructor.column import Column
3+
from typing import List
4+
5+
6+
def create_table(database, table, columns: List[Column], create_database=False):
7+
descriptions = [column.description for column in columns]
8+
CLIENT.create_table(database, table, column_descriptions=descriptions, create_database=create_database)
9+
10+
11+
def update_table(database, table, columns: List[Column]):
12+
descriptions = [column.description for column in columns]
13+
CLIENT.update_table_columns(database, table, column_descriptions=descriptions)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from pythonicMySQL.datatypes.mysqltypes import *
2+
from pythonicMySQL.datatypes.customtypes import *
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from pythonicMySQL.datatypes.mysqltypes import MySQLType
2+
import datetime
3+
4+
5+
class BOOLEAN(MySQLType):
6+
7+
def __init__(self):
8+
super(BOOLEAN, self).__init__("tinyint", length=1, python_type=bool)
9+
10+
def convert_to_mysql(self, python_value) -> object:
11+
return 1 if python_value is True else 0
12+
13+
def convert_to_python(self, mysql_value) -> object:
14+
if not isinstance(mysql_value, int):
15+
raise ValueError
16+
return bool(mysql_value)
17+
18+
19+
class TIMEDELTA(MySQLType):
20+
21+
def __init__(self):
22+
super(TIMEDELTA, self).__init__("decimal", length="10,3", python_type=datetime.timedelta)
23+
24+
def convert_to_python(self, mysql_value) -> datetime.timedelta:
25+
return datetime.timedelta(seconds=float(mysql_value))
26+
27+
def convert_to_mysql(self, python_value: datetime.timedelta) -> float:
28+
return python_value.seconds

0 commit comments

Comments
 (0)