model.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import io
  2. import json
  3. import os
  4. from dataclasses import dataclass, field
  5. from functools import cached_property
  6. from pathlib import Path
  7. import pandas as pd
  8. import pyodbc
  9. from sqlalchemy import Engine, create_engine
  10. @dataclass
  11. class DsnConfig:
  12. user: str = "sa"
  13. password: str = "Mffu3011#"
  14. server: str = "LOCALHOST\\GLOBALCUBE"
  15. database: str = "CARLO"
  16. driver: str = "mssql"
  17. schema: str = "import"
  18. def conn_ini(self, db_type: str) -> str:
  19. return "\n".join(
  20. [
  21. f'{db_type}_SERVER="{self.server}"',
  22. f'{db_type}_USER="{self.user}"',
  23. f'{db_type}_PASSWORD="{self.password}"',
  24. f'{db_type}_DATABASE="{self.database}"',
  25. ]
  26. )
  27. class DatabaseInspect:
  28. _cursor: pyodbc.Cursor = None
  29. _sqlalchemy_engine: Engine = None
  30. def __init__(self, dsn: DsnConfig, source=False):
  31. self.dsn = dsn
  32. self.type = "SOURCE" if source else "DEST"
  33. @property
  34. def conn_string(self) -> str:
  35. if self.dsn.driver == "mssql":
  36. return ";".join(
  37. [
  38. "Driver={SQL Server Native Client 11.0}",
  39. f"Server={self.dsn.server}",
  40. f"Database={self.dsn.database}",
  41. f"Uid={self.dsn.user}",
  42. f"Pwd={self.dsn.password}",
  43. ]
  44. )
  45. if self.dsn.driver == "mysql":
  46. return f"mysql+pymysql://{self.dsn.user}:{self.dsn.password}@{self.dsn.server}/{self.dsn.database}?charset=utf8mb4"
  47. return ";".join(
  48. [
  49. "Driver={PostgreSQL Unicode}",
  50. f"Server={self.dsn.server}",
  51. "Port=5432",
  52. f"Database={self.dsn.database}",
  53. f"Uid={self.dsn.user}",
  54. f"Pwd={self.dsn.password}",
  55. ]
  56. )
  57. # f"DSN={self.dsn.server};UID={self.dsn.user};PWD={self.dsn.password}"
  58. @property
  59. def conn_string_sqlalchemy(self) -> str:
  60. if self.dsn.driver == "mssql":
  61. return (
  62. f"mssql+pyodbc://{self.dsn.user}:{self.dsn.password}@{self.dsn.server}/{self.dsn.database}?"
  63. "driver=SQL+Server+Native+Client+11.0"
  64. )
  65. if self.dsn.driver == "mysql":
  66. return f"mysql+pymysql://{self.dsn.user}:{self.dsn.password}@{self.dsn.server}/{self.dsn.database}?charset=utf8mb4"
  67. return f"pyodbc://{self.dsn.user}:{self.dsn.password}@{self.dsn.server}/{self.dsn.database}?driver={self.dsn.driver}"
  68. @property
  69. def bcp_conn_params(self) -> str:
  70. return f"-S {self.dsn.server} -d {self.dsn.database} -U {self.dsn.user} -P {self.dsn.password}"
  71. @property
  72. def cursor(self) -> pyodbc.Cursor:
  73. if not self._cursor:
  74. self._cursor = self.connect()
  75. return self._cursor
  76. @property
  77. def sqlalchemy_engine(self) -> Engine:
  78. if not self._sqlalchemy_engine:
  79. self._sqlalchemy_engine = create_engine(self.conn_string_sqlalchemy)
  80. return self._sqlalchemy_engine
  81. def connect(self) -> pyodbc.Cursor:
  82. c = pyodbc.connect(self.conn_string)
  83. return c.cursor()
  84. @cached_property
  85. def tables_list(self) -> list[str]:
  86. tables = [x[2] for x in self.cursor.tables(tableType="TABLE")]
  87. views = [x[2] for x in self.cursor.tables(tableType="VIEW")]
  88. return tables + views
  89. def get_prefix(self) -> dict[str, str]:
  90. source_tables_prefix = dict(
  91. enumerate(sorted(list(set([t.split("$")[0] for t in self.tables_list if "$" in t]))), 1)
  92. )
  93. if len(source_tables_prefix) == 0:
  94. q = self.cursor.execute("select name FROM sys.databases")
  95. source_tables_prefix = [x[0] for x in q.fetchall()]
  96. return source_tables_prefix
  97. def get_columns(self, table: str) -> list[str]:
  98. source_insp_cols = [col.column_name for col in self.cursor.columns(table=table)]
  99. if len(source_insp_cols) == 0:
  100. q = self.cursor.execute(
  101. "SELECT COLUMN_NAME as column_name FROM information_schema.columns "
  102. + f"WHERE TABLE_NAME = '{self.convert_table(table)}'"
  103. )
  104. source_insp_cols = [col[0] for col in q.fetchall()]
  105. return source_insp_cols
  106. def get_columns_is_typeof_str(self, table: str) -> list[str]:
  107. source_insp_cols = [
  108. col.data_type in [pyodbc.SQL_CHAR, pyodbc.SQL_VARCHAR] for col in self.cursor.columns(table=table)
  109. ]
  110. if len(source_insp_cols) == 0:
  111. q = self.cursor.execute(
  112. "SELECT COLLATION_NAME as column_collation FROM information_schema.columns "
  113. + f"WHERE TABLE_NAME = '{self.convert_table(table)}'"
  114. )
  115. source_insp_cols = [len(col[0]) > 0 for col in q.fetchall()]
  116. return source_insp_cols
  117. def get_pkey(self, table: str, catalog: str) -> list[str]:
  118. source_insp_cols = [col.column_name for col in self.cursor.primaryKeys(table=table, catalog=catalog)]
  119. if len(source_insp_cols) == 0:
  120. self.cursor.execute(f"USE {catalog}")
  121. q = self.cursor.execute(
  122. "SELECT COLUMN_NAME "
  123. "FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE "
  124. "WHERE OBJECTPROPERTY(OBJECT_ID(CONSTRAINT_SCHEMA + '.' + QUOTENAME(CONSTRAINT_NAME)), 'IsPrimaryKey') = 1 "
  125. f"AND TABLE_NAME = '{self.convert_table(table)}' " # AND TABLE_SCHEMA = 'dbo'"
  126. )
  127. source_insp_cols = [col[0] for col in q.fetchall()]
  128. self.cursor.execute(f"USE {self.dsn.database}")
  129. return source_insp_cols
  130. def convert_table(self, table: str) -> str:
  131. if "." in table:
  132. table = table.split(".")[-1]
  133. if "[" in table:
  134. table = table[1:-1]
  135. return table
  136. @dataclass
  137. class DbCreateConfig:
  138. name: str = "CARLO"
  139. csv_file: str = "..\\config\\CARLO.csv"
  140. clients: dict[str, str] = field(default_factory=lambda: {"1": "M und S Fahrzeughandel GmbH"})
  141. filter: list[str] = (["2018-01-01T00:00:00", "2022-01-01T00:00:00"],)
  142. source_dsn: DsnConfig = None
  143. dest_dsn: DsnConfig = None
  144. temp_db: str = "CARLOX"
  145. stage_dir: str = "..\\temp"
  146. batch_dir: str = "..\\batch"
  147. logs_dir: str = "..\\logs"
  148. scripts_dir: str = "C:\\GlobalCube\\Tasks\\scripts"
  149. source_inspect: DatabaseInspect = None
  150. dest_inspect: DatabaseInspect = None
  151. @property
  152. def conn_ini(self) -> str:
  153. return "\n".join(
  154. [
  155. f'SQL_TEMP="{self.stage_dir}"',
  156. f'SQL_BATCH="{self.batch_dir}"',
  157. f'SQL_LOGS="{self.logs_dir}"',
  158. ]
  159. )
  160. @staticmethod
  161. def load_config(config_file: str):
  162. cfg_import = json.load(open(config_file, "r", encoding="latin-1"))
  163. base_dir = Path(config_file).resolve().parent
  164. cfg_import["name"] = Path(config_file).stem
  165. if "logs_dir" not in cfg_import:
  166. cfg_import["logs_dir"] = "..\\logs"
  167. if "scripts_dir" not in cfg_import:
  168. cfg_import["scripts_dir"] = "C:\\GlobalCube\\Tasks\\scripts"
  169. if "target_dsn" in cfg_import:
  170. cfg_import["dest_dsn"] = cfg_import["target_dsn"]
  171. del cfg_import["target_dsn"]
  172. for folder in ["stage_dir", "batch_dir", "logs_dir", "scripts_dir"]:
  173. if cfg_import[folder].startswith(".."):
  174. cfg_import[folder] = str(base_dir.joinpath(cfg_import[folder]).resolve())
  175. os.makedirs(cfg_import[folder], exist_ok=True)
  176. for folder in ["source", "dest", "diff"]:
  177. os.makedirs(cfg_import["stage_dir"] + "\\" + folder, exist_ok=True)
  178. if ":" not in cfg_import["csv_file"]:
  179. cfg_import["csv_file"] = str((base_dir / cfg_import["csv_file"]).resolve())
  180. cfg_import["source_dsn"] = DsnConfig(**cfg_import["source_dsn"])
  181. cfg_import["dest_dsn"] = DsnConfig(**cfg_import["dest_dsn"])
  182. cfg = DbCreateConfig(**cfg_import)
  183. cfg.source_inspect = DatabaseInspect(cfg.source_dsn, source=True)
  184. cfg.dest_inspect = DatabaseInspect(cfg.dest_dsn, source=False)
  185. DbCreateConfig._cfg = cfg
  186. return cfg
  187. @staticmethod
  188. def get_instance():
  189. return DbCreateConfig._cfg
  190. def create_db_ini(self) -> None:
  191. with open(self.scripts_dir + "/../DB.ini", "w", encoding="cp850") as fwh:
  192. fwh.write(self.conn_ini)
  193. fwh.write("\n\n")
  194. fwh.write(self.source_dsn.conn_ini("SOURCE"))
  195. fwh.write("\n\n")
  196. fwh.write(self.dest_dsn.conn_ini("DEST"))
  197. fwh.write("\n")
  198. @dataclass
  199. class SourceTable:
  200. source: str
  201. client_db: str
  202. prefix: str
  203. @property
  204. def stage_csv(self) -> str:
  205. return ""
  206. @property
  207. def table_client(self) -> str:
  208. return ""
  209. @property
  210. def table_name(self) -> str:
  211. return ""
  212. @property
  213. def select_query(self) -> str:
  214. return ""
  215. @dataclass
  216. class DestTable:
  217. source: str
  218. dest: str
  219. dest_db: str
  220. filter_: str
  221. query: str
  222. iterative: str
  223. cols: str
  224. source_tables: list[SourceTable] = None
  225. dest_inspect: DatabaseInspect = None
  226. cfg: DbCreateConfig = None
  227. @property
  228. def table_batch_file(self) -> str:
  229. return f"{self.cfg.batch_dir}/{self.dest}.bat"
  230. @property
  231. def full_table_name(self) -> str:
  232. return f"[{self.dest_db}].[{self.cfg.dest_dsn.schema}].[{self.dest}]"
  233. @property
  234. def temp_table_name(self) -> str:
  235. return f"[{self.cfg.temp_db}].[temp].[{self.dest}]"
  236. @cached_property
  237. def columns_list(self) -> list[str]:
  238. res = self.dest_inspect.get_columns(self.dest)
  239. if "CLIENT_DB" in res:
  240. res.remove("CLIENT_DB")
  241. res.append("Client_DB")
  242. return res
  243. @cached_property
  244. def column_types(self) -> list[str]:
  245. return self.dest_inspect.get_columns_is_typeof_str(self.dest)
  246. @cached_property
  247. def primary_key(self) -> list[str]:
  248. return self.dest_inspect.get_pkey(self.dest, self.dest_db)
  249. @property
  250. def insert_query(self) -> str:
  251. return f"INSERT INTO {self.full_table_name} SELECT * FROM {self.temp_table_name} T1"
  252. @property
  253. def delete_query(self) -> str:
  254. # pkey = self.primary_key
  255. if len(self.primary_key) == 0:
  256. return ""
  257. pkey_join_list = [f"T1.[{col}] = T2.[{col}]" for col in self.primary_key]
  258. pkey_join = " AND ".join(pkey_join_list)
  259. return f"DELETE T1 FROM {self.full_table_name} T1 INNER JOIN {self.temp_table_name} T2 ON {pkey_join}"
  260. class SourceTable2(SourceTable):
  261. dest_table: DestTable
  262. cfg: DbCreateConfig
  263. _select_query: str = None
  264. source_inspect: DatabaseInspect = None
  265. info: str = ""
  266. @property
  267. def table_client(self) -> str:
  268. return f"{self.dest_table.dest}_{self.client_db}"
  269. @property
  270. def stage_csv(self) -> str:
  271. return f"{self.cfg.stage_dir}\\{self.table_client}.csv"
  272. @property
  273. def table_name(self) -> str:
  274. return self.source.format(self.prefix)
  275. @cached_property
  276. def source_columns(self) -> set[str]:
  277. return set(self.source_inspect.get_columns(self.table_name))
  278. @cached_property
  279. def select_query(self):
  280. f = io.StringIO()
  281. # print("Auf beiden Seiten: " + ";".join(intersect))
  282. diff1 = self.source_columns.difference(self.dest_table.columns_list)
  283. if len(diff1) > 0:
  284. f.write("rem Nur in Quelle: " + ";".join(diff1) + "\n")
  285. diff2 = set(self.dest_table.columns_list).difference(self.source_columns)
  286. if "Client_DB" not in diff2:
  287. f.write("echo Spalte 'Client_DB' fehlt!\n")
  288. return
  289. diff2.remove("Client_DB")
  290. if len(diff2) > 0:
  291. f.write("rem Nur in Ziel: " + ";".join(diff2) + "\n")
  292. if not pd.isnull(self.dest_table.query):
  293. select_query = self.dest_table.query.format(self.prefix, self.cfg.filter[0], self.cfg.filter[1])
  294. elif "." in self.table_name or self.cfg.source_dsn.schema == "":
  295. if self.table_name[0] != "[":
  296. self.table_name = f"[{self.table_name}]"
  297. select_query = f"SELECT T1.* FROM {self.table_name} T1 "
  298. else:
  299. select_query = f"SELECT T1.* FROM [{self.cfg.source_dsn.schema}].[{self.table_name}] T1 "
  300. if not pd.isnull(self.dest_table.filter_):
  301. select_query += " WHERE " + self.dest_table.filter_.format("", self.cfg.filter[0], self.cfg.filter[1])
  302. elif "WHERE" not in select_query:
  303. select_query += " WHERE 1 = 1"
  304. if "timestamp" not in self.source_columns:
  305. print(self.dest_table.dest + " hat kein timestamp-Feld")
  306. self.info = f.getvalue()
  307. return select_query
  308. @property
  309. def select_query_with_columns(self) -> str:
  310. res = self.select_query.replace("T1.*", self.select_columns)
  311. if "timestamp" in self.source_columns:
  312. res += " ORDER BY T1.[timestamp] "
  313. return res
  314. @property
  315. def select_columns(self):
  316. intersect = self.source_columns.intersection(self.dest_table.columns_list)
  317. res = ""
  318. for col, is_char_type in zip(self.dest_table.columns_list, self.dest_table.column_types):
  319. if col in intersect:
  320. if False and is_char_type: # vorerst deaktiviert
  321. res += f"dbo.cln(T1.[{col}]), "
  322. else:
  323. res += f"T1.[{col}], "
  324. elif col == "Client_DB":
  325. res += f"'{self.client_db}' as [Client_DB], "
  326. else:
  327. res += "'' as [" + col + "], "
  328. res = res[:-2]
  329. return res