altas mudanças
All checks were successful
API and ETL Build / build_etl (push) Successful in 30s
API and ETL Build / build_api (push) Successful in 15s

This commit is contained in:
Jose Henrique 2025-06-10 20:16:22 -03:00
parent 684a2c0630
commit 23b1f0f14e
10 changed files with 175 additions and 83 deletions

View File

@ -90,5 +90,16 @@ namespace OpenCand.Repository
return (await connection.QueryAsync<RedeSocial>(query, new { idcandidato })).AsList();
}
}
public async Task<List<CandidatoExt>?> GetCandidatoExtById(Guid idcandidato)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
var query = @"
SELECT * FROM candidato_ext
WHERE idcandidato = @idcandidato";
return (await connection.QueryAsync<CandidatoExt>(query, new { idcandidato })).AsList();
}
}
}
}

View File

@ -59,11 +59,7 @@ namespace OpenCand.API.Services
{
var result = await candidatoRepository.GetCandidatoAsync(idcandidato);
var eleicoes = await candidatoRepository.GetCandidatoMappingById(idcandidato);
foreach (var eleicao in eleicoes)
{
eleicao.Partido = await candidatoRepository.GetPartidoBySigla(eleicao.Sgpartido);
}
var candidatoExt = await candidatoRepository.GetCandidatoExtById(idcandidato);
if (result == null)
{
@ -74,10 +70,16 @@ namespace OpenCand.API.Services
throw new KeyNotFoundException($"CandidatoMapping for ID {idcandidato} not found.");
}
foreach (var eleicao in eleicoes)
{
eleicao.Partido = await candidatoRepository.GetPartidoBySigla(eleicao.Sgpartido);
}
var lastEleicao = eleicoes.OrderByDescending(e => e.Ano).First();
result.FotoUrl = $"{fotoSettings.ApiBasePath}/foto_cand{lastEleicao.Ano}_{lastEleicao.SiglaUF}_div/F{lastEleicao.SiglaUF}{lastEleicao.SqCandidato}_div.jpg";
result.Eleicoes = eleicoes.OrderByDescending(e => e.Ano).ToList();
result.CandidatoExt = candidatoExt.OrderByDescending(ce => ce.Ano).ToList();
return result;
}

View File

@ -20,14 +20,14 @@ namespace OpenCand.Core.Models
public string Sexo { get; set; }
public string EstadoCivil { get; set; }
public string Localidade { get; set; }
public string Escolaridade { get; set; }
public string Ocupacao { get; set; }
public int Ultimoano { get; set; }
public List<CandidatoMapping> Eleicoes { get; set; }
public List<CandidatoExt> CandidatoExt { get; set; }
// API ONLY
public string FotoUrl { get; set; }
}
@ -37,7 +37,6 @@ namespace OpenCand.Core.Models
public Guid IdCandidato { get; set; }
public string Cpf { get; set; }
public string Nome { get; set; }
public string Apelido { get; set; }
public string SqCandidato { get; set; }
public int Ano { get; set; }
public string Turno { get; set; }
@ -52,6 +51,17 @@ namespace OpenCand.Core.Models
public Partido? Partido { get; set; } // Nullable to allow for candidates without a party
}
public class CandidatoExt
{
public Guid IdCandidato { get; set; }
public int Ano { get; set; }
public string Apelido { get; set; }
public string Email { get; set; }
public string EstadoCivil { get; set; }
public string Escolaridade { get; set; }
public string Ocupacao { get; set; }
}
public class RedeSocial
{
public Guid IdCandidato { get; set; }

View File

@ -2,11 +2,14 @@
{
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string value)
public static bool IsNullOrEmpty(this string? value)
{
return string.IsNullOrEmpty(value) ||
value == "#NE" ||
value == "#NULO";
value == "#NULO" ||
value == "#NULO#" ||
value == "NÃO DIVULGÁVEL" ||
value == "-4";
}
}
}

View File

