formatize 1.0.0__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.
- formatize-1.0.0/LICENSE +21 -0
- formatize-1.0.0/PKG-INFO +20 -0
- formatize-1.0.0/README.md +4 -0
- formatize-1.0.0/pyproject.toml +3 -0
- formatize-1.0.0/setup.cfg +28 -0
- formatize-1.0.0/src/formatize/__init__.py +3 -0
- formatize-1.0.0/src/formatize/form.py +805 -0
- formatize-1.0.0/src/formatize.egg-info/PKG-INFO +20 -0
- formatize-1.0.0/src/formatize.egg-info/SOURCES.txt +10 -0
- formatize-1.0.0/src/formatize.egg-info/dependency_links.txt +1 -0
- formatize-1.0.0/src/formatize.egg-info/top_level.txt +1 -0
formatize-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Asinerum Conlang Project
|
|
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.
|
formatize-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: formatize
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Literp Formatting Lib
|
|
5
|
+
Home-page: https://github.com/asinerum/formatize
|
|
6
|
+
Author: Asinerum Conlang Project
|
|
7
|
+
Author-email: asinerum.com@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
Detailed tips, tricks, and examples, can be found at project's repository
|
|
18
|
+
https://github.com/asinerum/formatize
|
|
19
|
+
|
|
20
|
+
(C) 2026 Asinerum Conlang Project
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = formatize
|
|
3
|
+
version = 1.0.0
|
|
4
|
+
author = Asinerum Conlang Project
|
|
5
|
+
author_email = asinerum.com@gmail.com
|
|
6
|
+
description = Literp Formatting Lib
|
|
7
|
+
long_description = file: README.md
|
|
8
|
+
long_description_content_type = text/markdown
|
|
9
|
+
url = https://github.com/asinerum/formatize
|
|
10
|
+
license = MIT
|
|
11
|
+
classifiers =
|
|
12
|
+
Programming Language :: Python :: 3
|
|
13
|
+
License :: OSI Approved :: MIT License
|
|
14
|
+
Operating System :: OS Independent
|
|
15
|
+
|
|
16
|
+
[options]
|
|
17
|
+
package_dir =
|
|
18
|
+
= src
|
|
19
|
+
packages = find:
|
|
20
|
+
python_requires = >=3.7
|
|
21
|
+
|
|
22
|
+
[options.packages.find]
|
|
23
|
+
where = src
|
|
24
|
+
|
|
25
|
+
[egg_info]
|
|
26
|
+
tag_build =
|
|
27
|
+
tag_date = 0
|
|
28
|
+
|
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict, create_model
|
|
2
|
+
from typing import List, Dict, Optional
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
|
|
6
|
+
import datetime
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import re
|
|
11
|
+
import inspect
|
|
12
|
+
import string
|
|
13
|
+
import uuid
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
import socket
|
|
17
|
+
import smtplib
|
|
18
|
+
import dns.resolver
|
|
19
|
+
|
|
20
|
+
import tkinter as tk
|
|
21
|
+
from PIL import Image, ImageTk
|
|
22
|
+
|
|
23
|
+
LENGTH_MD5 = 32
|
|
24
|
+
LENGTH_SHA256 = 64
|
|
25
|
+
APP_YAML = 'app.yaml'
|
|
26
|
+
ENCODING = 'utf-8'
|
|
27
|
+
ENCODINGSIG = 'utf-8-sig'
|
|
28
|
+
YAML_ENCODE = 'utf-8-sig'
|
|
29
|
+
|
|
30
|
+
EMPTY = ''
|
|
31
|
+
SPACE = ' '
|
|
32
|
+
DATESTRS = '-'
|
|
33
|
+
TIMESTRS = ':'
|
|
34
|
+
REGPUNCS = r'[' + string.punctuation + r']+'
|
|
35
|
+
REGTIMES = r'[' + SPACE + r'T]+'
|
|
36
|
+
|
|
37
|
+
ERROR = 'error'
|
|
38
|
+
PARAMS = 'params'
|
|
39
|
+
|
|
40
|
+
ADMIN = 'admin'
|
|
41
|
+
ACCOUNTING = 'accounting'
|
|
42
|
+
|
|
43
|
+
APP_PATH = 'script.app_'
|
|
44
|
+
DBA_PATH = 'lib.db.dbase_'
|
|
45
|
+
|
|
46
|
+
ERR_INPUT = 'Invalid input data'
|
|
47
|
+
|
|
48
|
+
## OLD LAMBDAS
|
|
49
|
+
|
|
50
|
+
def err_input (e):
|
|
51
|
+
return f'{ERR_INPUT}: {e}'
|
|
52
|
+
|
|
53
|
+
def data_dict ():
|
|
54
|
+
return {ERROR: None, PARAMS: {}}
|
|
55
|
+
|
|
56
|
+
def uid ():
|
|
57
|
+
return str(uuid.uuid4())
|
|
58
|
+
|
|
59
|
+
def md5 (text):
|
|
60
|
+
return crypto_md5(text)
|
|
61
|
+
|
|
62
|
+
def sha (text):
|
|
63
|
+
return crypto_sha256(text)
|
|
64
|
+
|
|
65
|
+
def now ():
|
|
66
|
+
return str(datetime.datetime.now())
|
|
67
|
+
|
|
68
|
+
def hstr (x):
|
|
69
|
+
return str(x or '')
|
|
70
|
+
|
|
71
|
+
def caller ():
|
|
72
|
+
return inspect.stack()[1][3]
|
|
73
|
+
|
|
74
|
+
def extract (s, sep='.'):
|
|
75
|
+
return s.split(sep)
|
|
76
|
+
|
|
77
|
+
def add_zero (n, zeros=2):
|
|
78
|
+
return str(n).zfill(zeros)
|
|
79
|
+
|
|
80
|
+
def sort_dict (obj):
|
|
81
|
+
return dict(sorted(obj.items()))
|
|
82
|
+
|
|
83
|
+
def copy_dict (obj): ## CloneObject
|
|
84
|
+
return json.loads(json.dumps(obj))
|
|
85
|
+
|
|
86
|
+
def dict_contain (obj, keys):
|
|
87
|
+
return all(key in obj for key in keys)
|
|
88
|
+
|
|
89
|
+
def sum_over_list_of_floats (lst):
|
|
90
|
+
return math.fsum(lst)
|
|
91
|
+
|
|
92
|
+
def sum_over_list_of_dicts (list_of_dicts, key):
|
|
93
|
+
return sum(float(d[key]) for d in list_of_dicts)
|
|
94
|
+
|
|
95
|
+
def format_decimal_number (num, dec=2):
|
|
96
|
+
return ('{:,.' + str(dec) + 'f}').format(float(num))
|
|
97
|
+
|
|
98
|
+
def draw_aligned_number (num, spaces=20, symb='$', dec=2):
|
|
99
|
+
return ralign(form(num, dec), spaces, symb)
|
|
100
|
+
|
|
101
|
+
def right_align_text (text, spaces=20, suffix=''):
|
|
102
|
+
return ('{:>' + str(spaces) + '}').format(text) + suffix
|
|
103
|
+
|
|
104
|
+
def output_keyval (object, left, right, sep='\n'):
|
|
105
|
+
return sep.join([('{:<' + str(left) + '}').format(key) + ('{:>' + str(right) + '}').format(object[key]) for key in object])
|
|
106
|
+
|
|
107
|
+
fs = sum_over_list_of_floats
|
|
108
|
+
|
|
109
|
+
lds = sum_over_list_of_dicts
|
|
110
|
+
|
|
111
|
+
form = format_decimal_number
|
|
112
|
+
|
|
113
|
+
number = draw_aligned_number
|
|
114
|
+
|
|
115
|
+
ralign = right_align_text
|
|
116
|
+
|
|
117
|
+
output = output_keyval
|
|
118
|
+
|
|
119
|
+
def to_upper (text):
|
|
120
|
+
return str_clean(text).upper()
|
|
121
|
+
|
|
122
|
+
def to_title (text):
|
|
123
|
+
return str_clean(text).title()
|
|
124
|
+
|
|
125
|
+
def dicts (models):
|
|
126
|
+
return [x.model_dump() for x in models]
|
|
127
|
+
|
|
128
|
+
def merge_dict (dict1, dict2):
|
|
129
|
+
return {**dict1, **dict2}
|
|
130
|
+
|
|
131
|
+
def generate_dict (items): ## TupleList
|
|
132
|
+
return {key:val for key,val in items}
|
|
133
|
+
|
|
134
|
+
def generate_json (items): ## TupleList
|
|
135
|
+
return json.dumps(generate_dict(items))
|
|
136
|
+
|
|
137
|
+
def create_dict (**kwargs): ## dcore.kwarg()
|
|
138
|
+
return {arg:kwargs[arg] for arg in kwargs}
|
|
139
|
+
|
|
140
|
+
def function_module2 (modules, funcname):
|
|
141
|
+
return next((key for key in modules if funcname in modules[key]), None)
|
|
142
|
+
|
|
143
|
+
def function_module (modules, funcname):
|
|
144
|
+
return next((key for key in app_functions(modules) if funcname in dir(modules[key])), None)
|
|
145
|
+
|
|
146
|
+
def app_functions (modules):
|
|
147
|
+
return {x: [y for y in dir(modules[x]) if callable(getattr(modules[x], y)) and y.lower()==y and not y.startswith('_')] for x in modules}
|
|
148
|
+
|
|
149
|
+
def prepare_json_info (info, InfoModel, defval=None):
|
|
150
|
+
return json.dumps(InfoModel(**info).model_dump()) if type(info) is dict else defval
|
|
151
|
+
|
|
152
|
+
def unpack_one_vs_many_db_values (model, mid, mvals, formatfunc=int):
|
|
153
|
+
return ','.join('(' + str(formatfunc(getattr(model,mid))) + ',' + str(formatfunc(v)) + ')' for v in getattr(model, mvals))
|
|
154
|
+
|
|
155
|
+
def flatten (matrix):
|
|
156
|
+
return [item for row in matrix for item in row]
|
|
157
|
+
|
|
158
|
+
def find_item (data, key, val): ## data:DictList
|
|
159
|
+
return find_item_in_list_of_dict(data, key, val)
|
|
160
|
+
|
|
161
|
+
def merge_list (list1, list2):
|
|
162
|
+
return merge_two_list_exclusive(list1, list2)
|
|
163
|
+
|
|
164
|
+
def check_list (list1, list2):
|
|
165
|
+
return any(x in list1 for x in list2)
|
|
166
|
+
|
|
167
|
+
def unpack_list_to_tuple_str (lst):
|
|
168
|
+
return f'{*lst,}'
|
|
169
|
+
|
|
170
|
+
def str_to_list (s):
|
|
171
|
+
return list(map(str.strip,s.split(',')))
|
|
172
|
+
|
|
173
|
+
def adjust_array (array, count, defval, defid=0):
|
|
174
|
+
return [defval]*count if type(array)!=list or not array else (array if count==len(array) else [array[defid]]+[defval]*(count-1))
|
|
175
|
+
|
|
176
|
+
def fillup_array (array, count, defval, defid=0):
|
|
177
|
+
return [defval]*count if type(array)!=list or not array else (array if count==len(array) else [array[defid]]*count)
|
|
178
|
+
|
|
179
|
+
news = adjust_array
|
|
180
|
+
|
|
181
|
+
dups = fillup_array
|
|
182
|
+
|
|
183
|
+
def default (params, attr, val):
|
|
184
|
+
return params.setdefault(attr, val)
|
|
185
|
+
|
|
186
|
+
def integer (val, defval=-1):
|
|
187
|
+
return int(float(val)) if is_numeric(val) else defval
|
|
188
|
+
|
|
189
|
+
def boolean (val):
|
|
190
|
+
return 0 if not val else 1
|
|
191
|
+
|
|
192
|
+
def setattrs (Module, attrs=[], vals={}):
|
|
193
|
+
return [setattr(Module, i, vals.get(i)) for i in attrs]
|
|
194
|
+
|
|
195
|
+
def get_attr (module, attr, defval=ValueError):
|
|
196
|
+
return getattr(module, attr) if attr in dir(module) else defval
|
|
197
|
+
|
|
198
|
+
def popattr (module=None, attr=None):
|
|
199
|
+
return getattr(module, attr) if attr in dir(module) else None
|
|
200
|
+
|
|
201
|
+
def popfunc (val, module=None, func=str, pattern=r'\/(.*?)\/'): ## val:/something/
|
|
202
|
+
return (popattr(module, m.group(1)) if (m:=re.search(pattern,str(val))) else val) or func
|
|
203
|
+
|
|
204
|
+
## BASIC FORMATTINGS
|
|
205
|
+
|
|
206
|
+
def replace_dict_placeholders(text: str, vals: dict, quot: str="'", holder: str=r'\:\S+\b') -> str:
|
|
207
|
+
## This may cause ambiguous matter in SQL STRING/NUMBER comparison
|
|
208
|
+
try:
|
|
209
|
+
subs = re.findall(holder, text)
|
|
210
|
+
items = iter(str(e) for e in subs)
|
|
211
|
+
return re.sub(holder, lambda lm: f"{quot}{vals[next(items)[1:]]}{quot}", text)
|
|
212
|
+
except:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
def replace_placeholders(text: str, vals: list, quot: str="'", holder: str=r'\?', formatfunc=int) -> str:
|
|
216
|
+
## This may cause ambiguous matter in SQL STRING/NUMBER comparison
|
|
217
|
+
try:
|
|
218
|
+
count = len(re.findall(holder, text))
|
|
219
|
+
if count != len(vals): return None
|
|
220
|
+
items = iter(str(formatfunc(e)) for e in vals)
|
|
221
|
+
return re.sub(holder, lambda lm: f"{quot}{next(items)}{quot}", text)
|
|
222
|
+
except:
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
def crc16(data: bytes, encoding: str='utf-8', purehex: bool=False) -> str:
|
|
226
|
+
if type(data)==str: data = data.encode(encoding)
|
|
227
|
+
if type(data) is not bytes: return None
|
|
228
|
+
crc = 0xFFFF
|
|
229
|
+
for byte in data:
|
|
230
|
+
crc ^= (byte << 8)
|
|
231
|
+
for _ in range(8):
|
|
232
|
+
if crc & 0x8000:
|
|
233
|
+
crc = (crc << 1) ^ 0x1021
|
|
234
|
+
else:
|
|
235
|
+
crc <<= 1
|
|
236
|
+
crc &= 0xFFFF
|
|
237
|
+
res = f'0x{crc:04X}'
|
|
238
|
+
return res if purehex else res[2:]
|
|
239
|
+
|
|
240
|
+
def module_extract(module, path: str, defval: any=None):
|
|
241
|
+
try:
|
|
242
|
+
obj = module
|
|
243
|
+
paths = extract(path)
|
|
244
|
+
for s in paths:
|
|
245
|
+
obj = getattr(obj, s)
|
|
246
|
+
return obj
|
|
247
|
+
except:
|
|
248
|
+
return defval
|
|
249
|
+
|
|
250
|
+
def dict_assign(object: dict, path: str, value: any, add: bool=False):
|
|
251
|
+
obj = object ## NoCopy
|
|
252
|
+
paths = extract(path)
|
|
253
|
+
for i, s in enumerate(paths):
|
|
254
|
+
if i==len(paths)-1:
|
|
255
|
+
obj[s] = value
|
|
256
|
+
else:
|
|
257
|
+
if s not in obj or type(obj[s]) != dict:
|
|
258
|
+
if add:
|
|
259
|
+
obj[s] = {}
|
|
260
|
+
else:
|
|
261
|
+
return
|
|
262
|
+
obj = obj[s]
|
|
263
|
+
|
|
264
|
+
def dict_extract(object: dict, path: str, defval: any=None) -> any:
|
|
265
|
+
try:
|
|
266
|
+
obj = copy_dict(object)
|
|
267
|
+
paths = extract(path)
|
|
268
|
+
for s in paths:
|
|
269
|
+
obj = obj.get(s)
|
|
270
|
+
return obj
|
|
271
|
+
except:
|
|
272
|
+
return defval
|
|
273
|
+
|
|
274
|
+
def dict_sort(object: dict, path: str='') -> dict:
|
|
275
|
+
subdict = dict_extract(object, path)
|
|
276
|
+
if subdict:
|
|
277
|
+
dict_assign(object, path, value=sort_dict(subdict))
|
|
278
|
+
else:
|
|
279
|
+
object = sort_dict(object)
|
|
280
|
+
return object
|
|
281
|
+
|
|
282
|
+
def merge_dicts(err: str, *sources) -> dict:
|
|
283
|
+
merged_dict = {}
|
|
284
|
+
for d in sources:
|
|
285
|
+
if not isinstance(d, dict): raise TypeError(err)
|
|
286
|
+
merged_dict.update(d)
|
|
287
|
+
return merged_dict
|
|
288
|
+
|
|
289
|
+
def find_field_in_list_of_dict(data: list, sub: dict, field: str, defval: any=None) -> any:
|
|
290
|
+
return next((item.get(field) for item in data if type(item)==dict and item.get(k:=list(sub.keys())[0])==sub[k]), defval)
|
|
291
|
+
|
|
292
|
+
def find_item_in_list_of_dict(data: list, key: str, val: any) -> dict:
|
|
293
|
+
return next((item for item in data if item[key]==val), None)
|
|
294
|
+
|
|
295
|
+
def select_items_from_dict(data: dict, selected_keys: list) -> dict:
|
|
296
|
+
for k in selected_keys:
|
|
297
|
+
if k not in data: selected_keys.remove(k)
|
|
298
|
+
return {k: data[k] for k in selected_keys}
|
|
299
|
+
|
|
300
|
+
def merge_two_list_exclusive(list1: list, list2: list) -> list:
|
|
301
|
+
return list(set(list(set(list2)-set(list1))+list1))
|
|
302
|
+
|
|
303
|
+
def str_is_hex(s: str) -> bool:
|
|
304
|
+
hex_digits = set(string.hexdigits)
|
|
305
|
+
return all(c in hex_digits for c in s)
|
|
306
|
+
|
|
307
|
+
def is_numeric(val: any, func=float) -> bool:
|
|
308
|
+
try:
|
|
309
|
+
func(val)
|
|
310
|
+
return True
|
|
311
|
+
except:
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
def prepare_password(params: dict, col_hpwd: str) -> bool:
|
|
315
|
+
params[col_hpwd] = prepare_hash_password(params.get(col_hpwd))
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
def prepare_info(params: dict, col_info: str, InfoModel) -> bool:
|
|
319
|
+
params[col_info] = prepare_json_info(params.get(col_info), InfoModel, params.setdefault(col_info,''))
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
def prepare_workinterval(model) -> bool:
|
|
323
|
+
model.workbegin = parse_time(model.workbegin)
|
|
324
|
+
model.workend = parse_time(model.workend)
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
def prepare_worktime(model) -> bool:
|
|
328
|
+
model.worktime = model.worktime or now()
|
|
329
|
+
model.worktime = parse_time(model.worktime)
|
|
330
|
+
return True
|
|
331
|
+
|
|
332
|
+
def totime(time: any=None, defval=None) -> datetime.datetime:
|
|
333
|
+
if not (time:=parse_time(time)): return defval
|
|
334
|
+
return datetime.datetime.fromisoformat(time)
|
|
335
|
+
|
|
336
|
+
def parse_doc_date(time: any=None) -> str:
|
|
337
|
+
if not (time:=parse_time(time)): return None ## For DATE()
|
|
338
|
+
return time[:10]
|
|
339
|
+
|
|
340
|
+
def parse_time(time: any=None) -> str:
|
|
341
|
+
if not time:
|
|
342
|
+
return None
|
|
343
|
+
elif type(time) is datetime.datetime:
|
|
344
|
+
return str(time)
|
|
345
|
+
elif type(time) is int:
|
|
346
|
+
return str(datetime.datetime.fromtimestamp(time))
|
|
347
|
+
try:
|
|
348
|
+
return str(datetime.datetime.fromisoformat(time))
|
|
349
|
+
except Exception:
|
|
350
|
+
try:
|
|
351
|
+
time = str_time_clean(time)
|
|
352
|
+
return str(datetime.datetime.fromisoformat(time))
|
|
353
|
+
except Exception:
|
|
354
|
+
return now()
|
|
355
|
+
|
|
356
|
+
def str_time_clean(text: str) -> str:
|
|
357
|
+
text = re.sub(REGTIMES,SPACE,text).strip()
|
|
358
|
+
part = text.split(SPACE)
|
|
359
|
+
try:
|
|
360
|
+
date = str_add_zero(str_clean(part[0],DATESTRS),DATESTRS)
|
|
361
|
+
time = str_add_zero(str_clean(part[1],TIMESTRS),TIMESTRS)
|
|
362
|
+
except IndexError:
|
|
363
|
+
time = ''
|
|
364
|
+
return (date+SPACE+time).strip()
|
|
365
|
+
|
|
366
|
+
def str_clean(text: str, sep: str=SPACE) -> str:
|
|
367
|
+
return re.sub(REGPUNCS,sep,text).strip()
|
|
368
|
+
|
|
369
|
+
def str_add_zero(text: str, sep: str) -> str:
|
|
370
|
+
try:
|
|
371
|
+
text = text.split(sep)
|
|
372
|
+
part = [f"{int(x):02d}" for x in text]
|
|
373
|
+
return sep.join(part)
|
|
374
|
+
except Exception:
|
|
375
|
+
return ''
|
|
376
|
+
|
|
377
|
+
def prepare_hash_password(given_pwd: str, default_pwd: str='123456') -> str:
|
|
378
|
+
if not given_pwd:
|
|
379
|
+
return text_to_password(default_pwd)
|
|
380
|
+
elif len(given_pwd)==LENGTH_MD5 and str_is_hex(given_pwd):
|
|
381
|
+
return crypto_sha256(given_pwd)
|
|
382
|
+
elif len(given_pwd)!=LENGTH_SHA256 or not str_is_hex(given_pwd):
|
|
383
|
+
return text_to_password(given_pwd)
|
|
384
|
+
return given_pwd
|
|
385
|
+
|
|
386
|
+
def text_to_password(text: str) -> str:
|
|
387
|
+
return crypto_sha256(crypto_md5(text))
|
|
388
|
+
|
|
389
|
+
def crypto_encode(text: str) -> (str, str):
|
|
390
|
+
return crypto_sha256(text), crypto_md5(text)
|
|
391
|
+
|
|
392
|
+
def crypto_sha256(text: str) -> str:
|
|
393
|
+
try:
|
|
394
|
+
text_bytes = text.encode(ENCODING)
|
|
395
|
+
sha256_hash = hashlib.sha256()
|
|
396
|
+
sha256_hash.update(text_bytes)
|
|
397
|
+
return sha256_hash.hexdigest()
|
|
398
|
+
except Exception:
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
def crypto_md5(text: str) -> str:
|
|
402
|
+
try:
|
|
403
|
+
text_bytes = text.encode(ENCODING)
|
|
404
|
+
md5_hash = hashlib.md5()
|
|
405
|
+
md5_hash.update(text_bytes)
|
|
406
|
+
return md5_hash.hexdigest()
|
|
407
|
+
except Exception:
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
def render_html(filename: str, dtexts: dict={}, dattributes: dict={}) -> BeautifulSoup:
|
|
411
|
+
soup = soup_from_template(filename)
|
|
412
|
+
return render_soup(soup, dtexts, dattributes) #soup
|
|
413
|
+
|
|
414
|
+
def render_soup(soup: BeautifulSoup, dtexts: dict={}, dattributes: dict={'placeholder':{}}) -> BeautifulSoup:
|
|
415
|
+
for id in dtexts:
|
|
416
|
+
soup = soup_replace_value(soup, id, dtexts[id])
|
|
417
|
+
for attr in dattributes:
|
|
418
|
+
for id in dattributes[attr]:
|
|
419
|
+
soup = soup_replace_attribute(soup, id, attr, dattributes[attr][id])
|
|
420
|
+
return soup
|
|
421
|
+
|
|
422
|
+
def soup_replace_attribute(soup: BeautifulSoup, id: str, attribute: str, newvalue: str) -> BeautifulSoup:
|
|
423
|
+
find = soup.find(id=id)
|
|
424
|
+
if find:
|
|
425
|
+
find[attribute] = newvalue
|
|
426
|
+
return soup
|
|
427
|
+
|
|
428
|
+
def soup_replace_value(soup: BeautifulSoup, id: str, newvalue: str) -> BeautifulSoup:
|
|
429
|
+
find = soup.find(id=id)
|
|
430
|
+
if find:
|
|
431
|
+
find.string.replace_with(newvalue)
|
|
432
|
+
return soup
|
|
433
|
+
|
|
434
|
+
def soup_from_template(filename: str, folder: str='') -> BeautifulSoup:
|
|
435
|
+
with open(f"{folder}{filename}", 'r', encoding=ENCODING) as file:
|
|
436
|
+
filetext = file.read()
|
|
437
|
+
return BeautifulSoup(filetext, 'html.parser')
|
|
438
|
+
|
|
439
|
+
def param_load_from_request(jsonstr: str) -> dict:
|
|
440
|
+
result = data_dict()
|
|
441
|
+
try:
|
|
442
|
+
result[PARAMS] = json.loads(jsonstr)
|
|
443
|
+
if type(result[PARAMS]) is not dict:
|
|
444
|
+
result[ERROR] = ERR_INPUT
|
|
445
|
+
except json.JSONDecodeError as e:
|
|
446
|
+
result[ERROR] = err_input(e)
|
|
447
|
+
except Exception as e:
|
|
448
|
+
result[ERROR] = err_input(e)
|
|
449
|
+
return result
|
|
450
|
+
|
|
451
|
+
def data_load_from_csv_file(filepath: str, ffill: bool=True, nonan: bool=True) -> dict:
|
|
452
|
+
import pandas as pd
|
|
453
|
+
import numpy as np
|
|
454
|
+
result = data_dict()
|
|
455
|
+
try:
|
|
456
|
+
df = pd.read_csv(filepath)
|
|
457
|
+
if ffill: df = df.fillna(method='ffill')
|
|
458
|
+
if nonan: df = df.replace({np.nan: None})
|
|
459
|
+
result[PARAMS] = df.to_dict(orient='records')
|
|
460
|
+
except Exception as e:
|
|
461
|
+
result[ERROR] = err_input(e)
|
|
462
|
+
return result
|
|
463
|
+
|
|
464
|
+
def data_load_from_json_file(filepath: str) -> dict:
|
|
465
|
+
result = data_dict()
|
|
466
|
+
try:
|
|
467
|
+
with open(filepath, encoding=ENCODINGSIG) as file:
|
|
468
|
+
result[PARAMS] = json.load(file)
|
|
469
|
+
except json.JSONDecodeError as e:
|
|
470
|
+
result[ERROR] = err_input(e)
|
|
471
|
+
except Exception as e:
|
|
472
|
+
result[ERROR] = err_input(e)
|
|
473
|
+
return result
|
|
474
|
+
|
|
475
|
+
def data_load_from_yaml_file(filepath: str=APP_YAML) -> dict:
|
|
476
|
+
result = data_dict()
|
|
477
|
+
try:
|
|
478
|
+
with open(filepath, encoding=ENCODINGSIG) as file:
|
|
479
|
+
result[PARAMS] = yaml.load(file, yaml.Loader)
|
|
480
|
+
if type(result[PARAMS]) is not dict:
|
|
481
|
+
result[ERROR] = ERR_INPUT
|
|
482
|
+
result[PARAMS] = {}
|
|
483
|
+
except Exception as e:
|
|
484
|
+
result[ERROR] = err_input(e)
|
|
485
|
+
return result
|
|
486
|
+
|
|
487
|
+
def data_dump_to_yaml_file(data: any, filepath: str, encoding=ENCODINGSIG, sort_keys=False) -> bool:
|
|
488
|
+
try:
|
|
489
|
+
with open(filepath, 'w', encoding=encoding) as file:
|
|
490
|
+
yaml.dump(data, file, default_flow_style=False, sort_keys=sort_keys)
|
|
491
|
+
return True
|
|
492
|
+
except:
|
|
493
|
+
return False
|
|
494
|
+
|
|
495
|
+
def data_dump_to_csv_file(listofdicts: list, filepath: str) -> bool:
|
|
496
|
+
import pandas as pd
|
|
497
|
+
try:
|
|
498
|
+
df = pd.DataFrame(listofdicts)
|
|
499
|
+
df.to_csv(filepath, index=False)
|
|
500
|
+
return True
|
|
501
|
+
except:
|
|
502
|
+
return False
|
|
503
|
+
|
|
504
|
+
dump_csv = data_dump_to_csv_file
|
|
505
|
+
|
|
506
|
+
def jsonloads(text: str) -> dict:
|
|
507
|
+
try:
|
|
508
|
+
return json.loads(text)
|
|
509
|
+
except:
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
def str_to_json(val: str) -> str:
|
|
513
|
+
if type(val) in [dict, list]: return json.dumps(val)
|
|
514
|
+
try:
|
|
515
|
+
return json.dumps(json.loads(str(val)))
|
|
516
|
+
except:
|
|
517
|
+
return '{}'
|
|
518
|
+
|
|
519
|
+
def load_yaml(yamlfile: str) -> dict:
|
|
520
|
+
with open(yamlfile, encoding=YAML_ENCODE) as file:
|
|
521
|
+
return yaml.load(file, Loader=yaml.Loader)
|
|
522
|
+
|
|
523
|
+
def dump_yaml(data: any, yamlfile: str) -> bool:
|
|
524
|
+
return data_dump_to_yaml_file(data, yamlfile, YAML_ENCODE)
|
|
525
|
+
|
|
526
|
+
def parse_model_dat(dat: dict) -> dict:
|
|
527
|
+
d = {x: tuple([eval(dat[x][0]), dat[x][1]]) for x in dat}
|
|
528
|
+
return d
|
|
529
|
+
|
|
530
|
+
def load_model(modelname: str, dat: dict) -> BaseModel:
|
|
531
|
+
return create_model(modelname, **parse_model_dat(dat), __config__=ConfigDict(coerce_numbers_to_str=True))
|
|
532
|
+
|
|
533
|
+
def load_model_from_yaml(modelname: str, modelfile: str='model.yaml') -> BaseModel:
|
|
534
|
+
try:
|
|
535
|
+
models = load_yaml(modelfile)
|
|
536
|
+
return load_model(modelname, models.get(modelname))
|
|
537
|
+
except:
|
|
538
|
+
return None
|
|
539
|
+
|
|
540
|
+
def load_models_from_yaml(modelfile: str='model.yaml') -> dict:
|
|
541
|
+
dat = {}
|
|
542
|
+
try:
|
|
543
|
+
models = load_yaml(modelfile)
|
|
544
|
+
for model in models: dat[model] = load_model(model, models[model])
|
|
545
|
+
except:
|
|
546
|
+
return None
|
|
547
|
+
return dat
|
|
548
|
+
|
|
549
|
+
def add_models_from_yaml(Module, modelfile: str='model.yaml') -> bool:
|
|
550
|
+
return update_module(Module, load_models_from_yaml(modelfile))
|
|
551
|
+
|
|
552
|
+
def prepare_model(model, BuildModel, DefModel):
|
|
553
|
+
if type(model) is dict:
|
|
554
|
+
return BuildModel(**model)
|
|
555
|
+
elif type(model) is list:
|
|
556
|
+
if issubclass(type(model[0]), BaseModel):
|
|
557
|
+
model = [x.model_dump() for x in model]
|
|
558
|
+
elif type(model[0]) is dict:
|
|
559
|
+
model = [BuildModel(**x).model_dump() for x in model]
|
|
560
|
+
return DefModel(array=model, limit=len(model))
|
|
561
|
+
return model
|
|
562
|
+
|
|
563
|
+
def ext_modules(appfile: str='app.yaml') -> list:
|
|
564
|
+
return data_load_from_yaml_file(appfile)[PARAMS].get('modules')
|
|
565
|
+
|
|
566
|
+
def merge_modules(source: str, targetmodule, exlist: list=[]):
|
|
567
|
+
sourcemodule = import_module(source)
|
|
568
|
+
for item in filter(lambda attr: attr in exlist or attr not in dir(targetmodule), dir(sourcemodule)):
|
|
569
|
+
setattr(targetmodule, item, getattr(sourcemodule, item))
|
|
570
|
+
|
|
571
|
+
def load_modules(sources: list, targetmodule, path: str=APP_PATH):
|
|
572
|
+
for source in sources: merge_modules(f'{path}{source}', targetmodule)
|
|
573
|
+
|
|
574
|
+
def load_ext_modules(targetmodule):
|
|
575
|
+
load_modules(ext_modules(), targetmodule)
|
|
576
|
+
|
|
577
|
+
add_modules_from_yaml = load_ext_modules
|
|
578
|
+
|
|
579
|
+
def setattrs_from_yaml(Module, iofile: str='io.yaml') -> bool:
|
|
580
|
+
return update_module(Module, data_load_from_yaml_file(iofile)[PARAMS])
|
|
581
|
+
|
|
582
|
+
add_consts_from_yaml = setattrs_from_yaml
|
|
583
|
+
|
|
584
|
+
def get_module(module, path=APP_PATH):
|
|
585
|
+
try:
|
|
586
|
+
return import_module(f'{path}{module}')
|
|
587
|
+
except:
|
|
588
|
+
return None
|
|
589
|
+
|
|
590
|
+
def get_modules_from_yaml(path: str=APP_PATH, appfile: str='app.yaml') -> dict:
|
|
591
|
+
modules = [ADMIN, ACCOUNTING]
|
|
592
|
+
params = data_load_from_yaml_file(appfile)[PARAMS]
|
|
593
|
+
if params:
|
|
594
|
+
mx = params.get('modules')
|
|
595
|
+
if type(mx) is str: mx = list(map(str.strip, mx.split(',')))
|
|
596
|
+
if type(mx) is list: modules = merge_list(modules, mx)
|
|
597
|
+
try:
|
|
598
|
+
return {module: get_module(module, path) for module in modules}
|
|
599
|
+
except:
|
|
600
|
+
return {}
|
|
601
|
+
|
|
602
|
+
def update_module(Module, vals: dict) -> bool:
|
|
603
|
+
try:
|
|
604
|
+
setattrs(Module, [x for x in vals], vals)
|
|
605
|
+
return True
|
|
606
|
+
except:
|
|
607
|
+
return False
|
|
608
|
+
|
|
609
|
+
## FRONTEND FORMATTINGS
|
|
610
|
+
|
|
611
|
+
def load_yaml_temp (filepath):
|
|
612
|
+
return data_load_from_yaml_file(filepath).get(PARAMS)
|
|
613
|
+
|
|
614
|
+
def load_yaml_account_temp (filename, folder='temp/yaml/account/'):
|
|
615
|
+
return load_yaml_temp(f'{folder}{filename}')
|
|
616
|
+
|
|
617
|
+
def load_yaml_subaccount_temp (filename='subaccount.yaml'):
|
|
618
|
+
return load_yaml_account_temp(filename).get('subaccount_data')
|
|
619
|
+
|
|
620
|
+
def load_yaml_detailaccount_temp (filename='detailaccount.yaml'):
|
|
621
|
+
return load_yaml_account_temp(filename).get('detailaccount_data')
|
|
622
|
+
|
|
623
|
+
def load_csv_temp (filepath):
|
|
624
|
+
return data_load_from_csv_file(filepath).get(PARAMS)
|
|
625
|
+
|
|
626
|
+
def load_csv_account_temp (filename, folder='temp/csv/account/'):
|
|
627
|
+
return load_csv_temp(f'{folder}{filename}')
|
|
628
|
+
|
|
629
|
+
def load_csv_subaccount_temp (filename='subaccount.csv'):
|
|
630
|
+
return load_csv_account_temp(filename)
|
|
631
|
+
|
|
632
|
+
def load_csv_detailaccount_temp (filename='detailaccount.csv'):
|
|
633
|
+
return load_csv_account_temp(filename)
|
|
634
|
+
|
|
635
|
+
class Napas:
|
|
636
|
+
QRORDER = 'form.method.account.currency.amount.country'
|
|
637
|
+
|
|
638
|
+
def __init__(self, napas: dict, dcopy: dict, banks: list):
|
|
639
|
+
self.napas = napas
|
|
640
|
+
self.dcopy = dcopy
|
|
641
|
+
self.banks = banks
|
|
642
|
+
|
|
643
|
+
def dget(self):
|
|
644
|
+
return self.dcopy
|
|
645
|
+
|
|
646
|
+
def concat(self, path: str) -> str:
|
|
647
|
+
item = dict_extract(self.dcopy, path)
|
|
648
|
+
if type(item)!=dict or not dict_contain(item, extract('id.length.value')):
|
|
649
|
+
raise ValueError('Invalid values assigned')
|
|
650
|
+
return f"{item['id']}{item['length']}{item['value']}"
|
|
651
|
+
|
|
652
|
+
def dset(self,
|
|
653
|
+
config: str,
|
|
654
|
+
bankcode: str,
|
|
655
|
+
account: str,
|
|
656
|
+
currency: str,
|
|
657
|
+
country: str,
|
|
658
|
+
amount: float=0,
|
|
659
|
+
data: dict=None):
|
|
660
|
+
method = dict_extract(self.napas, f'method.value.{config.lower()}')
|
|
661
|
+
if not method:
|
|
662
|
+
raise ValueError('Invalid initiation method')
|
|
663
|
+
dict_assign(self.dcopy, 'method.value', method)
|
|
664
|
+
service = dict_extract(self.napas, f'account.value.service.value.{config.lower()}')
|
|
665
|
+
if not service:
|
|
666
|
+
raise ValueError('Invalid service code')
|
|
667
|
+
dict_assign(self.dcopy, 'account.value.service.value', service)
|
|
668
|
+
bankbin = find_field_in_list_of_dict(self.banks, {'code': bankcode.upper()}, 'bin')
|
|
669
|
+
if not bankbin:
|
|
670
|
+
raise ValueError('Invalid bank code')
|
|
671
|
+
dict_assign(self.dcopy, 'account.value.bank.value.acquirer.value', bankbin)
|
|
672
|
+
if not account:
|
|
673
|
+
raise ValueError('Invalid beneficiary account')
|
|
674
|
+
accountlength = len(account)
|
|
675
|
+
len_merchant = add_zero(accountlength)
|
|
676
|
+
len_bank = add_zero(accountlength + 14)
|
|
677
|
+
len_account = add_zero(accountlength + 44)
|
|
678
|
+
dict_assign(self.dcopy, 'account.value.bank.value.merchant.value', account)
|
|
679
|
+
dict_assign(self.dcopy, 'account.value.bank.value.merchant.length', len_merchant)
|
|
680
|
+
dict_assign(self.dcopy, 'account.value.bank.length', len_bank)
|
|
681
|
+
dict_assign(self.dcopy, 'account.length', len_account)
|
|
682
|
+
currencycode = dict_extract(self.napas, f'currency.value.{currency.upper()}.code')
|
|
683
|
+
if not currencycode:
|
|
684
|
+
raise ValueError('Invalid currency code')
|
|
685
|
+
dict_assign(self.dcopy, 'currency.value', currencycode)
|
|
686
|
+
if float(amount)<0:
|
|
687
|
+
raise ValueError('Invalid billing amount')
|
|
688
|
+
amount = str(amount)
|
|
689
|
+
amountlength = len(amount)
|
|
690
|
+
len_amount = add_zero(amountlength)
|
|
691
|
+
dict_assign(self.dcopy, 'amount.value', amount)
|
|
692
|
+
dict_assign(self.dcopy, 'amount.length', len_amount)
|
|
693
|
+
countrycode = dict_extract(self.napas, f'country.value.{country.upper()}.code')
|
|
694
|
+
if not countrycode:
|
|
695
|
+
raise ValueError('Invalid country code')
|
|
696
|
+
dict_assign(self.dcopy, 'country.value', countrycode)
|
|
697
|
+
guidstring = self.concat('account.value.guid')
|
|
698
|
+
acquirerstring = self.concat('account.value.bank.value.acquirer')
|
|
699
|
+
merchantstring = self.concat('account.value.bank.value.merchant')
|
|
700
|
+
dict_assign(self.dcopy, 'account.value.bank.value', acquirerstring+merchantstring)
|
|
701
|
+
bankstring = self.concat('account.value.bank')
|
|
702
|
+
servicestring = self.concat('account.value.service')
|
|
703
|
+
accountstring = f'{guidstring}{bankstring}{servicestring}'
|
|
704
|
+
dict_assign(self.dcopy, 'account.value', accountstring)
|
|
705
|
+
beforedatastring = ''
|
|
706
|
+
for i in extract(self.QRORDER):
|
|
707
|
+
beforedatastring += self.concat(i)
|
|
708
|
+
napasdata = dict_extract(self.napas, 'data.value.id')
|
|
709
|
+
if data and dict_contain(napasdata, data):
|
|
710
|
+
more = ''
|
|
711
|
+
for key in data:
|
|
712
|
+
more += f"{napasdata[key]['code']}{add_zero(len(data[key]))}{data[key]}"
|
|
713
|
+
morelength = len(more)
|
|
714
|
+
dict_assign(self.dcopy, 'data.value', more)
|
|
715
|
+
dict_assign(self.dcopy, 'data.length', add_zero(morelength))
|
|
716
|
+
datastring = self.concat('data')
|
|
717
|
+
else:
|
|
718
|
+
del self.dcopy['data']
|
|
719
|
+
datastring = ''
|
|
720
|
+
beforecrcstring = f'{beforedatastring}{datastring}6304'
|
|
721
|
+
crcstring = crc16(beforecrcstring)
|
|
722
|
+
dict_assign(self.dcopy, 'crc.value', crcstring)
|
|
723
|
+
return f'{beforecrcstring}{crcstring}'
|
|
724
|
+
|
|
725
|
+
def qextract(self, qstr: str, qmodel=None, idcol='id') -> dict:
|
|
726
|
+
join = lambda d: ''.join(d.values())
|
|
727
|
+
take = lambda s: {'id': s[:2], 'length': s[2:4], 'value': s[4:4+int(s[2:4])]}
|
|
728
|
+
section = lambda qmodel, id: next(key for key in qmodel if qmodel[key][idcol]==id)
|
|
729
|
+
if qmodel is None: qmodel = self.napas
|
|
730
|
+
qdata = {}
|
|
731
|
+
try:
|
|
732
|
+
while len(qstr)>0:
|
|
733
|
+
d = take(qstr)
|
|
734
|
+
qdata[section(qmodel, d['id'])] = d
|
|
735
|
+
qstr = qstr[len(join(d)):]
|
|
736
|
+
return {'error': None, 'result': qdata}
|
|
737
|
+
except Exception as e:
|
|
738
|
+
return {'error': f'IOError: {e}', 'result': None}
|
|
739
|
+
|
|
740
|
+
def qextract2(self, qstr: str) -> dict:
|
|
741
|
+
qext = self.qextract(qstr)
|
|
742
|
+
if qext['error']: return qext
|
|
743
|
+
qdata = qext['result']
|
|
744
|
+
try:
|
|
745
|
+
qaccount = qdata['account']
|
|
746
|
+
account = self.qextract(qaccount['value'], dict_extract(self.napas, 'account.value'))['result']
|
|
747
|
+
bank = self.qextract(dict_extract(account, 'bank.value'), dict_extract(self.napas, 'account.value.bank.value'))['result']
|
|
748
|
+
service = account['service']
|
|
749
|
+
guid = account['guid']
|
|
750
|
+
qdata['account']['value'] = {'guid': guid, 'bank': bank, 'service': service}
|
|
751
|
+
dataval = dict_extract(qdata, 'data.value')
|
|
752
|
+
if dataval: qdata['data']['value'] = self.qextract(dataval, dict_extract(self.napas, 'data.value.id'), idcol='code')['result']
|
|
753
|
+
return {'error': None, 'result': qdata}
|
|
754
|
+
except Exception as e:
|
|
755
|
+
return {'error': f'IOError: {e}', 'result': None}
|
|
756
|
+
|
|
757
|
+
write = dset
|
|
758
|
+
read = qextract2
|
|
759
|
+
|
|
760
|
+
def mailvalid(address: str) -> bool:
|
|
761
|
+
records = dns.resolver.resolve('gmail.com', 'MX')
|
|
762
|
+
mxRecord = records[0].exchange
|
|
763
|
+
mxRecord = str(mxRecord)
|
|
764
|
+
host = socket.gethostname()
|
|
765
|
+
server = smtplib.SMTP()
|
|
766
|
+
server.set_debuglevel(0)
|
|
767
|
+
server.connect(mxRecord)
|
|
768
|
+
server.helo(host)
|
|
769
|
+
server.mail('me@domain.com')
|
|
770
|
+
code, message = server.rcpt(str(address))
|
|
771
|
+
server.quit()
|
|
772
|
+
return True if code==250 else False
|
|
773
|
+
|
|
774
|
+
def print_qrcode(data: str, path: str) -> bool:
|
|
775
|
+
import qrcode
|
|
776
|
+
try:
|
|
777
|
+
img = qrcode.make(data)
|
|
778
|
+
img.save(f'{path}.png')
|
|
779
|
+
return True
|
|
780
|
+
except:
|
|
781
|
+
return False
|
|
782
|
+
|
|
783
|
+
def popup_image(filepath, title: str='Image Popup', errfile: str='File not found or bad', errload: str='Error loading file'):
|
|
784
|
+
popup = tk.Toplevel()
|
|
785
|
+
popup.title(title)
|
|
786
|
+
try:
|
|
787
|
+
img = Image.open(filepath)
|
|
788
|
+
tkimg = ImageTk.PhotoImage(img)
|
|
789
|
+
label = tk.Label(popup, image=tkimg)
|
|
790
|
+
label.image = tkimg
|
|
791
|
+
label.pack()
|
|
792
|
+
except FileNotFoundError:
|
|
793
|
+
label = tk.Label(popup, text=f'FileError: {errfile}')
|
|
794
|
+
label.pack()
|
|
795
|
+
except Exception as e:
|
|
796
|
+
label = tk.Label(popup, text=f'LoadError: {errload}: {e}')
|
|
797
|
+
label.pack()
|
|
798
|
+
|
|
799
|
+
def window_image(filepath: str, title: str='Image Window'):
|
|
800
|
+
root = tk.Tk()
|
|
801
|
+
root.title(title)
|
|
802
|
+
tkimg = ImageTk.PhotoImage(Image.open(filepath))
|
|
803
|
+
label = tk.Label(root, image=tkimg)
|
|
804
|
+
label.pack()
|
|
805
|
+
root.mainloop()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: formatize
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Literp Formatting Lib
|
|
5
|
+
Home-page: https://github.com/asinerum/formatize
|
|
6
|
+
Author: Asinerum Conlang Project
|
|
7
|
+
Author-email: asinerum.com@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
Detailed tips, tricks, and examples, can be found at project's repository
|
|
18
|
+
https://github.com/asinerum/formatize
|
|
19
|
+
|
|
20
|
+
(C) 2026 Asinerum Conlang Project
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
formatize
|