trade-database-manager 0.0.1.dev4__tar.gz → 0.0.3.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {trade_database_manager-0.0.1.dev4/trade_database_manager.egg-info → trade_database_manager-0.0.3.dev1}/PKG-INFO +1 -1
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/pyproject.toml +1 -1
- trade_database_manager-0.0.3.dev1/trade_database_manager/core/kdb/__init__.py +5 -0
- trade_database_manager-0.0.3.dev1/trade_database_manager/core/kdb/kdbmanager.py +150 -0
- trade_database_manager-0.0.3.dev1/trade_database_manager/core/sql/__init__.py +5 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager/core/sql/sqlmanager.py +49 -11
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager/manager/metadata_sql.py +6 -8
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1/trade_database_manager.egg-info}/PKG-INFO +1 -1
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager.egg-info/SOURCES.txt +1 -1
- trade_database_manager-0.0.1.dev4/setup.py +0 -3
- trade_database_manager-0.0.1.dev4/trade_database_manager/core/kdb/__init__.py +0 -0
- trade_database_manager-0.0.1.dev4/trade_database_manager/core/sql/__init__.py +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/LICENSE +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/README.md +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/setup.cfg +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager/__init__.py +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager/config.py +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager/core/__init__.py +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager/core/sql/sqlreader.py +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager/core/sql/sqlwriter.py +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager/core/typedefs.py +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager/manager/__init__.py +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager/manager/fields_data_type.py +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager/manager/typedefs.py +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager.egg-info/dependency_links.txt +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager.egg-info/requires.txt +0 -0
- {trade_database_manager-0.0.1.dev4 → trade_database_manager-0.0.3.dev1}/trade_database_manager.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: trade_database_manager
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3.dev1
|
|
4
4
|
Summary: A wrapper of kdb and sql for convenient trade data management.
|
|
5
5
|
Author-email: "Y.Q. Cui" <qianyun210603@hotmail.com>
|
|
6
6
|
Classifier: Operating System :: POSIX :: Linux
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/4/27 12:26
|
|
3
|
+
# @Author : YQ Tsui
|
|
4
|
+
# @File : kdbmanager.py
|
|
5
|
+
# @Purpose :
|
|
6
|
+
import os.path
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import pykx
|
|
9
|
+
from ...config import CONFIG
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class KdbManager:
|
|
13
|
+
|
|
14
|
+
_instance = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def instance(cls):
|
|
18
|
+
if cls._instance is None:
|
|
19
|
+
cls._instance = cls()
|
|
20
|
+
cls._instance.__init__()
|
|
21
|
+
return cls._instance
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.host = CONFIG["kdbhost"]
|
|
25
|
+
self.port = CONFIG["kdbport"]
|
|
26
|
+
# self.username = CONFIG["username"]
|
|
27
|
+
# self.password = CONFIG["password"]
|
|
28
|
+
self.username = ""
|
|
29
|
+
self.password = ""
|
|
30
|
+
# print(self.host, self.port, self.username, self.password)
|
|
31
|
+
|
|
32
|
+
def path_exists(self, path: str):
|
|
33
|
+
"""
|
|
34
|
+
Checks if a path exists in the kdb database.
|
|
35
|
+
|
|
36
|
+
:param path: The path to check.
|
|
37
|
+
:type path: str
|
|
38
|
+
"""
|
|
39
|
+
with pykx.QConnection(self.host, self.port, username=self.username, password=self.password) as conn:
|
|
40
|
+
return bool(conn(f".path.exists `:{path}"))
|
|
41
|
+
|
|
42
|
+
def create_folder(self, path: str):
|
|
43
|
+
"""
|
|
44
|
+
Creates a folder in the kdb database.
|
|
45
|
+
|
|
46
|
+
:param path: The path of the folder.
|
|
47
|
+
:type path: str
|
|
48
|
+
"""
|
|
49
|
+
with pykx.QConnection(self.host, self.port, username=self.username, password=self.password) as conn:
|
|
50
|
+
conn(f".path.mkdir \"{path}\"")
|
|
51
|
+
|
|
52
|
+
def write(self, table_name: str, data: pd.DataFrame, path: str = "", splayed: bool = False):
|
|
53
|
+
"""
|
|
54
|
+
Writes data to the kdb database.
|
|
55
|
+
|
|
56
|
+
:param table_name: The table name in the kdb database.
|
|
57
|
+
:type table_name: str
|
|
58
|
+
:param data: The data to be written.
|
|
59
|
+
:type data: pd.DataFrame
|
|
60
|
+
:param path: The path of data
|
|
61
|
+
:type path: str
|
|
62
|
+
:param splayed: Whether the table is splayed.
|
|
63
|
+
:type splayed: bool
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
real_path = os.path.join(path, table_name) if path else table_name
|
|
67
|
+
if splayed:
|
|
68
|
+
real_path += "/"
|
|
69
|
+
with pykx.QConnection(self.host, self.port, username=self.username, password=self.password) as conn:
|
|
70
|
+
conn(f"{{`:{real_path}/ set x}}", data)
|
|
71
|
+
|
|
72
|
+
def write_partitioned(self, table_name: str, data: pd.DataFrame, path: str = "", partition_func=None, key_column=None):
|
|
73
|
+
"""
|
|
74
|
+
Writes data to the kdb database with partitioning.
|
|
75
|
+
|
|
76
|
+
:param table_name: The table name in the kdb database.
|
|
77
|
+
:type table_name: str
|
|
78
|
+
:param data: The data to be written.
|
|
79
|
+
:type data: pd.DataFrame
|
|
80
|
+
:param path: The path of data
|
|
81
|
+
:type path: str
|
|
82
|
+
:param partition_func: calculate the partition bucket from datetime column
|
|
83
|
+
:type partition_func: callable
|
|
84
|
+
:param key_column: Column which is sorted to server as key.
|
|
85
|
+
:type key_column: str
|
|
86
|
+
"""
|
|
87
|
+
if isinstance(data.index, pd.DatetimeIndex):
|
|
88
|
+
data.reset_index(inplace=True, names=["datetime"])
|
|
89
|
+
assert "datetime" in data.columns, "datetime column not found"
|
|
90
|
+
data.sort_values(by="datetime", inplace=True)
|
|
91
|
+
if key_column is not None and key_column in data.columns:
|
|
92
|
+
with pykx.QConnection(host=self.host, port=self.port, username=self.username, password=self.password) as conn:
|
|
93
|
+
for bucket, df in data.groupby(partition_func(data["datetime"])):
|
|
94
|
+
conn(f'{{`{table_name} set x; .partable.createOrAppend[`:{path};{bucket};`{key_column};`{table_name}]}}', df.reset_index(drop=True))
|
|
95
|
+
else:
|
|
96
|
+
with pykx.QConnection(host=self.host, port=self.port, username=self.username, password=self.password) as conn:
|
|
97
|
+
for bucket, df in data.groupby(by=partition_func(data["datetime"])):
|
|
98
|
+
conn(f'{{`{table_name} set x;.Q.dpt[`:{path};{bucket};`{table_name}]}}', df.reset_index(drop=True))
|
|
99
|
+
|
|
100
|
+
def read_partitioned(self, table_name: str, path: str = "", fields=None, start_time=None, end_time=None, partition_func=None,
|
|
101
|
+
other_conditions=None):
|
|
102
|
+
"""
|
|
103
|
+
Reads data from the kdb database with partitioning.
|
|
104
|
+
|
|
105
|
+
:param table_name: The table name in the kdb database.
|
|
106
|
+
:type table_name: str
|
|
107
|
+
:param path: The path of data
|
|
108
|
+
:type path: str
|
|
109
|
+
:param start_time: The start time of the data.
|
|
110
|
+
:type start_time: pd.Timestamp
|
|
111
|
+
:param end_time: The end time of the data.
|
|
112
|
+
:type end_time: pd.Timestamp
|
|
113
|
+
:param partition_func: calculate the partition bucket from datetime column
|
|
114
|
+
:type partition_func: callable
|
|
115
|
+
:param other_conditions: Other conditions for the query.
|
|
116
|
+
:type other_conditions: str
|
|
117
|
+
"""
|
|
118
|
+
time_format = "%Y.%m.%dD%H:%M:%S.%f"
|
|
119
|
+
start_time_str = start_time.strftime(time_format) if start_time is not None else None
|
|
120
|
+
end_time_str = end_time.strftime(time_format) if end_time is not None else None
|
|
121
|
+
if isinstance(fields, (bytes, str)):
|
|
122
|
+
fields = [fields]
|
|
123
|
+
select_clause = f"select from {table_name}" if fields is None else f"select {','.join(fields)} from {table_name}"
|
|
124
|
+
where_cond = ""
|
|
125
|
+
if start_time_str is not None and end_time_str is not None:
|
|
126
|
+
where_cond += f"datetime within ({start_time_str};{end_time_str})"
|
|
127
|
+
if partition_func is not None:
|
|
128
|
+
where_cond = f"int in {' '.join(str(x) for x in range(partition_func(start_time), partition_func(end_time) + 1))}" + ',' + where_cond
|
|
129
|
+
elif start_time_str is not None:
|
|
130
|
+
where_cond += f"datetime>={start_time_str}"
|
|
131
|
+
if partition_func is not None:
|
|
132
|
+
where_cond = f"int>={partition_func(start_time)}" + ',' + where_cond
|
|
133
|
+
elif end_time_str is not None:
|
|
134
|
+
where_cond += f"datetime<={end_time_str}"
|
|
135
|
+
if partition_func is not None:
|
|
136
|
+
where_cond = f"int<= {partition_func(end_time)}" + ',' + where_cond
|
|
137
|
+
if other_conditions is not None:
|
|
138
|
+
where_cond = other_conditions if where_cond == "" else where_cond + ',' + other_conditions
|
|
139
|
+
if where_cond:
|
|
140
|
+
where_cond = " where " + where_cond
|
|
141
|
+
final_query = select_clause + where_cond
|
|
142
|
+
|
|
143
|
+
with pykx.QConnection(host=self.host, port=self.port, username=self.username, password=self.password) as conn:
|
|
144
|
+
conn("`currpath__ set .path.pwd[]") # save current path to currpath__ as following command will change the path
|
|
145
|
+
try:
|
|
146
|
+
conn(f"\\l {path}") # load the path
|
|
147
|
+
q_table = conn(final_query)
|
|
148
|
+
return q_table.pd().set_index("datetime")
|
|
149
|
+
finally:
|
|
150
|
+
conn('system "cd ", currpath__')
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/4/15 20:28
|
|
3
|
+
# @Author : YQ Tsui
|
|
4
|
+
# @File : sqlmanager.py
|
|
5
|
+
# @Purpose : Core SQL operations
|
|
6
|
+
|
|
1
7
|
import re
|
|
2
8
|
from collections.abc import Container
|
|
3
9
|
from functools import partial, reduce
|
|
4
10
|
from typing import Any, Literal, Sequence, Type, Union
|
|
5
11
|
|
|
6
12
|
import pandas as pd
|
|
7
|
-
from sqlalchemy import
|
|
13
|
+
from sqlalchemy import DOUBLE_PRECISION, Index, Integer, MetaData, String, Table, create_engine, inspect, select, sql, text
|
|
8
14
|
from sqlalchemy.dialects.postgresql import insert
|
|
9
15
|
|
|
10
16
|
from ...config import CONFIG
|
|
@@ -107,7 +113,8 @@ class SqlManager:
|
|
|
107
113
|
self.add_index(table_name, column, unique=False)
|
|
108
114
|
return num_rows
|
|
109
115
|
|
|
110
|
-
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _convert_to_sqlalchemy_type(column_type: Type, **kwargs):
|
|
111
118
|
if isinstance(column_type, type):
|
|
112
119
|
column_type = column_type.__name__
|
|
113
120
|
if column_type == "str":
|
|
@@ -115,10 +122,10 @@ class SqlManager:
|
|
|
115
122
|
if column_type == "int":
|
|
116
123
|
return Integer()
|
|
117
124
|
if column_type == "float":
|
|
118
|
-
return
|
|
125
|
+
return DOUBLE_PRECISION()
|
|
119
126
|
raise ValueError(f"Unsupported column type {column_type}")
|
|
120
127
|
|
|
121
|
-
def insert_column(self, table_name: str, column_name: str, column_type:
|
|
128
|
+
def insert_column(self, table_name: str, column_name: str, column_type: str):
|
|
122
129
|
"""
|
|
123
130
|
Inserts a new column into a table.
|
|
124
131
|
|
|
@@ -126,13 +133,10 @@ class SqlManager:
|
|
|
126
133
|
:type table_name: str
|
|
127
134
|
:param column_name: The name of the new column.
|
|
128
135
|
:type column_name: str
|
|
129
|
-
:param column_type: The data type of the new column. It
|
|
130
|
-
:type column_type:
|
|
131
|
-
:param type_kwargs: Additional keyword arguments for the data type. Defaults to None.
|
|
132
|
-
:type type_kwargs: dict, optional
|
|
136
|
+
:param column_type: The data type of the new column. It is a string representation of the data type in PostgreSQL.
|
|
137
|
+
:type column_type: str
|
|
133
138
|
"""
|
|
134
|
-
|
|
135
|
-
sql_code = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {str(self._convert_to_sqlalchemy_type(column_type, **type_kwargs))}"
|
|
139
|
+
sql_code = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type}"
|
|
136
140
|
self._execute(sql_code)
|
|
137
141
|
|
|
138
142
|
def delete_column(self, table_name: str, column_name: str):
|
|
@@ -250,7 +254,6 @@ class SqlManager:
|
|
|
250
254
|
|
|
251
255
|
# cannot use pandas.read_sql here as it discards timezone info
|
|
252
256
|
res = self._execute(stmt)
|
|
253
|
-
pd.read_sql_query()
|
|
254
257
|
return pd.DataFrame(res.fetchall(), columns=res.keys())
|
|
255
258
|
|
|
256
259
|
def read_data_across_tables(
|
|
@@ -310,3 +313,38 @@ class SqlManager:
|
|
|
310
313
|
# cannot use pandas.read_sql here as it discards timezone info
|
|
311
314
|
res = self._execute(stmt)
|
|
312
315
|
return pd.DataFrame(res.fetchall(), columns=res.keys())
|
|
316
|
+
|
|
317
|
+
def create_ts_table(self, table_name: str, time_column: str, table_columns: dict[str, str],
|
|
318
|
+
time_chunk_interval: str = "1 year", other_chunkers: dict[str, int] = None):
|
|
319
|
+
"""
|
|
320
|
+
Creates a table with a timestamp column.
|
|
321
|
+
|
|
322
|
+
:param table_name: The name of the table to create.
|
|
323
|
+
:type table_name: str
|
|
324
|
+
:param time_column: The name of the timestamp column.
|
|
325
|
+
:type time_column: str
|
|
326
|
+
:param table_columns: The names of the columns to create with their data types. The keys are the column names and the values are the data types in PostgreSQL.
|
|
327
|
+
:type table_columns: Dict[str, str]
|
|
328
|
+
:param time_chunk_interval: The interval to chunk the time column by. Defaults to "1 year".
|
|
329
|
+
:type time_chunk_interval: str
|
|
330
|
+
:param other_chunkers: Other columns to chunk by. The keys are the column names and the values are the num of hashes. Defaults to None.
|
|
331
|
+
:type other_chunkers: Dict[str, int], optional
|
|
332
|
+
"""
|
|
333
|
+
query_create_sensordata_table = f"""
|
|
334
|
+
CREATE TABLE {table_name} (
|
|
335
|
+
{time_column} TIMESTAMP NOT NULL,
|
|
336
|
+
{", ".join([f"{column_name} {column_type}{' NOT NULL' if column_name in other_chunkers else ''}" for column_name, column_type in table_columns.items()])}
|
|
337
|
+
);
|
|
338
|
+
"""
|
|
339
|
+
query_create_sensordata_hypertable = f"SELECT create_hypertable('{table_name}', by_range('{time_column}', INTERVAL '{time_chunk_interval}'));"
|
|
340
|
+
if other_chunkers is not None:
|
|
341
|
+
for column_name, num_hash in other_chunkers.items():
|
|
342
|
+
query_create_sensordata_hypertable += f"\nSELECT add_dimension('{table_name}', by_hash('{column_name}', {num_hash}));"
|
|
343
|
+
|
|
344
|
+
uidx_name = f"uix_{table_name}_{'_'.join(other_chunkers.keys())}_{time_column}"
|
|
345
|
+
query_unique_index = f"CREATE UNIQUE INDEX {uidx_name} ON {table_name} ({', '.join(other_chunkers.keys())}, {time_column});"
|
|
346
|
+
with self.engine.connect() as conn:
|
|
347
|
+
conn.execute(text(query_create_sensordata_table))
|
|
348
|
+
conn.execute(text(query_create_sensordata_hypertable))
|
|
349
|
+
conn.execute(text(query_unique_index))
|
|
350
|
+
conn.commit()
|
|
@@ -133,7 +133,7 @@ class MetadataSql:
|
|
|
133
133
|
for inst_type, common_df_by_type in common_df.groupby("inst_type"):
|
|
134
134
|
inst_type = cast(INST_TYPE_LITERALS, inst_type)
|
|
135
135
|
if all_fields_common:
|
|
136
|
-
res[inst_type] = common_df_by_type
|
|
136
|
+
res[inst_type] = common_df_by_type.set_index(["ticker", "exchange"]) if len(common_df_by_type.columns) > 2 else common_df_by_type
|
|
137
137
|
continue
|
|
138
138
|
query_fields_type = (
|
|
139
139
|
["ticker", "exchange"] + [f for f in query_fields if f in TYPE_METADATA_COLUMNS[inst_type]]
|
|
@@ -147,7 +147,7 @@ class MetadataSql:
|
|
|
147
147
|
f"instruments_{inst_type.lower()}", query_fields=query_fields_type, filter_fields=filter_fields_type
|
|
148
148
|
)
|
|
149
149
|
type_df = common_df_by_type.merge(type_df, on=["ticker", "exchange"], how="inner")
|
|
150
|
-
res[inst_type] = type_df.set_index(["ticker", "exchange"])
|
|
150
|
+
res[inst_type] = type_df.set_index(["ticker", "exchange"]) if len(type_df.columns) > 2 else type_df
|
|
151
151
|
return res
|
|
152
152
|
|
|
153
153
|
def read_metadata_for_insttype(
|
|
@@ -187,9 +187,7 @@ class MetadataSql:
|
|
|
187
187
|
query_fields_cross = "*"
|
|
188
188
|
else:
|
|
189
189
|
query_fields_common = ["ticker", "exchange"] + [f for f in query_fields if f in COMMON_METADATA_COLUMNS]
|
|
190
|
-
query_fields_type = [
|
|
191
|
-
f for f in query_fields if f in TYPE_METADATA_COLUMNS[inst_type]
|
|
192
|
-
]
|
|
190
|
+
query_fields_type = [f for f in query_fields if f in TYPE_METADATA_COLUMNS[inst_type]]
|
|
193
191
|
query_fields_cross = {
|
|
194
192
|
"instruments": query_fields_common,
|
|
195
193
|
f"instruments_{inst_type.lower()}": query_fields_type,
|
|
@@ -213,8 +211,8 @@ class MetadataSql:
|
|
|
213
211
|
)
|
|
214
212
|
|
|
215
213
|
if isinstance(df.columns, pd.Index):
|
|
216
|
-
df = df.loc[:, ~df.columns.duplicated()]
|
|
217
|
-
|
|
218
|
-
df
|
|
214
|
+
df = df.loc[:, ~df.columns.duplicated()]
|
|
215
|
+
if len(df.columns) > 2:
|
|
216
|
+
df.set_index(["ticker", "exchange"], inplace=True)
|
|
219
217
|
self._convert_datetime_columns(df)
|
|
220
218
|
return df
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: trade_database_manager
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3.dev1
|
|
4
4
|
Summary: A wrapper of kdb and sql for convenient trade data management.
|
|
5
5
|
Author-email: "Y.Q. Cui" <qianyun210603@hotmail.com>
|
|
6
6
|
Classifier: Operating System :: POSIX :: Linux
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
|
-
setup.py
|
|
5
4
|
trade_database_manager/__init__.py
|
|
6
5
|
trade_database_manager/config.py
|
|
7
6
|
trade_database_manager.egg-info/PKG-INFO
|
|
@@ -12,6 +11,7 @@ trade_database_manager.egg-info/top_level.txt
|
|
|
12
11
|
trade_database_manager/core/__init__.py
|
|
13
12
|
trade_database_manager/core/typedefs.py
|
|
14
13
|
trade_database_manager/core/kdb/__init__.py
|
|
14
|
+
trade_database_manager/core/kdb/kdbmanager.py
|
|
15
15
|
trade_database_manager/core/sql/__init__.py
|
|
16
16
|
trade_database_manager/core/sql/sqlmanager.py
|
|
17
17
|
trade_database_manager/core/sql/sqlreader.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|