@ -36,10 +36,10 @@ namespace OpenCand.Parser.Models
public string Apelido { get; set; }
[Name("NR_CPF_CANDIDATO")]
public string CPFCandidato { get; set; }
public string? CPFCandidato { get; set; }
[Name("DS_EMAIL", "NM_EMAIL")]
public string Email { get; set; }
public string? Email { get; set; }
[Name("DT_NASCIMENTO")]
public string DataNascimento { get; set; }
@ -57,7 +57,7 @@ namespace OpenCand.Parser.Models
public string GrauInstrucao { get; set; }
[Name("DS_SIT_TOT_TURNO")]
public string SituacaoTurno { get; set; }
public string? SituacaoTurno { get; set; }
[Name("NR_PARTIDO")]
public int NumeroPartido { get; set; }

View File

@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
using OpenCand.Core.Models;
using OpenCand.ETL.Contracts;
using OpenCand.ETL.Extensions;
using OpenCand.Parser.Models;
using OpenCand.Services;
@ -22,62 +23,80 @@ namespace OpenCand.ETL.Parser.ParserServices
public async Task ParseObject(CandidatoCSV record)
{
if (string.IsNullOrWhiteSpace(record.CPFCandidato) || record.CPFCandidato.Length <= 3)
if (record.CPFCandidato?.Length <= 3 || record.CPFCandidato.IsNullOrEmpty())
{
record.CPFCandidato = null; // Handle null/empty/whitespace CPF
}
else
{
record.CPFCandidato = record.CPFCandidato.Trim();
}
if (record.NomeCandidato == "NÃO DIVULGÁVEL" ||
string.IsNullOrEmpty(record.NomeCandidato) ||
record.NomeCandidato == "#NULO")
if (record.Apelido.IsNullOrEmpty())
{
record.Apelido = null;
}
if (record.NomeCandidato.IsNullOrEmpty())
{
logger.LogCritical($"ParseCandidatosAsync - Candidate with id {record.SequencialCandidato} with invalid name, skipping...");
return; // Skip candidates with invalid name
}
if (string.IsNullOrWhiteSpace(record.Apelido) ||
record.Apelido.Contains("#NUL") ||
record.Apelido.Contains("NULO#") ||
record.Apelido.Contains("#NE"))
if (record.Apelido.IsNullOrEmpty())
{
record.Apelido = null;
}
if (record.SituacaoTurno.IsNullOrEmpty())
{
record.SituacaoTurno = null;
}
var candidato = new Candidato
{
Cpf = record.CPFCandidato,
SqCandidato = record.SequencialCandidato,
Nome = record.NomeCandidato,
Apelido = record.Apelido,
Email = record.Email.Contains("@") ? record.Email : null,
Sexo = record.Genero,
EstadoCivil = record.EstadoCivil,
Escolaridade = record.GrauInstrucao,
Ocupacao = record.Ocupacao,
Nome = record.NomeCandidato.Trim(),
Apelido = record.Apelido?.Trim(),
Sexo = record.Genero.Trim(),
Localidade = record.NomeUE.Trim(),
Ultimoano = record.AnoEleicao,
Eleicoes = new List<CandidatoMapping>()
{
new CandidatoMapping
{
Cpf = record.CPFCandidato,
Nome = record.NomeCandidato,
Apelido = record.Apelido,
SqCandidato = record.SequencialCandidato,
Ano = record.AnoEleicao,
Turno = record.Turno,
TipoEleicao = record.TipoAbrangencia,
NomeUE = record.NomeUE,
SiglaUF = record.SiglaUF,
Cargo = record.DescricaoCargo,
NrCandidato = record.NumeroCandidato,
Resultado = record.SituacaoTurno,
Partido = new Partido
{
Sigla = record.SiglaPartido,
Nome = record.NomePartido,
Numero = record.NumeroPartido,
}
}
}
{
new CandidatoMapping
{
Cpf = record.CPFCandidato,
Nome = record.NomeCandidato.Trim(),
SqCandidato = record.SequencialCandidato.Trim(),
Ano = record.AnoEleicao,
Turno = record.Turno.Trim(),
TipoEleicao = record.TipoAbrangencia.Trim(),
NomeUE = record.NomeUE.Trim(),
SiglaUF = record.SiglaUF.Trim(),
Cargo = record.DescricaoCargo.Trim(),
NrCandidato = record.NumeroCandidato.Trim(),
Resultado = record.SituacaoTurno?.Trim() ?? "-",
Partido = new Partido
{
Sigla = record.SiglaPartido.Trim(),
Nome = record.NomePartido.Trim(),
Numero = record.NumeroPartido,
}
}
},
CandidatoExt = new List<CandidatoExt>()
{
new CandidatoExt
{
Apelido = record.Apelido?.Trim(),
EstadoCivil = record.EstadoCivil.Trim(),
Escolaridade = record.GrauInstrucao.Trim(),
Ocupacao = record.Ocupacao.Trim(),
Ano = record.AnoEleicao,
Email = record.Email.IsNullOrEmpty() ? null : record.Email.Trim()
}
}
};
if (!string.IsNullOrEmpty(record.DataNascimento) &&

View File

@ -16,18 +16,17 @@ namespace OpenCand.Repository
using (var connection = new NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync(@"
INSERT INTO candidato (idcandidato, cpf, nome, apelido, datanascimento, email, sexo, estadocivil, escolaridade, ocupacao)
VALUES (@idcandidato, @cpf, @nome, @apelido, @datanascimento, @email, @sexo, @estadocivil, @escolaridade, @ocupacao)
INSERT INTO candidato (idcandidato, cpf, nome, apelido, datanascimento, sexo, ultimoano, localidade)
VALUES (@idcandidato, @cpf, @nome, @apelido, @datanascimento, @sexo, @ultimoano, @localidade)
ON CONFLICT (idcandidato) DO UPDATE SET
cpf = EXCLUDED.cpf,
nome = EXCLUDED.nome,
apelido = EXCLUDED.apelido,
datanascimento = EXCLUDED.datanascimento,
email = EXCLUDED.email,
sexo = EXCLUDED.sexo,
estadocivil = EXCLUDED.estadocivil,
escolaridade = EXCLUDED.escolaridade,
ocupacao = EXCLUDED.ocupacao,
apelido = EXCLUDED.apelido;",
localidade = EXCLUDED.localidade,
ultimoano = EXCLUDED.ultimoano
WHERE candidato.ultimoano IS NULL OR EXCLUDED.ultimoano > candidato.ultimoano;",
new
{
idcandidato = candidato.IdCandidato,
@ -35,11 +34,9 @@ namespace OpenCand.Repository
nome = candidato.Nome,
apelido = candidato.Apelido,
datanascimento = candidato.DataNascimento,
email = candidato.Email,
sexo = candidato.Sexo,
estadocivil = candidato.EstadoCivil,
escolaridade = candidato.Escolaridade,
ocupacao = candidato.Ocupacao
localidade = candidato.Localidade,
ultimoano = candidato.Ultimoano
});
}
}
@ -49,15 +46,14 @@ namespace OpenCand.Repository
using (var connection = new NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync(@"
INSERT INTO candidato_mapping (idcandidato, cpf, nome, apelido, sqcandidato, ano, turno, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, sgpartido, resultado)
VALUES (@idcandidato, @cpf, @nome, @apelido, @sqcandidato, @ano, @turno, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @sgpartido, @resultado)
INSERT INTO candidato_mapping (idcandidato, cpf, nome, sqcandidato, ano, turno, tipoeleicao, siglauf, nomeue, cargo, nrcandidato, sgpartido, resultado)
VALUES (@idcandidato, @cpf, @nome, @sqcandidato, @ano, @turno, @tipoeleicao, @siglauf, @nomeue, @cargo, @nrcandidato, @sgpartido, @resultado)
ON CONFLICT DO NOTHING;",
new
{
idcandidato = candidatoMapping.IdCandidato,
cpf = candidatoMapping.Cpf,
nome = candidatoMapping.Nome,
apelido = candidatoMapping.Apelido,
sqcandidato = candidatoMapping.SqCandidato,
ano = candidatoMapping.Ano,
turno = candidatoMapping.Turno,
@ -72,6 +68,32 @@ namespace OpenCand.Repository
}
}
public async Task AddCandidatoExtAsync(CandidatoExt candidatoExt)
{
using (var connection = new NpgsqlConnection(ConnectionString))
{
await connection.ExecuteAsync(@"
INSERT INTO candidato_ext (idcandidato, ano, apelido, email, estadocivil, escolaridade, ocupacao)
VALUES (@idcandidato, @ano, @apelido, @email, @estadocivil, @escolaridade, @ocupacao)
ON CONFLICT (idcandidato, ano) DO UPDATE SET
apelido = EXCLUDED.apelido,
email = EXCLUDED.email,
estadocivil = EXCLUDED.estadocivil,
escolaridade = EXCLUDED.escolaridade,
ocupacao = EXCLUDED.ocupacao;",
new
{
idcandidato = candidatoExt.IdCandidato,
ano = candidatoExt.Ano,
apelido = candidatoExt.Apelido,
email = candidatoExt.Email,
estadocivil = candidatoExt.EstadoCivil,
escolaridade = candidatoExt.Escolaridade,
ocupacao = candidatoExt.Ocupacao
});
}
}
public async Task<Candidato?> GetCandidatoByCpf(string cpf)
{
using (var connection = new NpgsqlConnection(ConnectionString))

View File

@ -17,7 +17,7 @@ namespace OpenCand.Services
public async Task AddCandidatoAsync(Candidato candidato)
{
if (candidato == null)
if (candidato == null || candidato.CandidatoExt == null || candidato.Eleicoes == null)
{
throw new ArgumentNullException(nameof(candidato), "Candidato cannot be null");
}
@ -28,6 +28,7 @@ namespace OpenCand.Services
}
var candidatoMapping = candidato.Eleicoes.First();
var candidatoExt = candidato.CandidatoExt.First();
// Add partido data
if (candidatoMapping.Partido != null)
@ -52,18 +53,18 @@ namespace OpenCand.Services
candidato.IdCandidato = existingCandidato.IdCandidato;
candidato.Cpf = GetNonEmptyString(existingCandidato.Cpf, candidato.Cpf);
candidato.Email = GetNonEmptyString(existingCandidato.Email, candidato.Email);
candidato.EstadoCivil = GetNonEmptyString(existingCandidato.EstadoCivil, candidato.EstadoCivil);
candidato.Apelido = GetNonEmptyString(existingCandidato.Apelido, candidato.Apelido);
candidato.Escolaridade = GetNonEmptyString(existingCandidato.Escolaridade, candidato.Escolaridade);
candidato.Ocupacao = GetNonEmptyString(existingCandidato.Ocupacao, candidato.Ocupacao);
candidato.Sexo = GetNonEmptyString(existingCandidato.Sexo, candidato.Sexo);
candidatoMapping.IdCandidato = candidato.IdCandidato;
candidatoMapping.Cpf = GetNonEmptyString(candidato.Cpf, candidatoMapping.Cpf);
candidatoExt.IdCandidato = candidato.IdCandidato;
// Update the entries for the existing candidate
await candidatoRepository.AddCandidatoAsync(candidato);
await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping);
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
return;
}
@ -79,10 +80,12 @@ namespace OpenCand.Services
if (existingMapping != null)
{
candidato.IdCandidato = existingMapping.IdCandidato;
candidatoExt.IdCandidato = existingMapping.IdCandidato;
candidato.Cpf = GetNonEmptyString(existingMapping.Cpf, candidato.Cpf);
candidato.Apelido = GetNonEmptyString(existingMapping.Apelido, candidato.Apelido);
await candidatoRepository.AddCandidatoAsync(candidato);
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
return;
}
@ -94,8 +97,11 @@ namespace OpenCand.Services
candidatoMapping.Cpf = candidato.Cpf;
candidatoMapping.Nome = candidato.Nome;
candidatoExt.IdCandidato = candidato.IdCandidato;
await candidatoRepository.AddCandidatoAsync(candidato);
await candidatoRepository.AddCandidatoMappingAsync(candidatoMapping);
await candidatoRepository.AddCandidatoExtAsync(candidatoExt);
}
public string GetNonEmptyString(string? value1, string? value2)

