mtcli 3.6.0.dev1__tar.gz → 3.7.0.dev0__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.
Files changed (74) hide show
  1. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/PKG-INFO +1 -1
  2. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/cli.py +34 -34
  3. mtcli-3.7.0.dev0/mtcli/commands/conf.py +44 -0
  4. mtcli-3.7.0.dev0/mtcli/conf.py +233 -0
  5. mtcli-3.7.0.dev0/mtcli/config_registre.py +41 -0
  6. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/data/csv.py +31 -31
  7. mtcli-3.7.0.dev0/mtcli/logger.py +136 -0
  8. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/models/consecutive_bars_model.py +77 -77
  9. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/models/rates_model.py +41 -41
  10. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/models/unconsecutive_bar_model.py +67 -67
  11. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugin.py +18 -18
  12. mtcli-3.7.0.dev0/mtcli/plugin_loader.py +81 -0
  13. mtcli-3.7.0.dev0/mtcli/plugin_manager.py +27 -0
  14. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/__init__.py +5 -5
  15. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/tests/test_mm.py +13 -13
  16. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/cli.py +32 -32
  17. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/models/average_range_model.py +29 -29
  18. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/tests/test_rm.py +11 -11
  19. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/cli.py +41 -41
  20. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/models/model_average_volume.py +31 -31
  21. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/tests/test_vm.py +21 -21
  22. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/views/close_view.py +37 -37
  23. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/views/full_view.py +65 -65
  24. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/views/high_view.py +37 -37
  25. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/views/low_view.py +37 -37
  26. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/views/min_view.py +42 -42
  27. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/views/open_view.py +37 -37
  28. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/views/ranges_view.py +41 -41
  29. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/views/rates_view.py +41 -41
  30. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/pyproject.toml +1 -1
  31. mtcli-3.6.0.dev1/mtcli/conf.py +0 -199
  32. mtcli-3.6.0.dev1/mtcli/logger.py +0 -47
  33. mtcli-3.6.0.dev1/mtcli/plugin_loader.py +0 -97
  34. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/LICENSE +0 -0
  35. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/README.md +0 -0
  36. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/__init__.py +0 -0
  37. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/commands/__init__.py +0 -0
  38. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/commands/bars.py +0 -0
  39. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/conecta.py +0 -0
  40. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/data/__init__.py +0 -0
  41. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/data/base.py +0 -0
  42. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/data/mt5.py +0 -0
  43. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/database.py +0 -0
  44. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/domain/__init__.py +0 -0
  45. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/domain/timeframe.py +0 -0
  46. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/marketdata/__init__.py +0 -0
  47. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/marketdata/tick_cache.py +0 -0
  48. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/marketdata/tick_repository.py +0 -0
  49. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/models/__init__.py +0 -0
  50. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/models/bar_model.py +0 -0
  51. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/models/bars_model.py +0 -0
  52. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/models/chart_model.py +0 -0
  53. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/models/conf_model.py +0 -0
  54. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/models/signals_model.py +0 -0
  55. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/mt5_context.py +0 -0
  56. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/__init__.py +0 -0
  57. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/exemplo.py-dist +0 -0
  58. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/cli.py +0 -0
  59. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/conf.py +0 -0
  60. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/models/__init__.py +0 -0
  61. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/models/model_media_movel.py +0 -0
  62. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/tests/__init__.py +0 -0
  63. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/media_movel/tests/test_model_media_movel.py +0 -0
  64. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/__init__.py +0 -0
  65. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/conf.py +0 -0
  66. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/models/__init__.py +0 -0
  67. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/range_medio/tests/__init__.py +0 -0
  68. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/__init__.py +0 -0
  69. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/conf.py +0 -0
  70. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/models/__init__.py +0 -0
  71. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/plugins/volume_medio/tests/__init__.py +0 -0
  72. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/views/__init__.py +0 -0
  73. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/views/vars_view.py +0 -0
  74. {mtcli-3.6.0.dev1 → mtcli-3.7.0.dev0}/mtcli/views/volumes_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtcli
