microw/microw.py

265 lines
11 KiB
Python

import sys
from enum import Enum
from pathlib import Path
import codecs
import re
from textwrap import wrap
MAN_DESCRIPTION = """
NOME
microw - convert data to MicroSIP accounts
USO
python3 microw.py [OPÇÔES]
DESCRIÇÂO
Utilitário para conversão de dados tabulares (CSV, TXT) em arquivos de
configuração (.ini) para o softphone MicroSIP e variantes.
"""
MAN_FOOTER = """
EXEMPLOS
1. Formato padrão com separador de ponto e vírgula:
python3 microw.py --delimiter ";"
2. Ignorando a 1ª coluna e formatando o nome de exibição:
python3 microw.py --columns "_ ramal nome setor" --label-pattern "nome [setor]"
3. Usando um arquivo específico e adicionando conta fantasma:
python3 microw.py --input-file lista_vendas.csv --add-ghost
CREDITOS
Desenvolvido por Lúcio Carvalho Almeida, Open Source.
Contato em luciocarvalhodev@gmail.com.
"""
ACCOUNT_TEMPLATE = r'''
[Account_]
label=$label
server=$server
proxy=$server
domain=$server
username=$ramal
password=$password
authID=$ramal
'''
GHOST_TEMPLATE = r'''
[Account_]
label=Desconectado
server=0.0.0.0
proxy=0.0.0.0
domain=0.0.0.0
username=0000
password=1234
authID=0000
'''
class Flags(Enum):
COLUMNS = "columns"
DELIMITER = "delimiter"
LABEL_PATTERN = "label-pattern"
SET_PASSWORD = "set-password"
SET_SERVER = "set-server"
ADD_GHOST = "add-ghost"
INPUT_FILE = "input-file"
OUTPUT_FILE = "output-file"
SET_TEMPLATE = "set-template"
READ_ENCODING = "read-encoding"
WRITE_ENCODING = "write-encoding"
HELP = "help"
SORT = "sort"
SORT_BY = "sort-by"
DENY_INCOMING = "deny-incoming"
AUTO_ANSWER = "auto-answer"
@classmethod
def from_str(cls, name: str):
normalized_flag_name = name.replace("-", "_").upper()
if not normalized_flag_name in cls.__members__:
error_string = f"O argumento '--{name}' não corresponde a uma flag válida."
raise ValueError(error_string)
return cls[normalized_flag_name]
def to_str(self):
return self._value_
# Quantidade de argumentos esperados
class FlagSchema(Enum):
NoArgument = 0
Argument = 1
# Os métodos dessa classe recebem instancias de Flags
class Config:
def __init__(self):
self.flags = {}
self.define_flag(flag=Flags.COLUMNS, schema=FlagSchema.Argument, default="ramal label", man="""Define a ordem das colunas no arquivo de entrada. Use nomes de variáveis (ex: ramal, password) ou '_' para ignorar uma coluna específica.""")
self.define_flag(flag=Flags.SET_PASSWORD, schema=FlagSchema.Argument, default=None, man="Quando presente determina uma única senha para ser usada por todas as contas.")
self.define_flag(flag=Flags.SET_SERVER, schema=FlagSchema.Argument, default=None, man="Quando presente determina o servidor de todas as contas.")
self.define_flag(flag=Flags.DELIMITER, schema=FlagSchema.Argument, default=",", man="""Define qual string será considerada como seprador das colunas de cada linha do input.""")
self.define_flag(flag=Flags.LABEL_PATTERN, schema=FlagSchema.Argument, default="label", man="""Template para customizar o nome de exibição. Substitui nomes de variáveis pelos seus valores.""")
self.define_flag(flag=Flags.HELP, schema=FlagSchema.NoArgument, default=False, man="""Exibe o manual.""")
self.define_flag(flag=Flags.ADD_GHOST, schema=FlagSchema.NoArgument, default=False, man="""Se presente, adiciona uma conta de 'Desconectado' como o primeiro perfil da lista.""")
self.define_flag(flag=Flags.SET_TEMPLATE, schema=FlagSchema.Argument, default=None, man="""Fornece o caminho para um arquivo que servira como template.""")
self.define_flag(flag=Flags.INPUT_FILE, schema=FlagSchema.Argument, default="./input.txt", man="""Caminho do arquivo de origem dos dados.""")
self.define_flag(flag=Flags.OUTPUT_FILE, schema=FlagSchema.Argument, default="./output.ini", man="""Caminho onde o arquivo .ini será gerado.""")
self.define_flag(flag=Flags.READ_ENCODING, schema=FlagSchema.Argument, default="utf-8", man="Codificação do arquivo lido por '--input'")
self.define_flag(flag=Flags.WRITE_ENCODING, schema=FlagSchema.Argument, default="utf-8", man="Codificação do arquivos gerados.")
self.define_flag(flag=Flags.SORT, schema=FlagSchema.NoArgument, default=False, man="""Ordena as contas no arquivo final. Caso não presente preservará a ordem das linhas do input.""")
self.define_flag(flag=Flags.SORT_BY, schema=FlagSchema.Argument, default="ramal", man="""Define qual coluna será usada para ordenação alfabética.""")
self.define_flag(flag=Flags.DENY_INCOMING, schema=FlagSchema.Argument, default="button", man="""Define se o aplicativo irá rejeitar ligações automaticamente.\nValores possíveis: all, no, server, user, button""")
self.define_flag(flag=Flags.AUTO_ANSWER, schema=FlagSchema.Argument, default="button", man="""Habilita o atendimento automático de chamadas.\nValores possíveis: all, no, button""")
def generate_flags_man(self):
res = [MAN_DESCRIPTION]
for flag in Flags:
res.append(self.flag_man(flag))
res.append(MAN_FOOTER)
return "\n".join(res)
def flag_man(self, flag: Flags):
ident = " " * 5
lines = wrap(self.flags[flag]["man"], 60)
lines[0] = (f"--{flag.to_str()}{ident}"[0:len(ident)] + lines[0])
for i in range(len(lines)-1):
lines[i+1] = ident + lines[i+1]
return "\n".join(lines)
def load_args(self, args: list[str]):
while len(args):
argument = args.pop(0)
if argument[:2] == "--":
argument = argument[2:]
flag = Flags.from_str(argument)
if self.schema(flag) == FlagSchema.Argument:
if len(args) == 0:
msg_error = f"Flag '--{argument}' exige um argumento."
raise ValueError(msg_error)
self.set(flag, codecs.decode(args.pop(0), "unicode_escape"))
else:
self.set(flag, not self.getDefault(flag))
def define_flag(self, flag: Flags, schema: FlagSchema, default, man: str):
self.flags[flag] = {
"schema": schema,
"default": default,
"man": man
}
def _validate_setting(self, setting: Flags):
if not isinstance(setting, Flags):
raise ValueError(f"'{setting}' is not a valid flag.")
return setting.value
def get(self, setting):
self._validate_setting(setting)
return self.flags[setting].get("value", self.flags[setting]["default"])
def getDefault(self, setting):
self._validate_setting(setting)
return self.flags[setting]["default"]
def set(self, setting, value):
self._validate_setting(setting)
if setting == Flags.DENY_INCOMING:
valid_values = ["all", "no", "server", "user", "button"]
if not value in valid_values:
error_msg = f"Valor '{value}' inválido para '--{setting.to_str()}'. Valores válidos: {', '.join(valid_values)}."
raise ValueError(error_msg)
if setting == Flags.AUTO_ANSWER:
valid_values = ["all", "no", "button"]
if not value in valid_values:
error_msg = f"Valor '{value}' inválido para '--{setting.to_str()}'. Valores válidos: {', '.join(valid_values)}."
raise ValueError(error_msg)
self.flags[setting]["value"] = value
def schema(self, setting):
self._validate_setting(setting)
return self.flags[setting]["schema"]
def main():
config = Config()
config.load_args(sys.argv[1:])
if config.get(Flags.HELP):
print(config.generate_flags_man())
return
output_file = Path(config.get(Flags.OUTPUT_FILE))
input_file = Path(config.get(Flags.INPUT_FILE))
if not input_file.exists():
error_msg = f"Arquivo de input especificado '{input_file.name}' não encontrado."
raise ValueError(error_msg)
input_lines = [line.strip() for line in input_file.open("r", encoding=config.get(Flags.READ_ENCODING)).readlines()]
accounts_settings = []
columns = config.get(Flags.COLUMNS).split(" ")
label_pattern = config.get(Flags.LABEL_PATTERN)
for line in input_lines:
if not line: continue
data = [field.strip() for field in line.split(config.get(Flags.DELIMITER))]
account_data = {}
# Mapeia os dados ignorando o caractere '_'
for i in range(min(len(data), len(columns))):
column_name = columns[i]
if column_name != "_":
account_data[column_name] = data[i]
# Customização do $label
formated_pattern = label_pattern
# Encontra nomes de variaveis para substituição no label_pattern
for pattern_part in re.finditer(r"[a-zA-Z]+", label_pattern):
pattern = pattern_part.group()
if pattern in columns:
formated_pattern = formated_pattern.replace(pattern, data[columns.index(pattern)])
account_data["label"] = formated_pattern
accounts_settings.append(account_data)
if config.get(Flags.SORT) : accounts_settings.sort(key=lambda account : account[config.get(Flags.SORT_BY)])
result = f"[Settings]\ndenyIncoming={config.get(Flags.DENY_INCOMING)}\nautoAnswer={config.get(Flags.AUTO_ANSWER)}\n\n"
if config.get(Flags.ADD_GHOST):
result += GHOST_TEMPLATE
current_account_template = ACCOUNT_TEMPLATE
if not config.get(Flags.SET_TEMPLATE) is None:
current_account_template = Path(config.get(Flags.SET_TEMPLATE)).read_text(encoding=config.get(Flags.READ_ENCODING))
if not config.get(Flags.SET_PASSWORD) is None:
current_account_template = current_account_template.replace("$password", config.get(Flags.SET_PASSWORD))
if not config.get(Flags.SET_SERVER) is None:
current_account_template = current_account_template.replace("$server", config.get(Flags.SET_SERVER))
for account in accounts_settings:
new_entry = current_account_template
for column_name, value in account.items():
new_entry = new_entry.replace("$" + column_name, str(value))
result += new_entry.strip() + "\n"
result = result.strip()
id = 1
while "Account_" in result:
result = result.replace("Account_", f"Account{id}", 1)
id += 1
output_file.write_text(result, encoding=config.get(Flags.WRITE_ENCODING))
print(f"Sucesso: {id-1} contas criadas em '{output_file}'.")
if __name__ == "__main__":
main()