cashd-core 0.3.2__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.
- cashd_core-0.3.2/PKG-INFO +25 -0
- cashd_core-0.3.2/pyproject.toml +51 -0
- cashd_core-0.3.2/setup.cfg +4 -0
- cashd_core-0.3.2/src/cashd_core/__init__.py +1 -0
- cashd_core-0.3.2/src/cashd_core/backup.py +280 -0
- cashd_core-0.3.2/src/cashd_core/const.py +95 -0
- cashd_core-0.3.2/src/cashd_core/data.py +916 -0
- cashd_core-0.3.2/src/cashd_core/prefs.py +295 -0
- cashd_core-0.3.2/src/cashd_core.egg-info/PKG-INFO +25 -0
- cashd_core-0.3.2/src/cashd_core.egg-info/SOURCES.txt +17 -0
- cashd_core-0.3.2/src/cashd_core.egg-info/dependency_links.txt +1 -0
- cashd_core-0.3.2/src/cashd_core.egg-info/entry_points.txt +2 -0
- cashd_core-0.3.2/src/cashd_core.egg-info/requires.txt +10 -0
- cashd_core-0.3.2/src/cashd_core.egg-info/top_level.txt +1 -0
- cashd_core-0.3.2/tests/__init__.py +0 -0
- cashd_core-0.3.2/tests/mock_data.py +88 -0
- cashd_core-0.3.2/tests/test_backup.py +237 -0
- cashd_core-0.3.2/tests/test_data.py +266 -0
- cashd_core-0.3.2/tests/test_prefs.py +50 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cashd_core
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Core functionality of Cashd and it's variants.
|
|
5
|
+
Author-email: "Vitor F. Lins" <vflins@live.com>
|
|
6
|
+
Keywords: backend,
|
|
7
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: SQLAlchemy==2.*
|
|
20
|
+
Requires-Dist: phonenumbers>=8.13.31
|
|
21
|
+
Requires-Dist: pyshortcuts==1.9.0
|
|
22
|
+
Requires-Dist: PyGObject; sys_platform == "linux"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
25
|
+
Requires-Dist: black>=24.4.2; extra == "dev"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cashd_core"
|
|
3
|
+
authors = [
|
|
4
|
+
{name = "Vitor F. Lins", email = "vflins@live.com"},
|
|
5
|
+
]
|
|
6
|
+
description = "Core functionality of Cashd and it's variants."
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
keywords = ["backend", ""]
|
|
9
|
+
license = {file = "LICENSE"}
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Topic :: Office/Business :: Financial :: Accounting",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"License :: OSI Approved :: MIT License"
|
|
22
|
+
]
|
|
23
|
+
version = "0.3.2"
|
|
24
|
+
dependencies = [
|
|
25
|
+
"SQLAlchemy==2.*",
|
|
26
|
+
"phonenumbers>=8.13.31",
|
|
27
|
+
"pyshortcuts==1.9.0",
|
|
28
|
+
"PyGObject; sys_platform == 'linux'"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev=[
|
|
33
|
+
"pytest-cov>=5.0.0",
|
|
34
|
+
"black>=24.4.2",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["setuptools", "setuptools-scm", "wheel"]
|
|
39
|
+
build-backend = "setuptools.build_meta"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
cashd-server = "cashd.app:start_cashd"
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = "tests"
|
|
46
|
+
pythonpath = "src"
|
|
47
|
+
addopts = "--cov=src --cov-report html"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
namespaces = false
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import data, prefs, const
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
from os import path, rename, makedirs
|
|
2
|
+
from sys import platform
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
import configparser
|
|
6
|
+
import sqlite3
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
|
|
10
|
+
from cashd_core.prefs import BackupPrefsHandler
|
|
11
|
+
|
|
12
|
+
####################
|
|
13
|
+
# GLOBAL VARS
|
|
14
|
+
####################
|
|
15
|
+
|
|
16
|
+
if platform == "win32":
|
|
17
|
+
CASHD_FILES_PATH = Path.home().joinpath("AppData", "Local", "Cashd")
|
|
18
|
+
CONFIG_PATH = Path(CASHD_FILES_PATH, "configs")
|
|
19
|
+
LOG_PATH = Path(CASHD_FILES_PATH, "logs")
|
|
20
|
+
else:
|
|
21
|
+
CASHD_FILES_PATH = Path.home().joinpath(".local", "share", "Cashd")
|
|
22
|
+
CONFIG_PATH = Path.home().joinpath(".config", "Cashd")
|
|
23
|
+
LOG_PATH = Path.home().joinpath(".local", "state", "Cashd", "logs")
|
|
24
|
+
|
|
25
|
+
CONFIG_FILE = Path(CONFIG_PATH, "backup.ini")
|
|
26
|
+
LOG_FILE = path.join(LOG_PATH, "backup.log")
|
|
27
|
+
DB_FILE = path.join(CASHD_FILES_PATH, "data", "database.db")
|
|
28
|
+
BACKUP_PATH = path.join(CASHD_FILES_PATH, "data", "backup")
|
|
29
|
+
|
|
30
|
+
for dirpath in [CASHD_FILES_PATH, CONFIG_PATH, LOG_PATH, BACKUP_PATH]:
|
|
31
|
+
makedirs(dirpath, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
settings = BackupPrefsHandler()
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger("cashd.backup")
|
|
36
|
+
logger.setLevel(logging.DEBUG)
|
|
37
|
+
logger.propagate = False
|
|
38
|
+
|
|
39
|
+
log_fmt = logging.Formatter("%(asctime)s :: %(levelname)s %(message)s")
|
|
40
|
+
log_handler = logging.FileHandler(LOG_FILE)
|
|
41
|
+
log_handler.setLevel(logging.DEBUG)
|
|
42
|
+
log_handler.setFormatter(log_fmt)
|
|
43
|
+
|
|
44
|
+
logger.addHandler(log_handler)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
####################
|
|
48
|
+
# UTILS
|
|
49
|
+
####################
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BackupPlacesSource:
|
|
53
|
+
def __init__(self):
|
|
54
|
+
"""Mimics `cashd_core.data._DataSource` behavior providing interaction to the
|
|
55
|
+
list of backup places.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def current_data(self) -> list[str]:
|
|
60
|
+
return settings.read_backup_places()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def copy_file(source_path: str, target_dir: str, _raise: bool = False):
|
|
64
|
+
"""Copies a file to `target_dir`.
|
|
65
|
+
|
|
66
|
+
:param source_path: Full path to the file to be copied.
|
|
67
|
+
:param target_dir: Directory where the file will be copied into.
|
|
68
|
+
:param _raise: Boolean indicating if errors should also be raised, if false, errors
|
|
69
|
+
will only be silently logged to a log file.
|
|
70
|
+
|
|
71
|
+
:raises NotADirectoryError: If `target_dir` is not a valid directory and `_raise=True`.
|
|
72
|
+
:raises FileNotFoundError: If `source_path` is not a valid file and `_raise=True`.
|
|
73
|
+
"""
|
|
74
|
+
logger.debug("function call: copy_file")
|
|
75
|
+
if not path.exists(target_dir):
|
|
76
|
+
err_msg = f"'{target_dir}' does not exist."
|
|
77
|
+
logger.error(err_msg)
|
|
78
|
+
if _raise:
|
|
79
|
+
raise NotADirectoryError(err_msg)
|
|
80
|
+
now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S_%f")
|
|
81
|
+
try:
|
|
82
|
+
filename = f"backup_{now}.db"
|
|
83
|
+
shutil.copyfile(source_path, path.join(target_dir, filename))
|
|
84
|
+
logger.info(f"Copia de '{source_path}' criada em '{target_dir}'")
|
|
85
|
+
except FileNotFoundError as xpt:
|
|
86
|
+
logger.error(f"Erro realizando copia: {xpt}.", exc_info=True)
|
|
87
|
+
if _raise:
|
|
88
|
+
raise xpt
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def rename_on_db_folder(current: str, new: str, _raise: bool = False):
|
|
92
|
+
"""Renames a file in the same folder where `DB_FILE` is located. If the renaming
|
|
93
|
+
operation fails because the file is in use, it makes a copy with the new name instead
|
|
94
|
+
of renaming.
|
|
95
|
+
|
|
96
|
+
:param current: Current name of the file.
|
|
97
|
+
:param new: Name that the file will be renamed into.
|
|
98
|
+
:param _raise: Boolean indicating if errors should also be raised, if false, errors
|
|
99
|
+
will only be silently logged to a log file.
|
|
100
|
+
|
|
101
|
+
:raises: The last error received if both operations fail and `_raise=True`.
|
|
102
|
+
"""
|
|
103
|
+
logger.debug("function call: rename_on_db_folder")
|
|
104
|
+
current, new = str(current), str(new)
|
|
105
|
+
|
|
106
|
+
db_folder = path.split(DB_FILE)[0]
|
|
107
|
+
path_to_current = path.join(db_folder, current)
|
|
108
|
+
path_to_new = path.join(db_folder, new)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
rename(path_to_current, path_to_new)
|
|
112
|
+
logger.info(f"{path_to_current} renomeado como {path_to_new}")
|
|
113
|
+
except WindowsError:
|
|
114
|
+
shutil.copy(path_to_current, path_to_new)
|
|
115
|
+
except Exception as xpt:
|
|
116
|
+
logger.error(f"Erro renomeando {path_to_current}: {
|
|
117
|
+
xpt}", exc_info=True)
|
|
118
|
+
if _raise:
|
|
119
|
+
raise xpt
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def check_sqlite(file: str, _raise: bool = False) -> bool | None:
|
|
123
|
+
"""Checks if `file` represents a sqlite database. All unexpected errors are only
|
|
124
|
+
logged to `backup.log`, but can also be raised if `_raise=True`.
|
|
125
|
+
|
|
126
|
+
:param file: Path to the file to be checked.
|
|
127
|
+
:param _raise: Boolean indicating if errors should also be raised, if false, errors
|
|
128
|
+
will only be silently logged to a log file.
|
|
129
|
+
|
|
130
|
+
:returns: A boolean value indicating if `file` is a SQLite database, or None if an
|
|
131
|
+
unexpected error is catched.
|
|
132
|
+
"""
|
|
133
|
+
logger.debug("function call: check_sqlite")
|
|
134
|
+
if not path.exists(file):
|
|
135
|
+
msg = f"{file=} does not exist."
|
|
136
|
+
logger.error(msg)
|
|
137
|
+
if _raise:
|
|
138
|
+
raise FileExistsError(msg)
|
|
139
|
+
try:
|
|
140
|
+
con = sqlite3.connect(file)
|
|
141
|
+
except sqlite3.OperationalError as err:
|
|
142
|
+
return False
|
|
143
|
+
cursor = con.cursor()
|
|
144
|
+
try:
|
|
145
|
+
res = cursor.execute(f"PRAGMA schema_version;").fetchone()[0]
|
|
146
|
+
return type(res) is int
|
|
147
|
+
except sqlite3.DatabaseError as err:
|
|
148
|
+
return False
|
|
149
|
+
except Exception as err:
|
|
150
|
+
logger.critical(
|
|
151
|
+
f"Unexpected error validating {file}: {str(err)}", exc_info=True
|
|
152
|
+
)
|
|
153
|
+
if _raise:
|
|
154
|
+
raise err
|
|
155
|
+
return None
|
|
156
|
+
finally:
|
|
157
|
+
con.close()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def read_db_size(file_path: str = DB_FILE) -> int:
|
|
161
|
+
"""Gives the size (kilobytes) of the provided file, 0 (zero) if it doesn't exist."""
|
|
162
|
+
logger.debug("function call: read_db_size")
|
|
163
|
+
try:
|
|
164
|
+
return path.getsize(file_path)
|
|
165
|
+
except Exception as xpt:
|
|
166
|
+
logger.error(f"Erro lendo tamanho do arquivo: {str(xpt)}")
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
####################
|
|
171
|
+
# LEITURAS
|
|
172
|
+
####################
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def read_last_recorded_size(config_file: str = CONFIG_FILE):
|
|
176
|
+
"""Reads last recorded DB file size from config file."""
|
|
177
|
+
logger.debug("function call: read_last_recorded_size")
|
|
178
|
+
config = configparser.ConfigParser()
|
|
179
|
+
config.read(config_file)
|
|
180
|
+
if "file_sizes" in config:
|
|
181
|
+
return config["file_sizes"].getint("dbsize", fallback=None)
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
####################
|
|
186
|
+
# ESCRITAS
|
|
187
|
+
####################
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def write_current_size(
|
|
191
|
+
current_size: int | None = read_db_size(), settings: BackupPrefsHandler = settings
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Writes current database size to `backup.ini`."""
|
|
194
|
+
logger.debug("function call: write_current_size")
|
|
195
|
+
settings.write_dbsize(current_size)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def write_add_backup_place(path: str, settings: BackupPrefsHandler = settings) -> None:
|
|
199
|
+
"""Includes the input `path` in the 'backup_places' option in `backup.ini`."""
|
|
200
|
+
logger.debug("function call: write_add_backup_place")
|
|
201
|
+
settings.add_backup_place(path)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def write_rm_backup_place(idx: int, settings: BackupPrefsHandler = settings) -> None:
|
|
205
|
+
"""Removes the `idx`-th item from the 'backup_places' list in `backup.ini`."""
|
|
206
|
+
logger.debug("function call: write_rm_backup_place")
|
|
207
|
+
settings.rm_backup_place(idx)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def load(file: str, _raise: bool = False) -> None:
|
|
211
|
+
"""If `file` is a valid SQLite database, loads it as the current database in Cashd.
|
|
212
|
+
If a database is already present, it will be renamed to a unique name and will be
|
|
213
|
+
kept in the directory.
|
|
214
|
+
|
|
215
|
+
:param file: Full path to the file that be loaded as the new database.
|
|
216
|
+
:param _raise: All unexpected errors are only logged to `backup.log`, but can also
|
|
217
|
+
be raised if `_raise=True`.
|
|
218
|
+
"""
|
|
219
|
+
logger.debug("function call: load")
|
|
220
|
+
db_is_present = path.isfile(DB_FILE)
|
|
221
|
+
|
|
222
|
+
if not check_sqlite(file):
|
|
223
|
+
msg = f"Cannot load non-SQLite file: '{file}'."
|
|
224
|
+
logger.error(msg)
|
|
225
|
+
if _raise:
|
|
226
|
+
raise OSError(msg)
|
|
227
|
+
|
|
228
|
+
if db_is_present:
|
|
229
|
+
now = datetime.now()
|
|
230
|
+
dbfilename = path.split(DB_FILE)[1]
|
|
231
|
+
stashfilename = f"stashed{now}.db".replace(":", "-")
|
|
232
|
+
rename_on_db_folder(dbfilename, stashfilename)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
shutil.copyfile(file, DB_FILE)
|
|
236
|
+
except shutil.SameFileError:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def run(
|
|
241
|
+
force: bool = False, settings: BackupPrefsHandler = settings, _raise: bool = False
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Copies the database file to the local backup folder and to the folders listed in
|
|
244
|
+
the 'backup_places' option in `backup.ini`.
|
|
245
|
+
|
|
246
|
+
:param force: Using `force=False` will only make a copy if the file has increased in
|
|
247
|
+
size, compared to what's recorded in 'file_sizes'. Otherwise, will copy anyway.
|
|
248
|
+
:param settings: Settings handler that will be used to read the database file's size.
|
|
249
|
+
:param _raise: All unexpected errors are only logged to `backup.log`, but can also be
|
|
250
|
+
raised if `_raise=True`.
|
|
251
|
+
"""
|
|
252
|
+
backup_places: list = settings.read_backup_places()
|
|
253
|
+
error_was_raised = False
|
|
254
|
+
|
|
255
|
+
current_size = read_db_size()
|
|
256
|
+
previous_size = settings.read_dbsize()
|
|
257
|
+
|
|
258
|
+
if not force:
|
|
259
|
+
if current_size <= previous_size:
|
|
260
|
+
return
|
|
261
|
+
settings.write_dbsize(current_size)
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
backup_places = [i for i in [BACKUP_PATH] + backup_places if i != ""]
|
|
265
|
+
for place in backup_places:
|
|
266
|
+
try:
|
|
267
|
+
copy_file(DB_FILE, place, _raise=_raise)
|
|
268
|
+
except Exception as err:
|
|
269
|
+
logger.error(
|
|
270
|
+
f"Nao foi possivel salvar em '{place}': {err}", exc_info=True
|
|
271
|
+
)
|
|
272
|
+
if _raise:
|
|
273
|
+
error_was_raised = True
|
|
274
|
+
except Exception as err:
|
|
275
|
+
logger.error(f"Erro inesperado durante o backup: {err}", exc_info=True)
|
|
276
|
+
finally:
|
|
277
|
+
if error_was_raised:
|
|
278
|
+
raise NotADirectoryError(
|
|
279
|
+
f"Erro em alguma etapa do backup, verifique o log: {LOG_FILE}"
|
|
280
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from sys import platform
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sys_dark_mode() -> bool:
|
|
7
|
+
"""Detects if dark mode is currently enabled in the OS.
|
|
8
|
+
|
|
9
|
+
:returns: `True` if dark mode is enabled, `False` otherwise.
|
|
10
|
+
"""
|
|
11
|
+
match platform:
|
|
12
|
+
case "linux":
|
|
13
|
+
return _linux_dark_mode()
|
|
14
|
+
case "win32":
|
|
15
|
+
return False # ignore while dark theme is not implemented for winforms
|
|
16
|
+
# return _windows_dark_mode()
|
|
17
|
+
case _:
|
|
18
|
+
Warning(f"Not capable of detecting {platform=} dark mode.")
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _windows_dark_mode() -> bool:
|
|
23
|
+
"""Dectects if dark mode is enabled in a Windows system."""
|
|
24
|
+
# stackoverflow.com/a/65349866
|
|
25
|
+
# github.com/albertosottile/darkdetect/blob/master/darkdetect/_windows_detect.py
|
|
26
|
+
try:
|
|
27
|
+
import winreg
|
|
28
|
+
except ImportError:
|
|
29
|
+
return False
|
|
30
|
+
try:
|
|
31
|
+
key = winreg.OpenKey(
|
|
32
|
+
winreg.HKEY_CURRENT_USER,
|
|
33
|
+
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize",
|
|
34
|
+
)
|
|
35
|
+
subkey = winreg.QueryValueEx(key=key, name="AppsUseLightTheme")[0]
|
|
36
|
+
# subkey value is 1 when the system is using light mode
|
|
37
|
+
return subkey == 0
|
|
38
|
+
except FileNotFoundError:
|
|
39
|
+
# assume light mode when key is not found
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _linux_dark_mode() -> bool:
|
|
44
|
+
"""Detects if dark mode is enabled in a GTK3/4 based GNU/Linux system.
|
|
45
|
+
Will depend on `gsettings` being installed in the system to work properly.
|
|
46
|
+
"""
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
["gsettings", "get", "org.gnome.desktop.interface", "gtk-theme"],
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True
|
|
51
|
+
)
|
|
52
|
+
return "dark" in result.stdout.lower()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
RESOURCES_DIR = Path(Path(__file__).parent, "resources")
|
|
56
|
+
|
|
57
|
+
FONT_SIZE = 10
|
|
58
|
+
MAIN_WINDOW_SIZE = (660, 560)
|
|
59
|
+
|
|
60
|
+
NA_VALUE = "N/D"
|
|
61
|
+
MAX_ALLOWED_VALUE = 99999999
|
|
62
|
+
SMALL_FONT_SIZE = abs(FONT_SIZE * 0.8)
|
|
63
|
+
BIG_FONT_SIZE = abs(FONT_SIZE * 1.15)
|
|
64
|
+
CONTENT_WIDTH = 560
|
|
65
|
+
FORM_WIDTH = 395
|
|
66
|
+
|
|
67
|
+
ESTADOS = [
|
|
68
|
+
"AC",
|
|
69
|
+
"AL",
|
|
70
|
+
"AP",
|
|
71
|
+
"AM",
|
|
72
|
+
"BA",
|
|
73
|
+
"CE",
|
|
74
|
+
"DF",
|
|
75
|
+
"ES",
|
|
76
|
+
"GO",
|
|
77
|
+
"MA",
|
|
78
|
+
"MT",
|
|
79
|
+
"MS",
|
|
80
|
+
"MG",
|
|
81
|
+
"PA",
|
|
82
|
+
"PB",
|
|
83
|
+
"PR",
|
|
84
|
+
"PE",
|
|
85
|
+
"PI",
|
|
86
|
+
"RJ",
|
|
87
|
+
"RN",
|
|
88
|
+
"RS",
|
|
89
|
+
"RO",
|
|
90
|
+
"RR",
|
|
91
|
+
"SC",
|
|
92
|
+
"SP",
|
|
93
|
+
"SE",
|
|
94
|
+
"TO",
|
|
95
|
+
]
|