3
- Version: 3.6.0.dev1
3
+ Version: 3.7.0.dev0
4
4
  Summary: Aplicativo CLI para exibir gráficos do MetaTrader 5 screen reader friendly
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1,34 +1,34 @@
1
- """
2
- CLI principal do mtcli.
3
-
4
- Este módulo define o grupo principal `mt`
5
- e inicializa o carregamento de plugins.
6
- """
7
-
8
- import click
9
-
10
- from mtcli.plugin_loader import load_plugins
11
- from .commands.bars import bars
12
-
13
-
14
- @click.group(context_settings={"max_content_width": 120})
15
- @click.version_option(package_name="mtcli")
16
- def mt():
17
- """
18
- CLI principal do mtcli.
19
-
20
- Exibe gráficos e informações de mercado
21
- em formato textual compatível com leitores de tela.
22
- """
23
- pass
24
-
25
-
26
- mt.add_command(bars, name="bars")
27
-
28
-
29
- # Carrega plugins automaticamente
30
- load_plugins(mt)
31
-
32
-
33
- if __name__ == "__main__":
34
- mt()
1
+ """
2
+ CLI principal do mtcli.
3
+
4
+ Este módulo define o grupo principal `mt`
5
+ e inicializa o carregamento de plugins.
6
+ """
7
+
8
+ import click
9
+
10
+ from mtcli.plugin_loader import load_plugins
11
+ from .commands.bars import bars
12
+
13
+
14
+ @click.group(context_settings={"max_content_width": 120})
15
+ @click.version_option(package_name="mtcli")
16
+ def mt():
17
+ """
18
+ CLI principal do mtcli.
19
+
20
+ Exibe gráficos e informações de mercado
21
+ em formato textual compatível com leitores de tela.
22
+ """
23
+ pass
24
+
25
+
26
+ mt.add_command(bars, name="bars")
27
+
28
+
29
+ # Carrega plugins automaticamente
30
+ load_plugins(mt)
31
+
32
+
33
+ if __name__ == "__main__":
34
+ mt()
@@ -0,0 +1,44 @@
1
+ """
2
+ Comando para exibir configurações disponíveis.
3
+ """
4
+
5
+ import click
6
+
7
+ from mtcli.config_registry import registry
8
+ from mtcli.conf import conf
9
+
10
+
11
+ @click.command()
12
+ @click.argument("section", required=False)
13
+ def conf_cmd(section):
14
+ """
15
+ Exibe configurações disponíveis.
16
+
17
+ Exemplos:
18
+
19
+ mt conf
20
+ mt conf renko
21
+ """
22
+
23
+ if section:
24
+ options = registry.get_section(section)
25
+ else:
26
+ options = registry.get_all()
27
+
28
+ if not options:
29
+ click.echo("Nenhuma configuração registrada.")
30
+ return
31
+
32
+ for opt in options:
33
+
34
+ value = conf.get(opt.name, section=opt.section, default=opt.default)
35
+
36
+ click.echo(
37
+ f"{opt.section}.{opt.name} = {value} "
38
+ f"(default={opt.default})"
39
+ )
40
+
41
+ if opt.description:
42
+ click.echo(f" {opt.description}")
43
+
44
+ click.echo()
@@ -0,0 +1,233 @@
1
+ """
2
+ Sistema central de configuração do mtcli.
3
+
4
+ Fornece leitura de configuração a partir de:
5
+
6
+ 1. Variáveis de ambiente
7
+ 2. Arquivo mtcli.ini
8
+ 3. Valores default
9
+
10
+ Também oferece utilidades usadas por plugins como:
11
+
12
+ - descoberta do diretório MQL5/Files
13
+ - seleção da fonte de dados (CSV ou MT5)
14
+
15
+ Plugins devem acessar a configuração através do objeto global `conf`.
16
+ """
17
+
18
+ import os
19
+ import configparser
20
+
21
+ import MetaTrader5 as mt5
22
+
23
+ from mtcli.mt5_context import mt5_conexao
24
+
25
+
26
+ class Config:
27
+ """
28
+ Gerenciador central de configurações do mtcli.
29
+ """
30
+
31
+ def __init__(self, filename="mtcli.ini"):
32
+ self.config = configparser.ConfigParser()
33
+ self.config.read(filename)
34
+
35
+ # ---------------------------------------------------------
36
+ # leitura de valores
37
+ # ---------------------------------------------------------
38
+
39
+ def get(self, key, section="DEFAULT", cast=None, default=None):
40
+ """
41
+ Retorna um valor de configuração.
42
+
43
+ Prioridade:
44
+
45
+ 1. Variável de ambiente SECTION_KEY
46
+ 2. Variável de ambiente KEY
47
+ 3. mtcli.ini [section]
48
+ 4. mtcli.ini [DEFAULT]
49
+ 5. default
50
+
51
+ Args:
52
+ key (str)
53
+ section (str)
54
+ cast (type | None)
55
+ default (Any)
56
+
57
+ Returns:
58
+ Any
59
+ """
60
+
61
+ env_key = f"{section.upper()}_{key.upper()}"
62
+
63
+ value = os.getenv(env_key) or os.getenv(key.upper())
64
+
65
+ if value is None:
66
+
67
+ if self.config.has_option(section, key):
68
+ value = self.config.get(section, key)
69
+
70
+ elif self.config.has_option("DEFAULT", key):
71
+ value = self.config.get("DEFAULT", key)
72
+
73
+ else:
74
+ value = default
75
+
76
+ if cast and value is not None:
77
+
78
+ try:
79
+
80
+ if cast is bool:
81
+ value = str(value).lower() in ("1", "true", "yes")
82
+
83
+ else:
84
+ value = cast(value)
85
+
86
+ except ValueError:
87
+ value = default
88
+
89
+ return value
90
+
91
+ # ---------------------------------------------------------
92
+ # seção helper
93
+ # ---------------------------------------------------------
94
+
95
+ def section(self, section):
96
+ """
97
+ Retorna um helper para acessar uma seção específica.
98
+ """
99
+
100
+ class Section:
101
+
102
+ def __init__(self, parent, section):
103
+ self.parent = parent
104
+ self.section = section
105
+
106
+ def get(self, key, cast=None, default=None):
107
+ return self.parent.get(key, self.section, cast, default)
108
+
109
+ return Section(self, section)
110
+
111
+ # ---------------------------------------------------------
112
+ # caminho MT5
113
+ # ---------------------------------------------------------
114
+
115
+ def get_csv_path(self):
116
+ """
117
+ Retorna o caminho da pasta MQL5/Files.
118
+ """
119
+
120
+ path = self.get("mt5_pasta")
121
+
122
+ if path:
123
+ return os.path.normpath(path) + os.sep
124
+
125
+ with mt5_conexao():
126
+
127
+ info = mt5.terminal_info()
128
+
129
+ if info is None:
130
+ raise RuntimeError(
131
+ "Não foi possível obter informações do terminal MT5."
132
+ )
133
+
134
+ path = os.path.join(info.data_path, "MQL5", "Files")
135
+
136
+ return os.path.normpath(path) + os.sep
137
+
138
+ # ---------------------------------------------------------
139
+ # data source
140
+ # ---------------------------------------------------------
141
+
142
+ def get_data_source(self, source=None):
143
+ """
144
+ Retorna a fonte de dados configurada.
145
+ """
146
+
147
+ from mtcli.data import CsvDataSource, MT5DataSource
148
+
149
+ src = (source or self.get("dados", default="mt5")).lower()
150
+
151
+ if src == "csv":
152
+ return CsvDataSource()
153
+
154
+ if src == "mt5":
155
+ return MT5DataSource()
156
+
157
+ raise ValueError(f"Fonte de dados desconhecida: {src}")
158
+
159
+
160
+ # instância global usada por todo o sistema
161
+ conf = Config()
162
+
163
+
164
+ # ---------------------------------------------------------
165
+ # timeframes suportados
166
+ # ---------------------------------------------------------
167
+
168
+ _HOURS = [12, 8, 6, 4, 3, 2, 1]
169
+ _MINUTES = [30, 20, 15, 12, 10, 6, 5, 4, 3, 2, 1]
170
+
171
+ TIMEFRAMES = (
172
+ ["mn1", "w1", "d1"]
173
+ + [f"h{i}" for i in _HOURS]
174
+ + [f"m{i}" for i in _MINUTES]
175
+ )
176
+
177
+
178
+ # ---------------------------------------------------------
179
+ # Configurações gerais
180
+ # ---------------------------------------------------------
181
+
182
+ SYMBOL = conf.get("symbol", default="WIN$N")
183
+ DIGITOS = conf.get("digitos", cast=int, default=2)
184
+ PERIOD = conf.get("period", default="D1")
185
+ BARS = conf.get("count", cast=int, default=999)
186
+
187
+ VIEW = conf.get("view", default="ch")
188
+ VOLUME = conf.get("volume", default="tick")
189
+ DATE = conf.get("date", default="")
190
+
191
+ # ---------------------------------------------------------
192
+ # Configurações de leitura de candles
193
+ # ---------------------------------------------------------
194
+
195
+ LATERAL = conf.get("lateral", default="doji")
196
+ ALTA = conf.get("alta", default="verde")
197
+ BAIXA = conf.get("baixa", default="vermelho")
198
+
199
+ ROMPIMENTO_ALTA = conf.get("rompimento_alta", default="c")
200
+ ROMPIMENTO_BAIXA = conf.get("rompimento_baixa", default="v")
201
+
202
+ PERCENTUAL_ROMPIMENTO = conf.get(
203
+ "percentual_rompimento", cast=int, default=50
204
+ )
205
+
206
+ PERCENTUAL_DOJI = conf.get(
207
+ "percentual_doji", cast=int, default=10
208
+ )
209
+
210
+ # ---------------------------------------------------------
211
+ # Configurações de padrões de barra
212
+ # ---------------------------------------------------------
213
+
214
+ UP_BAR = conf.get("up_bar", default="asc")
215
+ DOWN_BAR = conf.get("down_bar", default="desc")
216
+
217
+ INSIDE_BAR = conf.get("inside_bar", default="ib")
218
+ OUTSIDE_BAR = conf.get("outside_bar", default="ob")
219
+
220
+ SOMBRA_SUPERIOR = conf.get("sombra_superior", default="top")
221
+ SOMBRA_INFERIOR = conf.get("sombra_inferior", default="bottom")
222
+
223
+ # ---------------------------------------------------------
224
+ # Fonte de dados
225
+ # ---------------------------------------------------------
226
+
227
+ DATA_SOURCE = conf.get_data_source()
228
+
229
+ # ---------------------------------------------------------
230
+ # caminho inicial do CSV (pode vir do ini/env)
231
+ # ---------------------------------------------------------
232
+
233
+ _INITIAL_CSV_PATH = conf.get_csv_path()
@@ -0,0 +1,41 @@
1
+ """
2
+ Registry de configurações do mtcli.
3
+
4
+ Plugins podem registrar suas opções de configuração aqui
5
+ para permitir descoberta automática via CLI.
6
+ """
7
+
8
+
9
+ class ConfigOption:
10
+ """
11
+ Representa uma opção de configuração registrada.
12
+ """
13
+
14
+ def __init__(self, section, name, type=str, default=None, description=""):
15
+ self.section = section
16
+ self.name = name
17
+ self.type = type
18
+ self.default = default
19
+ self.description = description
20
+
21
+
22
+ class ConfigRegistry:
23
+ """
24
+ Registro central de opções de configuração.
25
+ """
26
+
27
+ def __init__(self):
28
+ self._options = []
29
+
30
+ def register(self, section, name, type=str, default=None, description=""):
31
+ option = ConfigOption(section, name, type, default, description)
32
+ self._options.append(option)
33
+
34
+ def get_all(self):
35
+ return self._options
36
+
37
+ def get_section(self, section):
38
+ return [o for o in self._options if o.section == section]
39
+
40
+
41
+ registry = ConfigRegistry()
@@ -1,31 +1,31 @@
1
- """Módulo fonte de dados via CSV."""
2
-
3
- import csv
4
- import os
5
-
6
- from mtcli import conf
7
- from mtcli.logger import setup_logger
8
-
9
- from .base import DataSourceBase
10
-
11
- logger = setup_logger()
12
-
13
-
14
- class CsvDataSource(DataSourceBase):
15
- """Fonte de dados via CSV."""
16
-
17
- def get_data(self, symbol, period, count=100):
18
- """Retorna dados CSV em uma lista de lista."""
19
- file_path = os.path.join(conf._INITIAL_CSV_PATH, f"{symbol}{period}.csv")
20
- logger.info(f"Iniciando coleta de dados via CSV: {file_path}.")
21
- csv_data = []
22
- try:
23
- with open(file_path, encoding="utf-16", newline="") as f:
24
- lines = csv.reader(f, delimiter=",", quotechar="'")
25
- for line in lines:
26
- csv_data.append(line)
27
- except:
28
- logger.warning(f"Arquivo {file_path} não encontrado.")
29
- print("Arquivo %s nao encontrado! Tente novamente" % file_path)
30
- logger.info("Coleta de dados via CSV finalizada.")
31
- return csv_data
1
+ """Módulo fonte de dados via CSV."""
2
+
3
+ import csv
4
+ import os
5
+
6
+ from mtcli import conf
7
+ from mtcli.logger import setup_logger
8
+
9
+ from .base import DataSourceBase
10
+
11
+ logger = setup_logger()
12
+
13
+
14
+ class CsvDataSource(DataSourceBase):
15
+ """Fonte de dados via CSV."""
16
+
17
+ def get_data(self, symbol, period, count=100):
18
+ """Retorna dados CSV em uma lista de lista."""
19
+ file_path = os.path.join(conf._INITIAL_CSV_PATH, f"{symbol}{period}.csv")
20
+ logger.info(f"Iniciando coleta de dados via CSV: {file_path}.")
21
+ csv_data = []
22
+ try:
23
+ with open(file_path, encoding="utf-16", newline="") as f:
24
+ lines = csv.reader(f, delimiter=",", quotechar="'")
25
+ for line in lines:
26
+ csv_data.append(line)
27
+ except:
28
+ logger.warning(f"Arquivo {file_path} não encontrado.")
29
+ print("Arquivo %s nao encontrado! Tente novamente" % file_path)
30
+ logger.info("Coleta de dados via CSV finalizada.")
31
+ return csv_data
@@ -0,0 +1,136 @@
1
+ """
2
+ Sistema central de logging do mtcli.
3
+
4
+ Este módulo fornece uma função única `setup_logger()` utilizada por todo
5
+ o ecossistema de plugins do mtcli para configurar logging consistente.
6
+
7
+ Características principais
8
+ --------------------------
9
+
10
+ ✔ Arquivo de log rotativo em:
11
+ %APPDATA%/mtcli/logs/mtcli.log
12
+
13
+ ✔ Rotação automática:
14
+ - tamanho máximo: 2 MB
15
+ - até 3 arquivos de backup
16
+
17
+ ✔ Proteção contra duplicação de handlers quando plugins
18
+ inicializam o logger múltiplas vezes.
19
+
20
+ ✔ Encoding UTF-8 garantido (evita problemas de acentuação
21
+ no Windows).
22
+
23
+ ✔ Compatível com pytest (caplog).
24
+
25
+ Observação
26
+ ----------
27
+
28
+ Os logs **não são exibidos no console**.
29
+ Toda saída é direcionada exclusivamente para o arquivo de log.
30
+ """
31
+
32
+ import logging
33
+ from logging.handlers import RotatingFileHandler
34
+ import os
35
+
36
+
37
+ # ==========================================================
38
+ # DIRETÓRIO DE LOG
39
+ # ==========================================================
40
+
41
+ base_dir = os.getenv("APPDATA", os.path.expanduser("~"))
42
+
43
+ LOG_DIR = os.path.join(base_dir, "mtcli", "logs")
44
+
45
+ os.makedirs(LOG_DIR, exist_ok=True)
46
+
47
+ LOG_FILE = os.path.join(LOG_DIR, "mtcli.log")
48
+
49
+
50
+ # ==========================================================
51
+ # LOGGER SETUP
52
+ # ==========================================================
53
+
54
+ def setup_logger(name: str = "mtcli") -> logging.Logger:
55
+ """
56
+ Cria ou retorna um logger configurado para o mtcli.
57
+
58
+ O logger utiliza **apenas um handler de arquivo rotativo**.
59
+ Nenhuma saída é enviada ao console.
60
+
61
+ A função é **idempotente**, ou seja, pode ser chamada
62
+ múltiplas vezes sem duplicar handlers.
63
+
64
+ Parameters
65
+ ----------
66
+ name : str
67
+ Nome do logger (normalmente `__name__`).
68
+
69
+ Returns
70
+ -------
71
+ logging.Logger
72
+ Instância configurada do logger.
73
+ """
74
+
75
+ logger = logging.getLogger(name)
76
+
77
+ logger.setLevel(logging.DEBUG)
78
+
79
+ formatter = logging.Formatter(
80
+ "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
81
+ datefmt="%Y-%m-%d %H:%M:%S",
82
+ )
83
+
84
+ # ======================================================
85
+ # REMOVE STREAM HANDLERS (garante silêncio no console)
86
+ # ======================================================
87
+
88
+ for handler in list(logger.handlers):
89
+ if isinstance(handler, logging.StreamHandler) and not isinstance(handler, RotatingFileHandler):
90
+ logger.removeHandler(handler)
91
+
92
+ # ======================================================
93
+ # FILE HANDLER ROTATIVO
94
+ # ======================================================
95
+
96
+ file_handler_exists = any(
97
+ isinstance(h, RotatingFileHandler) for h in logger.handlers
98
+ )
99
+
100
+ if not file_handler_exists:
101
+
102
+ file_handler = RotatingFileHandler(
103
+ LOG_FILE,
104
+ maxBytes=2_000_000,
105
+ backupCount=3,
106
+ encoding="utf-8",
107
+ delay=True,
108
+ )
109
+
110
+ file_handler.setFormatter(formatter)
111
+
112
+ logger.addHandler(file_handler)
113
+
114
+ # ======================================================
115
+ # PROPAGATION
116
+ # ======================================================
117
+
118
+ # Permite que pytest caplog capture logs
119
+ logger.propagate = True
120
+
121
+ return logger
122
+
123
+
124
+ # ==========================================================
125
+ # LOGGER PADRÃO DO MTCLI
126
+ # ==========================================================
127
+
128
+ """
129
+ Logger padrão utilizado por módulos internos do mtcli.
130
+
131
+ Plugins geralmente criam seu próprio logger usando:
132
+
133
+ setup_logger(__name__)
134
+ """
135
+
136
+ log = setup_logger()