View File

@ -30,7 +30,7 @@ namespace OpenCand.Services
}
redeSocial.IdCandidato = candidato.IdCandidato;
redeSocial.Rede = GetRedeSocialType(redeSocial.Link);
redeSocial.Rede = GetRedeSocialType(redeSocial.Link.Trim());
await redeSocialRepository.AddRedeSocialAsync(redeSocial);
}

View File

@ -1,5 +1,6 @@
DROP TABLE IF EXISTS bem_candidato CASCADE;
DROP TABLE IF EXISTS candidato_mapping CASCADE;
DROP TABLE IF EXISTS candidato_ext CASCADE;
DROP TABLE IF EXISTS rede_social CASCADE;
DROP TABLE IF EXISTS candidato CASCADE;
DROP TABLE IF EXISTS partido CASCADE;
@ -10,14 +11,12 @@ CREATE TABLE candidato (
idcandidato UUID NOT NULL PRIMARY KEY,
cpf VARCHAR(11),
nome VARCHAR(255) NOT NULL,
apelido VARCHAR(255),
datanascimento TIMESTAMPTZ,
email TEXT,
sexo CHAR(15),
estadocivil VARCHAR(50),
escolaridade VARCHAR(50),
ocupacao VARCHAR(150),
popularidade BIGINT DEFAULT 0,
apelido VARCHAR(255),
localidade VARCHAR(100),
ultimoano INT,
popularidade BIGINT DEFAULT 0
);
CREATE INDEX idx_candidato_nome ON candidato (nome);
CREATE INDEX idx_candidato_apelido ON candidato (apelido);
@ -30,7 +29,6 @@ CREATE TABLE candidato_mapping (
idcandidato UUID NOT NULL,
cpf VARCHAR(11),
nome VARCHAR(255) NOT NULL,
apelido VARCHAR(255),
sqcandidato VARCHAR(50) NOT NULL,
turno VARCHAR(2) NOT NULL,
ano INT NOT NULL,
@ -44,12 +42,25 @@ CREATE TABLE candidato_mapping (
CONSTRAINT pk_candidato_mapping PRIMARY KEY (idcandidato, ano, siglauf, nomeue, cargo, nrcandidato, resultado),
CONSTRAINT fk_candidato_mapping_candidato FOREIGN KEY (idcandidato) REFERENCES candidato(idcandidato) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX idx_candidato_mapping_idcandidato ON candidato_mapping (idcandidato);
CREATE INDEX idx_candidato_mapping_cpf ON candidato_mapping (cpf);
CREATE INDEX idx_candidato_mapping_nome ON candidato_mapping (nome);
CREATE INDEX idx_candidato_mapping_apelido ON candidato_mapping (apelido);
CREATE INDEX idx_candidato_mapping_ano ON candidato_mapping (ano);
CREATE INDEX idx_candidato_mapping_sqcandidato ON candidato_mapping (sqcandidato);
CREATE TABLE candidato_ext (
idcandidato UUID NOT NULL,
ano INT NOT NULL,
apelido VARCHAR(255),
email TEXT,
estadocivil VARCHAR(50),
escolaridade VARCHAR(50),
ocupacao TEXT,
CONSTRAINT pk_candidato_ext PRIMARY KEY (idcandidato, ano)
);
CREATE INDEX idx_candidato_ext_idcandidato ON candidato_ext (idcandidato);
CREATE INDEX idx_candidato_ext_ano ON candidato_ext (ano);
---- Table for storing assets of candidates
CREATE TABLE bem_candidato (
idcandidato UUID NOT NULL,
@ -137,4 +148,12 @@ CREATE TABLE receitas_candidato (
CREATE INDEX idx_receitas_candidato_idcandidato ON receitas_candidato (idcandidato);
CREATE INDEX idx_receitas_candidato_ano ON receitas_candidato (ano);
CREATE INDEX idx_receitas_candidato_sqcandidato ON receitas_candidato (sqcandidato);
CREATE INDEX idx_receitas_candidato_sgpartido ON receitas_candidato (sgpartido);
CREATE INDEX idx_receitas_candidato_sgpartido ON receitas_candidato (sgpartido);
-- Search function
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_candidato_nome_trgm ON candidato USING GIN (nome gin_trgm_ops);
CREATE INDEX idx_candidato_apelido_trgm ON candidato USING GIN (apelido gin_trgm_ops);
CREATE INDEX idx_candidato_cpf_trgm ON candidato USING GIN (cpf gin_trgm_ops);