diff --git a/.github/TEMPLATES/COMMIT_TEMPLATE.md b/.github/TEMPLATES/COMMIT_TEMPLATE.md new file mode 100644 index 00000000..90f6168a --- /dev/null +++ b/.github/TEMPLATES/COMMIT_TEMPLATE.md @@ -0,0 +1,68 @@ +# Modelo de Mensagem de Commit (Conventional Commits) + +Este documento serve como um guia rápido para a criação de mensagens de commit padronizadas. O uso deste formato é essencial para manter o histórico do projeto legível, facilitar a automação e gerar changelogs de forma automática. + +## Estrutura Principal + +Cada mensagem de commit consiste em um cabeçalho, um corpo opcional e um rodapé opcional. A estrutura é a seguinte: + +```md +[escopo opcional]: + +[corpo opcional] + +[rodapé(s) opcional(is)] +``` + +- **tipo**: Obrigatório. Define a categoria da mudança (ex: `feat`, `fix`, `docs`). +- **escopo**: Opcional. Especifica a parte do código que foi alterada (ex: `api`, `parser`, `database`). +- **descrição**: Obrigatório. Um resumo conciso da mudança, em letras minúsculas e sem ponto final. +- **corpo**: Opcional. Fornece mais contexto, explicando o "porquê" da mudança. Separado da descrição por uma linha em branco. +- **rodapé**: Opcional. Usado para referenciar issues (ex: `Refs: #42`) ou para declarar _breaking changes_ (ex: `BREAKING CHANGE:...`). + +## Tipos de Commit Recomendados + +| Tipo | Descrição | +| :--------- | :----------------------------------------------------------------------------- | +| `feat` | Introduz uma nova funcionalidade ou capacidade. | +| `fix` | Corrige um bug ou erro no código. | +| `docs` | Alterações relacionadas exclusivamente à documentação. | +| `refactor` | Alterações no código que não corrigem um bug nem adicionam uma funcionalidade. | +| `perf` | Uma mudança de código que melhora o desempenho. | +| `test` | Adição ou correção de testes automatizados. | +| `build` | Mudanças que afetam o sistema de build ou dependências externas. | +| `ci` | Mudanças nos arquivos e scripts de configuração de Integração Contínua (CI). | +| `chore` | Outras mudanças que não modificam o código-fonte ou os testes. | +| `style` | Mudanças de estilo de código que não afetam a lógica (formatação, etc.). | + +### Exemplos Práticos + +**1. Commit de correção de bug (fix):** + +`fix: corrige cálculo de offset na paginação da API` + +**2. Commit de nova funcionalidade (feat) com escopo:** + +```md +feat(parser): adiciona suporte para o formato de dados do TSE + +Refs: #45 +``` + +**3. Commit com corpo para mais detalhes:** + +```md +perf(database): otimiza query para busca de metadados + +A query anterior utilizava um JOIN desnecessário que causava lentidão +em datasets com mais de 10.000 registros. Esta mudança simplifica +a consulta e adiciona um índice na coluna de metadados. +``` + +**4. Commit que fecha uma issue do GitHub:** + +```md +fix(ui): resolve problema de renderização de tabelas no Firefox + +Closes: #78 +``` diff --git a/.github/actions/setup-poetry/action.yml b/.github/actions/setup-poetry/action.yml new file mode 100644 index 00000000..779ee67a --- /dev/null +++ b/.github/actions/setup-poetry/action.yml @@ -0,0 +1,38 @@ +name: Setup Poetry +description: Install Python, cache & install deps (dev) +runs: + using: composite + steps: + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python_version }} + + - name: Cache Poetry + uses: actions/cache@v4 + with: + path: | + ${{ inputs.poetry_cache_dir }} + .venv + key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} + restore-keys: | + ${{ runner.os }}-poetry- + + - name: Install Poetry + deps + shell: bash + run: | + pip install poetry==${{ inputs.poetry_version }} + poetry config virtualenvs.in-project true + poetry install --no-root --with dev --no-interaction +inputs: + python_version: + description: Python version + required: true + default: "3.11" + poetry_version: + description: Poetry version + required: true + default: "1.8.5" + poetry_cache_dir: + description: Poetry cache dir + required: true + default: "~/.cache/poetry" diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 00000000..da1764a7 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,202 @@ +name: Python CI/CD + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + PYTHON_VERSION: "3.11" + POETRY_VERSION: "1.8.5" + POETRY_VIRTUALENVS_IN_PROJECT: "true" + POETRY_CACHE_DIR: "/home/runner/.cache/poetry" + DBT_PROJECT_DIR: "${{ github.workspace }}/airflow_lappis/dags/dbt/ipea" + + IMAGE_REGISTRY_OWNER: govhub-br + IMAGE_NAME: ghcr.io/govhub-br/airflow-ipea + IMAGE_TAG_SHA: ${{ github.sha }} + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-poetry + with: + python_version: ${{ env.PYTHON_VERSION }} + poetry_version: ${{ env.POETRY_VERSION }} + poetry_cache_dir: ${{ env.POETRY_CACHE_DIR }} + + - name: Run lint + run: poetry run make lint-ci || true + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-poetry + with: + python_version: ${{ env.PYTHON_VERSION }} + poetry_version: ${{ env.POETRY_VERSION }} + poetry_cache_dir: ${{ env.POETRY_CACHE_DIR }} + + - name: Run tests + run: > + poetry run pytest tests + --junitxml=report.xml + --cov=. --cov-report=xml:coverage.xml + + - uses: actions/upload-artifact@v4 + with: + name: reports + path: | + report.xml + coverage.xml + + docker_build: + name: Docker build + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + + - name: Build + uses: docker/build-push-action@v5 + with: + push: false + context: . + cache-from: type=gha + cache-to: type=gha,mode=max + tags: | + ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG_SHA }} + + docker_push: + name: Docker push + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: docker_build + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + + - name: Login GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build & Push + uses: docker/build-push-action@v5 + with: + push: true + context: . + cache-from: type=gha + cache-to: type=gha,mode=max + tags: | + ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG_SHA }} + ${{ env.IMAGE_NAME }}:latest + + dbt_docs: + name: DBT Docs (deploy) + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: docker_build + permissions: + contents: read + pages: write + id-token: write + steps: + - name: Check VPN secrets + env: + CLIENT_OVPN: ${{ secrets.CLIENT_OVPN }} + run: | + if [ -z "$CLIENT_OVPN" ]; then + echo "VPN não configurada — pulando." + exit 0 + fi + + - uses: actions/checkout@v4 + + - name: Instalar deps (DBT + VPN) + run: | + sudo apt-get update + sudo apt-get -y install openvpn iputils-ping + pip install dbt-core dbt-postgres + + - name: Conectar VPN + env: + CLIENT_OVPN: ${{ secrets.CLIENT_OVPN }} + VPN_P12: ${{ secrets.VPN_P12 }} + VPN_USER: ${{ secrets.VPN_USER }} + VPN_PWD: ${{ secrets.VPN_PWD }} + run: | + sudo mkdir -p /etc/openvpn + echo "$CLIENT_OVPN" | sudo tee /etc/openvpn/client.ovpn >/dev/null + + echo "$VPN_USER" | sudo tee /etc/openvpn/cred.txt >/dev/null + echo "$VPN_PWD" | sudo tee -a /etc/openvpn/cred.txt >/dev/null + + echo "$VPN_P12" | base64 --decode | sudo tee /etc/openvpn/auth.p12 >/dev/null + sudo chmod 600 /etc/openvpn/* + + sudo rm -f /tmp/ovpn.log || true + sudo touch /tmp/ovpn.log + sudo chmod 644 /tmp/ovpn.log + + sudo openvpn \ + --config /etc/openvpn/client.ovpn \ + --auth-user-pass /etc/openvpn/cred.txt \ + --pkcs12 /etc/openvpn/auth.p12 \ + --verb 3 --daemon --log /tmp/ovpn.log + + timeout=60 + until sudo grep -q 'Initialization Sequence Completed' /tmp/ovpn.log; do + sleep 1 + (( timeout-- )) + if (( timeout == 0 )); then + echo "❌ VPN não inicializou em 60s" >&2 + sudo tail -n 200 /tmp/ovpn.log || true + exit 1 + fi + done + + - name: Gerar DBT docs + env: + DB_DW_HOST: ${{ secrets.DB_DW_HOST }} + DB_DW_DBNAME: ${{ secrets.DB_DW_DBNAME }} + DB_DW_USER: ${{ secrets.DB_DW_USER }} + DB_DW_PASSWORD: ${{ secrets.DB_DW_PASSWORD }} + run: | + cd "${{ env.DBT_PROJECT_DIR }}" + dbt deps + dbt docs generate + mkdir -p public + mv target/* public/ + + - name: Finalizar VPN + if: always() + run: sudo pkill openvpn || true + + - uses: actions/upload-pages-artifact@v3 + with: + path: ${{ env.DBT_PROJECT_DIR }}/public + + - uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 4ffde2ce..cdba53cd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ dist/ **/*.lock __pycache__/ .poetry/ +requirements.generated.txt # Cache directories .cache/ @@ -41,8 +42,14 @@ dbt_packages/ logs/ # Jupyter -.ipynb_checkpoints/ +**/*.ipynb_checkpoints/ +**/*.ipynb # System files .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db + +# VPN +/connect.sh +*.ovpn +*.p12 \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index ccfd75c0..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,65 +0,0 @@ -image: python:3.11-slim - -variables: - POETRY_HOME: "/opt/poetry" - POETRY_VERSION: "1.8.5" - POETRY_VIRTUALENVS_IN_PROJECT: "true" - POETRY_CACHE_DIR: "$CI_PROJECT_DIR/.cache/poetry" - PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" - DBT_PROJECT_DIR: "${CI_PROJECT_DIR}/plugins/dbt" - GIT_CI_USER: "ci bot" - GIT_CI_EMAIL: "ci.lappis.rocks@gmail.com" - -cache: - paths: - - .cache/poetry - - .cache/pip - - .venv - -stages: - - lint - - test - -.install-poetry: - before_script: - - apt-get update - - apt-get -y install make - - pip install poetry==${POETRY_VERSION} - - poetry install --no-root --with dev - - source $(poetry env info --path)/bin/activate - -lint: - stage: lint - extends: .install-poetry - script: - - make lint - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - -test: - stage: test - extends: .install-poetry - script: - - poetry run pytest tests --junitxml=report.xml - coverage: '/TOTAL.*?(\d+%)$/' - artifacts: - reports: - coverage_report: - coverage_format: cobertura - path: coverage.xml - junit: report.xml - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - -scan: - stage: test - image: sonarsource/sonar-scanner-cli:latest - script: - - sonar-scanner -Dsonar.projectKey=$SONAR_PROJECT_KEY -Dsonar.organization=$SONAR_ORG -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=$SONAR_TOKEN - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - changes: - - dags/** - - plugins/** - - src/** - - tests/** diff --git a/.sqlfluff.ci b/.sqlfluff.ci new file mode 100644 index 00000000..384a2e4d --- /dev/null +++ b/.sqlfluff.ci @@ -0,0 +1,9 @@ +[sqlfluff] +dialect = postgres +templater = jinja + +[sqlfluff:templater:jinja] +apply_dbt_builtins = True + +[sqlfluff:templater:jinja:context] +target_name = "ci" diff --git a/.sqlfluffignore b/.sqlfluffignore new file mode 100644 index 00000000..3c8cce41 --- /dev/null +++ b/.sqlfluffignore @@ -0,0 +1,2 @@ +**/target/**/*.sql +**/compiled/**/*.sql diff --git a/.user.yml b/.user.yml new file mode 100644 index 00000000..78630811 --- /dev/null +++ b/.user.yml @@ -0,0 +1 @@ +id: ad35bb68-68d6-445c-a5ef-dea5f0fe9986 diff --git a/Dockerfile b/Dockerfile index a1d8b92f..7c38ad83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,13 +30,11 @@ RUN apt-get update \ && sed -i 's/^# pt_BR.UTF-8 UTF-8$/pt_BR.UTF-8 UTF-8/g' /etc/locale.gen \ && locale-gen en_US.UTF-8 pt_BR.UTF-8 \ && update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 + USER airflow +WORKDIR ${AIRFLOW_HOME} # Para rodar o airflow só precisamos instalar as dependências visto que o código -# sempre será sincronizado via git sync ou via volumes localmente -RUN pip install --no-cache-dir poetry -WORKDIR /opt -COPY poetry.lock pyproject.toml ./ -RUN poetry install --no-root - -WORKDIR ${AIRFLOW_HOME} +# sempre será sincronizado via git sync ou via volumes localmente +COPY requirements.txt . +RUN pip install -r requirements.txt diff --git a/Dockerfile.superset b/Dockerfile.superset new file mode 100644 index 00000000..3a301ac0 --- /dev/null +++ b/Dockerfile.superset @@ -0,0 +1,12 @@ +FROM apache/superset:latest + +USER root + +# Install PostgreSQL driver directly in the virtual environment site-packages +# This ensures the driver is available when Superset tries to connect to PostgreSQL +RUN /usr/local/bin/python -m pip install psycopg2-binary --target /app/.venv/lib/python3.10/site-packages/ + +# Install other useful database drivers for future use +RUN /usr/local/bin/python -m pip install pymysql --target /app/.venv/lib/python3.10/site-packages/ + +USER superset \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..2fa07ff7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Gov Hub + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index 6e7a0c3c..0a6d0194 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,71 @@ +export PYTHONPATH := $(CURDIR)/airflow_lappis +export MYPYPATH := $(CURDIR):$(CURDIR)/airflow_lappis/dags:$(CURDIR)/airflow_lappis/helpers:$(CURDIR)/airflow_lappis/plugins + +AIRFLOW_SERVICE ?= airflow +AIRFLOW_LOCAL_ORGAO ?= ipea +AIRFLOW_LOCAL_DB_HOST ?= postgres +AIRFLOW_LOCAL_DB_NAME ?= postgres +AIRFLOW_LOCAL_DB_USER ?= postgres +AIRFLOW_LOCAL_DB_PASSWORD ?= postgres +AIRFLOW_LOCAL_DB_PORT ?= 5432 + setup: - pip install poetry==1.8.5 + @if ! command -v poetry >/dev/null 2>&1; then \ + echo "Poetry não encontrado. Instale antes com pipx install poetry==1.8.5"; \ + exit 1; \ + fi + @if [ ! -f .env ]; then \ + if [ -f local.env ]; then \ + cp local.env .env; \ + echo ".env criado a partir de local.env"; \ + else \ + echo "local.env não encontrado. Crie o .env manualmente."; \ + exit 1; \ + fi; \ + fi + poetry self add poetry-plugin-export || true poetry config virtualenvs.in-project false - poetry config warnings.export false poetry lock poetry install --no-root --with dev - poetry export --without-hashes --format=requirements.txt > requirements.txt + poetry export --without-hashes --format=requirements.txt > requirements.generated.txt bash setup-git-hooks.sh + docker compose up -d --build + $(MAKE) dev + $(MAKE) dev-check format: poetry run black . poetry run ruff check --fix . - poetry run sqlfmt ./src/dbt - poetry run sqlfluff fix ./src/dbt + poetry run sqlfmt ./airflow_lappis/dags/dbt lint: poetry run black . --check poetry run ruff check . - poetry run mypy . - poetry run sqlfmt ./src/dbt --check - poetry run sqlfluff lint ./src/dbt + poetry run mypy . --explicit-package-bases --install-types --non-interactive + poetry run sqlfmt ./airflow_lappis/dags/dbt --check + [ "${GITLAB_CI}" ] || poetry run sqlfluff lint ./airflow_lappis/dags/dbt + +lint-ci: + poetry run sqlfmt ./airflow_lappis/dags/dbt --check + poetry run sqlfluff lint ./airflow_lappis/dags/dbt --config .sqlfluff.ci --ignore templating test: poetry run pytest tests + +dev: + @docker compose ps --status running $(AIRFLOW_SERVICE) >/dev/null 2>&1 || (echo "Serviço '$(AIRFLOW_SERVICE)' não está em execução. Rode: docker compose up -d" && exit 1) + @echo "Aguardando Airflow/DB ficarem prontos..." + @docker compose exec -T $(AIRFLOW_SERVICE) sh -c 'for i in $$(seq 1 30); do airflow db init >/dev/null 2>&1 && exit 0; sleep 2; done; echo "Airflow DB não ficou pronto a tempo para inicializar."; exit 1' + @docker compose exec -T $(AIRFLOW_SERVICE) airflow variables set airflow_orgao '$(AIRFLOW_LOCAL_ORGAO)' + @docker compose exec -T $(AIRFLOW_SERVICE) airflow variables set airflow_variables '{"ipea":{"codigos_ug":[113601,113602]},"unb":{"codigos_ug":[154040]},"ibama":{"codigos_ug":[440001,440048,440050]},"mgi":{"codigos_ug":[201082]}}' + @docker compose exec -T $(AIRFLOW_SERVICE) airflow variables set dynamic_schedules '{"empenhos_tesouro_ingest_dag":{"type":"cron","value":"0 13 * * 1-6"},"nc_tesouro_ingest_dag":{"type":"cron","value":"0 13 * * 1-6"},"pf_tesouro_ingest_dag":{"type":"cron","value":"0 13 * * 1-6"},"visao_orcamentaria_ingest":{"type":"cron","value":"0 13 * * 1-6"}}' + @docker compose exec -T $(AIRFLOW_SERVICE) sh -c "printf '%s\n' '{\"postgres_default\":{\"conn_type\":\"postgres\",\"host\":\"$(AIRFLOW_LOCAL_DB_HOST)\",\"schema\":\"$(AIRFLOW_LOCAL_DB_NAME)\",\"login\":\"$(AIRFLOW_LOCAL_DB_USER)\",\"password\":\"$(AIRFLOW_LOCAL_DB_PASSWORD)\",\"port\":$(AIRFLOW_LOCAL_DB_PORT)}}' > /tmp/airflow-connections.json && airflow connections import --overwrite /tmp/airflow-connections.json && rm -f /tmp/airflow-connections.json" + @echo "Ambiente local do Airflow configurado com sucesso." + +dev-check: + @docker compose ps --status running $(AIRFLOW_SERVICE) >/dev/null 2>&1 || (echo "Serviço '$(AIRFLOW_SERVICE)' não está em execução. Rode: docker compose up -d" && exit 1) + @docker compose exec -T $(AIRFLOW_SERVICE) airflow variables get airflow_orgao >/dev/null + @docker compose exec -T $(AIRFLOW_SERVICE) airflow variables get airflow_variables >/dev/null + @docker compose exec -T $(AIRFLOW_SERVICE) airflow variables get dynamic_schedules >/dev/null + @docker compose exec -T $(AIRFLOW_SERVICE) airflow connections get postgres_default >/dev/null + @echo "Validação concluída: variables e connection do Airflow estão configuradas." diff --git a/README.md b/README.md index c9f949e8..deb627b7 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,117 @@ +# Gov Hub BR - Transformando Dados em Valor para Gestão Pública + +O Gov Hub BR é uma iniciativa para enfrentar os desafios da fragmentação, redundância e inconsistências nos sistemas estruturantes do governo federal. O projeto busca transformar dados públicos em ativos estratégicos, promovendo eficiência administrativa, transparência e melhor tomada de decisão. A partir da integração de dados, gestores públicos terão acesso a informações qualificadas para subsidiar decisões mais assertivas, reduzir custos operacionais e otimizar processos internos. + +Potencializamos informações de sistemas como TransfereGov, Siape, Siafi, ComprasGov e Siorg para gerar diagnósticos estratégicos, indicadores confiáveis e decisões baseadas em evidências. + +![Informações do Projeto](https://github.com/GovHub-br/gov-hub/blob/main/docs/land/dist/images/imagem_informacoes.jpg) + +- Transparência pública e cultura de dados abertos +- Indicadores confiáveis para acompanhamento e monitoramento +- Decisões baseadas em evidências e diagnósticos estratégicos +- Exploração de inteligência artificial para gerar insights +- Gestão orientada a dados em todos os níveis + +## Fluxo/Arquitetura de Dados + +A arquitetura do Gov Hub BR é baseada na Arquitetura Medallion, em um fluxo de dados que permite a coleta, transformação e visualização de dados. + +![Fluxo de Dados](https://github.com/GovHub-br/gov-hub/blob/main/fluxo_dados.jpg) + +Para mais informações sobre o projeto, veja o nosso [e-book](https://github.com/GovHub-br/gov-hub/blob/main/docs/land/dist/ebook/GovHub_Livro-digital_0905.pdf). +E temos também alguns slides falando do projeto e como ele pode ajudar a transformar a gestão pública. + +[Slides](https://www.figma.com/slides/PlubQE0gaiBBwFAV5GcVlH/Gov-Hub---F%C3%B3rum-IA---Giga-candanga?node-id=5-131&t=hlLiJiwfyPEPRFys-1) + +## Apoio + +Esse trabalho é mantido pelo [Lab Livre](https://www.instagram.com/lab.livre/) e apoiado pelo [IPEA/Dides](https://www.ipea.gov.br/portal/categorias/72-estrutura-organizacional/210-dides-estrutura-organizacional). + +## Contato + +Para dúvidas, sugestões ou para contribuir com o projeto, entre em contato conosco: [lablivreunb@gmail.com](mailto:lablivreunb@gmail.com) + + # Data Pipeline Project -This project implements a modern data stack using Airflow, dbt, Jupyter, and Superset for data orchestration, transformation, analysis, and visualization. +O Data Pipeline Project é uma solução moderna que utiliza ferramentas como Airflow, DBT, Jupyter e Superset para orquestração, transformação, análise e visualização de dados. -## 🚀 Stack Components +## 🚀 Stack do projeto -- **Apache Airflow**: Workflow orchestration -- **dbt**: Data transformation -- **Jupyter**: Interactive data analysis -- **Apache Superset**: Data visualization and exploration -- **Docker**: Containerization and local development -- **Make**: Build automation and setup +- **Apache Airflow**: Orquestração de workflows +- **dbt**: Transformação de dados +- **Jupyter**: Análise de dados interativa +- **Apache Superset**: Visualização e exploração de dados +- **Docker**: Containerização e desenvolvimento local +- **Make**: Automação de build e configuração -## 📋 Prerequisites +## 📋 Pré-requisitos -- Docker and Docker Compose +- Docker e Docker Compose - Make -- Python 3.x +- Python 3.11.x - Git ## 🔧 Setup -1. Clone the repository: +1. Clone o repositório: ```bash -git clone git@gitlab.com:lappis-unb/gest-odadosipea/app-lappis-ipea.git -cd app-lappis-ipea +git clone git@github.com:GovHub-br/data-application-gov-hub.git +cd data-application-gov-hub ``` -2. Run the setup using Make: +2. Execute a configuração usando Make: ```bash make setup ``` -This will: -- Create necessary virtual environments -- Install dependencies -- Set up pre-commit hooks -- Configure development environment +- Isso irá: + - Criar os ambientes virtuais necessários + - Instalar as dependências + - Configurar os hooks de pre-commit + - Preparar o ambiente de desenvolvimento + + +3. Configuração de ambiente + +Este projeto depende de variáveis de ambiente para o desenvolvimento local. + +Você pode configurá-las seguindo **[este guia](https://gov-hub.io/govhub/documentacao/instalacao/)**. + -## 🏃‍♂️ Running Locally +## 🏃‍♂️ Executando localmente -Start all services using Docker Compose: +Inicie todos os serviços usando Docker Compose: ```bash docker-compose up -d ``` -Access the different components: +Acesse os diferentes componentes: - Airflow: http://localhost:8080 - Jupyter: http://localhost:8888 - Superset: http://localhost:8088 -## 💻 Development +## 💻 Desenvolvimento -### Code Quality +### Qualidade de Código -This project uses several tools to maintain code quality: -- Pre-commit hooks -- Linting configurations -- Automated testing +Este projeto utiliza diversas ferramentas para manter a qualidade do código: +- Hooks de pre-commit +- Configurações de lint +- Testes automatizados -Run linting checks: +Execute a verificação de lint: ```bash make lint ``` -Run tests: +Execute os testes: ```bash make test ``` -### Project Structure +### Estrutura do projeto ``` . @@ -87,39 +129,40 @@ make test └── README.md ``` -### Makefile Commands +### Comandos do Makefile -- `make setup`: Initial project setup -- `make lint`: Run linting checks -- `make tests`: Run test suite -- `make clean`: Clean up generated files -- `make build`: Build Docker images +- `make setup`: Configuração inicial do projeto +- `make lint`: Executa verificações de lint +- `make tests`: Executa a suíte de testes +- `make clean`: Remove arquivos gerados +- `make build`: Constrói as imagens Docker -## 🔐 Git Workflow +## 🔐 Fluxo de Trabalho com Git -This project requires signed commits. To set up GPG signing: +Este projeto exige commits assinados. Para configurar a assinatura com GPG: -1. Generate a GPG key: +1. Gere uma chave GPG: ```bash gpg --full-generate-key ``` -2. Configure Git to use GPG signing: +2. Configure o Git para usar assinatura GPG: ```bash -git config --global user.signingkey YOUR_KEY_ID +git config --global user.signingkey SUA_KEY_ID git config --global commit.gpgsign true ``` -3. Add your GPG key to your GitLab account +3. Adicione sua chave GPG à sua conta do GitLab -## 📚 Documentation +## 📚 Documentação -- [Airflow Documentation](https://airflow.apache.org/docs/) -- [dbt Documentation](https://docs.getdbt.com/) -- [Superset Documentation](https://superset.apache.org/docs/intro) +- [Documentação do Airflow](https://airflow.apache.org/docs/) +- [Documentação do dbt](https://docs.getdbt.com/) +- [Documentação do Superset](https://superset.apache.org/docs/intro) +- [Documentação do GovHub](https://gov-hub.io/govhub/documentacao/instalacao/) -## 🤝 Contributing +## 🤝 Contribuição -1. Create a new branch for your feature -2. Make changes and ensure all tests pass -3. Submit a merge request +1. Crie uma nova branch para sua feature +2. Faça as alterações e garanta que todos os testes passam +3. Envie um merge request \ No newline at end of file diff --git a/airflow_lappis/airflow.cfg b/airflow_lappis/airflow.cfg new file mode 100644 index 00000000..081853c1 --- /dev/null +++ b/airflow_lappis/airflow.cfg @@ -0,0 +1,6 @@ +[core] +dags_folder = /opt/airflow/dags +plugins_folder = /opt/airflow/plugins + +[logging] +extra_path = /opt/airflow/helpers diff --git a/airflow_lappis/dags/dashboards/__init__.py b/airflow_lappis/dags/dashboards/__init__.py new file mode 100644 index 00000000..da6b0a9a --- /dev/null +++ b/airflow_lappis/dags/dashboards/__init__.py @@ -0,0 +1,3 @@ +""" +Módulo de DAGs para geração de dashboards. +""" diff --git a/airflow_lappis/dags/dashboards/dashboard_servidores_dag.py b/airflow_lappis/dags/dashboards/dashboard_servidores_dag.py new file mode 100644 index 00000000..d79f1578 --- /dev/null +++ b/airflow_lappis/dags/dashboards/dashboard_servidores_dag.py @@ -0,0 +1,168 @@ +""" +DAG para gerar arquivo JSON com dados do dashboard de servidores. +""" + +import json +import logging +from datetime import datetime, timedelta +from typing import Dict + +from airflow.decorators import dag, task +from airflow.models import Variable + +from postgres_helpers import get_postgres_conn +from cliente_postgres import ClientPostgresDB +from cliente_github import ClienteGitHub + +# Configuração de logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Configurações do GitHub +GITHUB_OWNER = "GovHub-br" +GITHUB_REPO = "gov-hub" +GITHUB_FILE_PATH = "docs/land/public/data/pessoas_visao_geral.json" +GITHUB_BRANCH = "main" + + +@dag( + dag_id="dashboard_servidores_json", + schedule_interval="0 6 * * *", # Executa diariamente às 6h + start_date=datetime(2025, 11, 16), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["dashboard", "pessoas", "json"], + description="Gera arquivo JSON com dados do dashboard de servidores", +) +def dashboard_servidores_dag() -> None: + """ + DAG que gera arquivo JSON consolidado com dados do dashboard de servidores. + """ + + @task + def generate_dashboard_json() -> Dict: + """ + Gera dados do dashboard de servidores. + + Returns: + Dicionário com os dados do dashboard + """ + logger.info("Iniciando geração dos dados do dashboard") + + try: + # Conectar ao banco de dados usando helper + postgres_conn_str = get_postgres_conn() + client = ClientPostgresDB(postgres_conn_str) + logger.info("Conectado ao banco de dados com sucesso") + + # Buscar dados + logger.info("Buscando KPIs...") + kpis = client.get_dashboard_kpis() + + logger.info("Buscando distribuição por gênero...") + genero = client.get_dashboard_genero() + + logger.info("Buscando distribuição por raça/cor...") + raca_cor = client.get_dashboard_raca_cor() + + logger.info("Buscando distribuição por situação funcional...") + situacao_funcional = client.get_dashboard_situacao_funcional() + + logger.info("Buscando distribuição geográfica por UF...") + mapa_uf = client.get_dashboard_mapa_uf() + + logger.info("Buscando tabela de servidores agregada...") + tabela_servidores = client.get_dashboard_tabela_servidores(limit=100) + + # Montar estrutura do JSON + dashboard_data = { + "meta": {"atualizado_em": datetime.now().isoformat() + "Z"}, + "kpis": { + "total_servidores": kpis.get("total_servidores", 0), + "servidores_ativos_permanentes": kpis.get( + "servidores_ativos_permanentes", 0 + ), + "aposentados": kpis.get("aposentados", 0), + "estagiarios": kpis.get("estagiarios", 0), + "terceirizados": kpis.get("terceirizados", 0), + }, + "genero": genero, + "raca_cor": raca_cor, + "mapa_uf": mapa_uf, + "situacao_funcional": situacao_funcional, + "tabela_servidores": tabela_servidores, + } + + return dashboard_data + + except Exception as e: + logger.error(f"Erro ao gerar dados do dashboard: {str(e)}") + raise + + @task + def publish_to_github(dashboard_data: Dict) -> Dict[str, str]: + """ + Publica os dados do dashboard no repositório GitHub. + + Args: + dashboard_data: Dicionário com os dados do dashboard + + Returns: + Informações sobre o commit realizado + """ + logger.info("Iniciando publicação dos dados no GitHub") + + try: + # Obter token do GitHub das variáveis do Airflow + github_token = Variable.get("GITHUB_TOKEN") + logger.info("Token do GitHub obtido com sucesso") + + # Converter dicionário para JSON string + json_content = json.dumps(dashboard_data, ensure_ascii=False, indent=2) + logger.info("Dados convertidos para JSON") + + # Inicializar cliente GitHub + github_client = ClienteGitHub(github_token) + + # Criar mensagem de commit + commit_message = ( + f"Update dashboard data - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + + # Publicar no GitHub + logger.info(f"Publicando em {GITHUB_OWNER}/{GITHUB_REPO}:{GITHUB_FILE_PATH}") + result = github_client.update_file( + owner=GITHUB_OWNER, + repo=GITHUB_REPO, + path=GITHUB_FILE_PATH, + content=json_content, + message=commit_message, + branch=GITHUB_BRANCH, + ) + + commit_info = { + "commit_sha": result.get("commit", {}).get("sha", ""), + "commit_url": result.get("commit", {}).get("html_url", ""), + "file_url": result.get("content", {}).get("html_url", ""), + } + + logger.info("Arquivo publicado com sucesso no GitHub!") + logger.info(f"Commit SHA: {commit_info['commit_sha']}") + logger.info(f"URL do arquivo: {commit_info['file_url']}") + + return commit_info + + except Exception as e: + logger.error(f"Erro ao publicar no GitHub: {str(e)}") + raise + + # Definir dependências entre tasks usando XCom + dashboard_data = generate_dashboard_json() + publish_to_github(dashboard_data) + + +dag_instance = dashboard_servidores_dag() diff --git a/airflow_lappis/dags/data_ingest/compras_gov/contratos_inativos_ingest_dag.py b/airflow_lappis/dags/data_ingest/compras_gov/contratos_inativos_ingest_dag.py new file mode 100755 index 00000000..9f06fbc8 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/compras_gov/contratos_inativos_ingest_dag.py @@ -0,0 +1,72 @@ +import logging +import yaml +from airflow.decorators import dag, task +from airflow.models import Variable +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_contratos import ClienteContratos +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("contratos_inativos_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["contratos_inativos_api", "compras_gov"], +) +def api_contratos_inativos_dag() -> None: + """DAG para buscar e armazenar contratos inativos de uma API no PostgreSQL.""" + + @task + def fetch_and_store_contratos_inativos() -> None: + logging.info("Iniciando fetch_and_store_contratos_inativos") + + orgao_alvo = Variable.get("airflow_orgao", default_var=None) + if not orgao_alvo: + logging.error("Variável airflow_orgao não definida!") + raise ValueError("airflow_orgao não definida") + + orgaos_config_str = Variable.get("airflow_variables", default_var="{}") + orgaos_config = yaml.safe_load(orgaos_config_str) + + ug_codes = orgaos_config.get(orgao_alvo, {}).get("codigos_ug", []) + + if not ug_codes: + logging.warning(f"Nenhum código UG encontrado para o órgão '{orgao_alvo}'") + return + + api = ClienteContratos() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + for ug_code in ug_codes: + logging.info(f"Buscando contratos inativos para UG: {ug_code}") + contratos = api.get_contratos_inativos_by_ug(ug_code) + if contratos: + # Adicionar dt_ingest a cada contrato + for contrato in contratos: + contrato["dt_ingest"] = datetime.now().isoformat() + + logging.info( + f"Inserindo contratos inativos da UG {ug_code} no schema compras_gov" + ) + db.insert_data( + contratos, + "contratos", + conflict_fields=["id"], + primary_key=["id"], + schema="compras_gov", + ) + else: + logging.warning(f"Nenhum contrato inativo encontrado para UG {ug_code}") + + fetch_and_store_contratos_inativos() + + +dag_instance = api_contratos_inativos_dag() diff --git a/airflow_lappis/dags/data_ingest/compras_gov/contratos_ingest_dag.py b/airflow_lappis/dags/data_ingest/compras_gov/contratos_ingest_dag.py new file mode 100755 index 00000000..ad789ce7 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/compras_gov/contratos_ingest_dag.py @@ -0,0 +1,78 @@ +import logging +import yaml +from airflow.decorators import dag, task +from airflow.operators.trigger_dagrun import TriggerDagRunOperator +from airflow.models import Variable +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_contratos import ClienteContratos +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("contratos_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["contratos_api", "compras_gov"], +) +def api_contratos_dag() -> None: + """DAG para buscar e armazenar contratos por órgão definido.""" + + @task + def fetch_and_store_contratos() -> None: + logging.info("[contratos_ingest_dag.py] Iniciando extração") + + orgao_alvo = Variable.get("airflow_orgao", default_var=None) + if not orgao_alvo: + logging.error("Variável airflow_orgao não definida!") + raise ValueError("airflow_orgao não definida") + + orgaos_config_str = Variable.get("airflow_variables", default_var="{}") + orgaos_config = yaml.safe_load(orgaos_config_str) + + codigos_ug = orgaos_config.get(orgao_alvo, {}).get("codigos_ug", []) + + if not codigos_ug: + logging.warning(f"Nenhum código UG encontrado para o órgão '{orgao_alvo}'") + return + + api = ClienteContratos() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + for ug_code in codigos_ug: + logging.info(f"Buscando contratos para UG: {ug_code}") + contratos = api.get_contratos_by_ug(ug_code) + + if contratos: + # Adicionar dt_ingest a cada contrato + for contrato in contratos: + contrato["dt_ingest"] = datetime.now().isoformat() + + logging.info(f"Inserindo contratos da UG {ug_code} no schema compras_gov") + db.insert_data( + contratos, + "contratos", + conflict_fields=["id"], + primary_key=["id"], + schema="compras_gov", + ) + else: + logging.warning(f"Nenhum contrato encontrado para UG {ug_code}") + + trigger_contratos_inativos = TriggerDagRunOperator( + task_id="trigger_contratos_inativos", + trigger_dag_id="api_contratos_inativos_dag", + wait_for_completion=False, + ) + + fetch_and_store_contratos() >> trigger_contratos_inativos + + +dag_instance = api_contratos_dag() diff --git a/airflow_lappis/dags/data_ingest/compras_gov/cronograma_ingest_dag.py b/airflow_lappis/dags/data_ingest/compras_gov/cronograma_ingest_dag.py new file mode 100755 index 00000000..5eeccfc1 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/compras_gov/cronograma_ingest_dag.py @@ -0,0 +1,67 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_contratos import ClienteContratos +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("cronograma_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["cronogramas_api", "compras_gov"], +) +def api_cronogramas_dag() -> None: + """DAG para buscar e armazenar cronogramas de uma API no PostgreSQL.""" + + @task + def fetch_cronogramas() -> None: + logging.info("[cronograma_ingest_dag.py] Starting fetch_cronogramas task") + api = ClienteContratos() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + contratos_ids = db.get_contratos_ids() + + # Drop the existing cronograma table before inserting new data + logging.info("[cronograma_ingest_dag.py] Dropping existing cronograma table") + db.drop_table_if_exists("cronograma", schema="compras_gov") + logging.info("[cronograma_ingest_dag.py] Table dropped successfully") + + for contrato_id in contratos_ids: + logging.info( + f"[cronograma_ingest_dag.py] Fetching cronograma for contrato ID: " + f"{contrato_id}" + ) + cronograma = api.get_cronograma_by_contrato_id(contrato_id) + if cronograma: + # Adicionar dt_ingest a cada item do cronograma + for item in cronograma: + item["dt_ingest"] = datetime.now().isoformat() + + logging.info( + f"[cronograma_ingest_dag.py] Inserting cronograma for contrato ID: " + f"{contrato_id} into PostgreSQL" + ) + db.insert_data( + cronograma, + "cronograma", + conflict_fields=["id"], + primary_key=["id"], + schema="compras_gov", + ) + else: + logging.warning( + f"[cronograma_ingest_dag.py] No cronograma found for contrato ID: " + f"{contrato_id}" + ) + + fetch_cronogramas() + + +dag_instance = api_cronogramas_dag() diff --git a/airflow_lappis/dags/data_ingest/compras_gov/empenhos_ingest_dag.py b/airflow_lappis/dags/data_ingest/compras_gov/empenhos_ingest_dag.py new file mode 100755 index 00000000..828efb5e --- /dev/null +++ b/airflow_lappis/dags/data_ingest/compras_gov/empenhos_ingest_dag.py @@ -0,0 +1,65 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_contratos import ClienteContratos +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("empenhos_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["empenhos_api", "compras_gov"], +) +def api_empenhos_dag() -> None: + """DAG para buscar e armazenar dados de empenhos de uma API.""" + + @task + def fetch_empenhos() -> None: + logging.info("[empenhos_ingest_dag.py] Starting fetch_empenhos task") + api = ClienteContratos() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + contratos_ids = db.get_contratos_ids() + + for contrato_id in contratos_ids: + try: + logging.info( + f"[empenhos_ingest_dag.py] Fetching empenhos for contrato ID: " + f"{contrato_id}" + ) + empenhos = api.get_empenhos_by_contrato_id(str(contrato_id)) + + if empenhos: + for empenho in empenhos: + empenho["contrato_id"] = contrato_id + empenho["dt_ingest"] = datetime.now().isoformat() + + logging.info( + f"[empenhos_ingest_dag.py] Inserting empenhos for contrato ID: " + f"{contrato_id} into PostgreSQL" + ) + db.insert_data( + empenhos, + "empenhos", + conflict_fields=["id", "contrato_id"], + primary_key=["id", "contrato_id"], + schema="compras_gov", + ) + except Exception as e: + logging.error( + f"[empenhos_ingest_dag.py] Error fetching empenhos for contrato " + f"ID {contrato_id}: {e}" + ) + + fetch_empenhos() + + +dag_instance = api_empenhos_dag() diff --git a/airflow_lappis/dags/data_ingest/compras_gov/faturas_ingest_dag.py b/airflow_lappis/dags/data_ingest/compras_gov/faturas_ingest_dag.py new file mode 100755 index 00000000..62008d8f --- /dev/null +++ b/airflow_lappis/dags/data_ingest/compras_gov/faturas_ingest_dag.py @@ -0,0 +1,64 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from cliente_contratos import ClienteContratos +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn + + +@dag( + schedule_interval=get_dynamic_schedule("faturas_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["faturas_api", "compras_gov"], +) +def api_faturas_dag() -> None: + """DAG para buscar e armazenar faturas de uma API no PostgreSQL.""" + + @task + def fetch_faturas() -> None: + logging.info("[faturas_ingest_dag.py] Starting fetch_faturas task") + api = ClienteContratos() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + contratos_ids = db.get_contratos_ids() + + for contrato_id in contratos_ids: + try: + logging.info( + f"[faturas_ingest_dag.py] Fetching faturas for contrato ID: " + f"{contrato_id}" + ) + faturas = api.get_faturas_by_contrato_id(str(contrato_id)) + + # Adicionar dt_ingest a cada fatura + if faturas: + for fatura in faturas: + fatura["dt_ingest"] = datetime.now().isoformat() + + logging.info( + f"[faturas_ingest_dag.py] Inserting faturas for contrato ID: " + f"{contrato_id} into PostgreSQL" + ) + db.insert_data( + faturas, + "faturas", + conflict_fields=["id"], + primary_key=["id"], + schema="compras_gov", + ) + except Exception as e: + logging.error( + f"[faturas_ingest_dag.py] Error fetching faturas for contrato ID " + f"{contrato_id}: {e}" + ) + + fetch_faturas() + + +dag_instance = api_faturas_dag() diff --git a/airflow_lappis/dags/data_ingest/compras_gov/terceirizados_ingest_dag.py b/airflow_lappis/dags/data_ingest/compras_gov/terceirizados_ingest_dag.py new file mode 100644 index 00000000..34083e50 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/compras_gov/terceirizados_ingest_dag.py @@ -0,0 +1,62 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_contratos import ClienteContratos +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("terceirizados_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["terceirizados_api", "compras_gov"], +) +def api_terceirizados_dag() -> None: + """DAG para buscar e armazenar dados de terceirizados de uma API.""" + + @task + def fetch_terceirizados() -> None: + logging.info("[terceirizados_ingest_dag.py] Starting fetch_terceirizados task") + api = ClienteContratos() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + contratos_ids = db.get_contratos_ids() + + for contrato_id in contratos_ids: + try: + logging.info(f"Fetching terceirizados for contrato ID: " f"{contrato_id}") + terceirizados = api.get_terceirizados_by_contrato_id(str(contrato_id)) + + # Adicionar dt_ingest a cada terceirizado + if terceirizados: + for terceirizado in terceirizados: + terceirizado["dt_ingest"] = datetime.now().isoformat() + + logging.info( + f"Inserting terceirizados for contrato ID: " + f"{contrato_id} into PostgreSQL" + ) + db.insert_data( + terceirizados, + "terceirizados", + conflict_fields=["id"], + primary_key=["id"], + schema="compras_gov", + ) + except Exception as e: + logging.error( + f"[terceirizados_ingest_dag.py] Error fetching terceirizados for " + f"contrato ID {contrato_id}: {e}" + ) + + fetch_terceirizados() + + +dag_instance = api_terceirizados_dag() diff --git a/airflow_lappis/dags/data_ingest/dados_abertos/deputados_ingest_dag.py b/airflow_lappis/dags/data_ingest/dados_abertos/deputados_ingest_dag.py new file mode 100644 index 00000000..01ee5e6b --- /dev/null +++ b/airflow_lappis/dags/data_ingest/dados_abertos/deputados_ingest_dag.py @@ -0,0 +1,77 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_deputados import ClienteDeputados +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("deputados_ingest_dag"), + start_date=datetime(2025, 1, 1), + catchup=False, + default_args={ + "owner": "Leonardo e Mateus", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["camara_deputados", "deputados", "dados_abertos", "MIR"], +) +def deputados_ingest_dag() -> None: + """DAG para buscar e armazenar dados de deputados da Câmara dos Deputados.""" + + @task + def fetch_and_store_deputados() -> None: + logging.info("[deputados_ingest_dag.py] Iniciando extração de deputados") + + api = ClienteDeputados() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + deputados_data = api.get_all_deputados() + + unique_key = ["id", "siglapartido", "idlegislatura"] + + if deputados_data: + vistos = set() + lista_limpa = [] + + for item in deputados_data: + + if item.get("siglaPartido") is None: + item["siglaPartido"] = "Sem Partido" + + chave_unica = tuple(item.get(key) for key in unique_key) + + if chave_unica not in vistos: + item["dt_ingest"] = datetime.now().isoformat() + lista_limpa.append(item) + vistos.add(chave_unica) + + deputados_data = lista_limpa + + logging.info( + f"[deputados_ingest_dag.py] Inserindo " + f"{len(deputados_data)} deputados no schema camara_deputados" + ) + + db.insert_data( + deputados_data, + "deputados", + conflict_fields=["id", "siglapartido", "idlegislatura"], + primary_key=["id", "siglapartido", "idlegislatura"], + schema="camara_deputados", + ) + + logging.info( + f"[deputados_ingest_dag.py] Concluído. " + f"Total de {len(deputados_data)} registros processados." + ) + else: + logging.warning("[deputados_ingest_dag.py] Nenhum deputado encontrado") + + fetch_and_store_deputados() + + +deputados_ingest_dag() diff --git a/airflow_lappis/dags/data_ingest/dados_abertos/logo_partidos_dag.py b/airflow_lappis/dags/data_ingest/dados_abertos/logo_partidos_dag.py new file mode 100644 index 00000000..42694b9d --- /dev/null +++ b/airflow_lappis/dags/data_ingest/dados_abertos/logo_partidos_dag.py @@ -0,0 +1,79 @@ +import logging +import time +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_partidos import ClientePartidos +from cliente_postgres import ClientPostgresDB + +@dag( + schedule_interval=get_dynamic_schedule("logo_partidos_dag"), + start_date=datetime(2025, 1, 1), + catchup=False, + default_args={ + "owner": "Ingrid", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["logo_partidos", "partidos", "dados_abertos", "MIR"], +) +def logo_partidos_dag() -> None: + """DAG para buscar e armazenar dados de partidos e seus logos da Câmara dos Deputados.""" + + @task + def fetch_and_store_partidos() -> None: + logging.info("[logo_partidos_dag.py] Iniciando extração de partidos") + + api = ClientePartidos() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + partidos_basicos = api.get_all_partidos() + + partidos_completos = [] + + if partidos_basicos: + for p in partidos_basicos: + partido_id = p.get("id") + if partido_id: + detalhe = api.get_partido_by_id(partido_id) + if detalhe: + registro = { + "id": detalhe.get("id"), + "sigla": detalhe.get("sigla"), + "nome": detalhe.get("nome"), + "uri": detalhe.get("uri"), + "urllogo": detalhe.get("urlLogo"), + "dt_ingest": datetime.now().isoformat() + } + partidos_completos.append(registro) + + time.sleep(0.5) + + logging.info( + f"[logo_partidos_dag.py] Inserindo " + f"{len(partidos_completos)} partidos no schema camara_deputados" + ) + + if partidos_completos: + db.insert_data( + partidos_completos, + "logo_partidos", + conflict_fields=["id"], + primary_key=["id"], + schema="dados_abertos", + ) + + logging.info( + f"[logo_partidos_dag.py] Concluído. " + f"Total de {len(partidos_completos)} registros processados." + ) + else: + logging.warning("[logo_partidos_dag.py] Nenhum dado de detalhe retornado para os partidos.") + else: + logging.warning("[logo_partidos_dag.py] Nenhum partido encontrado na lista principal.") + + fetch_and_store_partidos() + +logo_partidos_dag() \ No newline at end of file diff --git a/airflow_lappis/dags/data_ingest/dados_abertos/parlamentares_controle_historico_dag.py b/airflow_lappis/dags/data_ingest/dados_abertos/parlamentares_controle_historico_dag.py new file mode 100644 index 00000000..b61051b6 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/dados_abertos/parlamentares_controle_historico_dag.py @@ -0,0 +1,474 @@ +import logging +from datetime import datetime, timedelta + +import psycopg2 +import psycopg2.extras +from airflow.decorators import dag, task + +from cliente_deputados import ClienteDeputados +from cliente_postgres import ClientPostgresDB +from cliente_senadores import ClienteSenadores +from postgres_helpers import get_postgres_conn +from schedule_loader import get_dynamic_schedule + +CONTROLE_TABLE = "parlamentares_controle" +CONTROLE_SCHEMA = "dados_abertos" + +# Helpers + + +def _table_exists(cursor, schema: str, table: str) -> bool: + cursor.execute( + """ + SELECT EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE table_schema = %s + AND table_name = %s + ) + """, + (schema, table), + ) + return bool(cursor.fetchone()[0]) + + +def _table_has_column(cursor, schema: str, table: str, column: str) -> bool: + cursor.execute( + """ + SELECT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = %s + AND table_name = %s + AND column_name = %s + ) + """, + (schema, table, column), + ) + return bool(cursor.fetchone()[0]) + + +def _fetch_historico_ids(cursor, schema: str, table: str) -> set[int]: + if not _table_exists(cursor, schema, table): + return set() + + cursor.execute( + f""" + SELECT DISTINCT CAST(id::text AS BIGINT) + FROM {schema}.{table} + WHERE id IS NOT NULL + AND id::text ~ '^[0-9]+$' + """ + ) + return {int(row[0]) for row in cursor.fetchall()} + + +def _clean_existing_historico( + conn_str: str, schema: str, table: str, records: list[dict] +) -> None: + """Remove o histórico antigo para evitar duplicação antes do insert em lote.""" + if not records: + return + + ids = tuple( + set(item["parlamentar_id"] for item in records if "parlamentar_id" in item) + ) + if not ids: + return + + conn = psycopg2.connect(conn_str) + try: + with conn.cursor() as cursor: + if _table_exists(cursor, schema, table): + cursor.execute( + f"DELETE FROM {schema}.{table} WHERE parlamentar_id IN %s", (ids,) + ) + conn.commit() + finally: + conn.close() + + +# --- + + +@dag( + schedule_interval=get_dynamic_schedule("parlamentares_controle_historico_dag"), + start_date=datetime(2025, 1, 1), + catchup=False, + default_args={ + "owner": "Tiago", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["MIR", "dados_abertos", "parlamentares", "deputados", "senadores", "historico"], +) +def parlamentares_controle_historico_dag() -> None: + """Sincroniza parlamentares atuais e controla extração de histórico por ciclo temporal.""" + + @task + def sync_atuais() -> dict[str, list[int]]: + """Task 1: Busca parlamentares atuais da Câmara e do Senado.""" + logging.info( + "[parlamentares_controle_historico_dag.py] Iniciando sync de parlamentares atuais" + ) + + cliente_deputados = ClienteDeputados() + cliente_senadores = ClienteSenadores() + + deputados = cliente_deputados.get_deputados_atuais() + senadores = cliente_senadores.get_senadores_atuais() + + if deputados is None: + raise RuntimeError( + "Falha ao obter snapshot atual da Camara. " + "Execucao interrompida para evitar fechamento indevido." + ) + + if not senadores: + raise RuntimeError( + "Falha ao obter snapshot atual do Senado (vazio inesperado). " + "Execucao interrompida para evitar fechamento indevido." + ) + + deputados_ids = { + int(item["id"]) + for item in deputados + if isinstance(item, dict) and item.get("id") is not None + } + senadores_ids = { + int(item.get("IdentificacaoParlamentar", {}).get("CodigoParlamentar")) + for item in senadores + if isinstance(item, dict) + and item.get("IdentificacaoParlamentar", {}).get("CodigoParlamentar") + is not None + } + + payload = { + "camara": sorted(list(deputados_ids)), + "senado": sorted(list(senadores_ids)), + } + + logging.info( + f"[parlamentares_controle_historico_dag.py] Sync concluido: " + f"camara={len(payload['camara'])}, senado={len(payload['senado'])}" + ) + return payload + + @task + def state_logic(parlamentares_atuais: dict[str, list[int]]) -> list[dict[str, str]]: + """Task 2: Mantém a tabela de controle com estados de atividade.""" + conn_str = get_postgres_conn("postgres_mir") + + create_table_sql = f""" + CREATE SCHEMA IF NOT EXISTS {CONTROLE_SCHEMA}; + CREATE TABLE IF NOT EXISTS {CONTROLE_SCHEMA}.{CONTROLE_TABLE} ( + fonte TEXT NOT NULL, + parlamentar_id BIGINT NOT NULL, + status TEXT NOT NULL, + first_seen_at TIMESTAMP NOT NULL, + last_seen_at TIMESTAMP NOT NULL, + last_historico_at TIMESTAMP NULL, + updated_at TIMESTAMP NOT NULL, + PRIMARY KEY (fonte, parlamentar_id) + ); + """ + + now = datetime.now() + + conn = psycopg2.connect(conn_str) + try: + with conn.cursor() as cursor: + cursor.execute(create_table_sql) + + cursor.execute(f"SELECT COUNT(*) FROM {CONTROLE_SCHEMA}.{CONTROLE_TABLE}") + controle_vazio = cursor.fetchone()[0] == 0 + + if controle_vazio: + historico_camara_ids = _fetch_historico_ids( + cursor, "camara_deputados", "deputados" + ) + historico_senado_ids = _fetch_historico_ids( + cursor, "senado_federal", "senadores" + ) + + for fonte, ids_historicos in ( + ("camara", historico_camara_ids), + ("senado", historico_senado_ids), + ): + ids_atuais = set(parlamentares_atuais.get(fonte, [])) + universo = ids_historicos.union(ids_atuais) + + if not universo: + continue + + values = [ + ( + fonte, + parlamentar_id, + "ATIVO" if parlamentar_id in ids_atuais else "INATIVO", + now, + now, + now, + ) + for parlamentar_id in universo + ] + + psycopg2.extras.execute_values( + cursor, + f""" + INSERT INTO {CONTROLE_SCHEMA}.{CONTROLE_TABLE} + (fonte, parlamentar_id, status, first_seen_at, last_seen_at, updated_at) + VALUES %s + ON CONFLICT (fonte, parlamentar_id) + DO UPDATE SET + status = EXCLUDED.status, + updated_at = EXCLUDED.updated_at + """, + values, + ) + + logging.info( + f"[parlamentares_controle_historico_dag.py] Bootstrap realizado para " + f"fonte={fonte}: universo={len(universo)}, atuais={len(ids_atuais)}" + ) + + for fonte in ("camara", "senado"): + ids_atuais = [int(v) for v in parlamentares_atuais.get(fonte, [])] + + if ids_atuais: + values = [ + (fonte, parlamentar_id, "ATIVO", now, now, now) + for parlamentar_id in ids_atuais + ] + psycopg2.extras.execute_values( + cursor, + f""" + INSERT INTO {CONTROLE_SCHEMA}.{CONTROLE_TABLE} + (fonte, parlamentar_id, status, first_seen_at, last_seen_at, updated_at) + VALUES %s + ON CONFLICT (fonte, parlamentar_id) + DO UPDATE SET + status = EXCLUDED.status, + last_seen_at = EXCLUDED.last_seen_at, + updated_at = EXCLUDED.updated_at + """, + values, + ) + + cursor.execute( + f""" + UPDATE {CONTROLE_SCHEMA}.{CONTROLE_TABLE} + SET status = 'PENDENTE_FECHAMENTO', + updated_at = %s + WHERE fonte = %s + AND status = 'ATIVO' + AND last_seen_at < %s + """, + (now, fonte, now), + ) + else: + logging.warning( + f"[parlamentares_controle_historico_dag.py] Snapshot de atuais vazio para " + f"fonte={fonte}. Fechamento ignorado nesta execucao." + ) + + # Evita recarga inicial desnecessária em parlamentares já contidos na bronze nativa + if _table_exists( + cursor, "camara_deputados", "deputados_historico" + ) and _table_has_column( + cursor, "camara_deputados", "deputados_historico", "parlamentar_id" + ): + cursor.execute( + f""" + UPDATE {CONTROLE_SCHEMA}.{CONTROLE_TABLE} c + SET last_historico_at = %s, + updated_at = %s + WHERE c.fonte = 'camara' + AND c.last_historico_at IS NULL + AND EXISTS ( + SELECT 1 + FROM camara_deputados.deputados_historico h + WHERE h.parlamentar_id::text ~ '^[0-9]+$' + AND CAST(h.parlamentar_id::text AS BIGINT) = c.parlamentar_id + ) + """, + (now, now), + ) + + if _table_exists( + cursor, "senado_federal", "senadores_historico" + ) and _table_has_column( + cursor, "senado_federal", "senadores_historico", "parlamentar_id" + ): + cursor.execute( + f""" + UPDATE {CONTROLE_SCHEMA}.{CONTROLE_TABLE} c + SET last_historico_at = %s, + updated_at = %s + WHERE c.fonte = 'senado' + AND c.last_historico_at IS NULL + AND EXISTS ( + SELECT 1 + FROM senado_federal.senadores_historico h + WHERE h.parlamentar_id::text ~ '^[0-9]+$' + AND CAST(h.parlamentar_id::text AS BIGINT) = c.parlamentar_id + ) + """, + (now, now), + ) + + cursor.execute( + f""" + SELECT fonte, parlamentar_id, status, last_historico_at + FROM {CONTROLE_SCHEMA}.{CONTROLE_TABLE} + WHERE (status = 'ATIVO' AND ( + last_historico_at IS NULL + OR last_historico_at <= NOW() - INTERVAL '7 days' + )) + OR status = 'PENDENTE_FECHAMENTO' + ORDER BY + CASE WHEN status = 'PENDENTE_FECHAMENTO' THEN 0 ELSE 1 END, + COALESCE(last_historico_at, TIMESTAMP '1900-01-01') ASC, + parlamentar_id ASC + """ + ) + rows = cursor.fetchall() + conn.commit() + finally: + conn.close() + + candidatos = [ + { + "fonte": row[0], + "parlamentar_id": str(row[1]), + "status": row[2], + "last_historico_at": row[3].isoformat() if row[3] else "", + } + for row in rows + ] + + logging.info( + f"[parlamentares_controle_historico_dag.py] State logic concluido. " + f"Parlamentares elegiveis para historico: {len(candidatos)}" + ) + return candidatos + + @task + def extrair_historico(candidatos: list[dict[str, str]]) -> None: + """Task 3: Extrai histórico da fonte oficial e injeta na base.""" + if not candidatos: + logging.info( + "[parlamentares_controle_historico_dag.py] Nenhum parlamentar elegivel para historico" + ) + return + + conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(conn_str) + cliente_deputados = ClienteDeputados() + cliente_senadores = ClienteSenadores() + + now = datetime.now() + historico_camara: list[dict] = [] + historico_senado: list[dict] = [] + status_updates: list[tuple[str, str, int]] = [] + + for candidato in candidatos: + fonte = candidato["fonte"] + parlamentar_id = int(candidato["parlamentar_id"]) + status = candidato["status"] + + try: + extracao_ok = False + + if fonte == "camara": + dados = cliente_deputados.get_historico_deputado(parlamentar_id) + if dados is not None: + extracao_ok = True + for item in dados: + if isinstance(item, dict): + item["parlamentar_id"] = parlamentar_id + item["fonte"] = fonte + item["dt_ingest"] = now.isoformat() + historico_camara.append(item) + else: + dados = cliente_senadores.get_filiacoes_senador(parlamentar_id) + if dados is not None: + extracao_ok = True + for item in dados: + if isinstance(item, dict): + item["parlamentar_id"] = parlamentar_id + item["fonte"] = fonte + item["dt_ingest"] = now.isoformat() + historico_senado.append(item) + + if not extracao_ok: + logging.warning( + f"[parlamentares_controle_historico_dag.py] Sem confirmação de " + f"extração para fonte={fonte}, parlamentar_id={parlamentar_id}. Status mantido." + ) + continue + + novo_status = "INATIVO" if status == "PENDENTE_FECHAMENTO" else "ATIVO" + status_updates.append((novo_status, fonte, parlamentar_id)) + except Exception as e: + logging.error( + f"[parlamentares_controle_historico_dag.py] Erro ao extrair historico " + f"fonte={fonte}, parlamentar_id={parlamentar_id}: {e}" + ) + + if historico_camara: + _clean_existing_historico( + conn_str, "camara_deputados", "deputados_historico", historico_camara + ) + db.insert_data( + historico_camara, + table_name="deputados_historico", + schema="camara_deputados", + ) + + if historico_senado: + _clean_existing_historico( + conn_str, "senado_federal", "senadores_historico", historico_senado + ) + db.insert_data( + historico_senado, + table_name="senadores_historico", + schema="senado_federal", + ) + + if status_updates: + conn = psycopg2.connect(conn_str) + try: + with conn.cursor() as cursor: + psycopg2.extras.execute_batch( + cursor, + f""" + UPDATE {CONTROLE_SCHEMA}.{CONTROLE_TABLE} + SET status = %s, + last_historico_at = %s, + updated_at = %s + WHERE fonte = %s + AND parlamentar_id = %s + """, + [ + (status, now, now, fonte, parlamentar_id) + for status, fonte, parlamentar_id in status_updates + ], + ) + conn.commit() + finally: + conn.close() + + logging.info( + f"[parlamentares_controle_historico_dag.py] Extração concluida. " + f"Historico camara={len(historico_camara)}, " + f"historico senado={len(historico_senado)}, " + f"status atualizados={len(status_updates)}" + ) + + parlamentares_atuais = sync_atuais() + candidatos = state_logic(parlamentares_atuais) + extrair_historico(candidatos) + + +parlamentares_controle_historico_dag() diff --git a/airflow_lappis/dags/data_ingest/dados_abertos/periodo_legislatura_ingest_dag.py b/airflow_lappis/dags/data_ingest/dados_abertos/periodo_legislatura_ingest_dag.py new file mode 100644 index 00000000..ccbdfc10 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/dados_abertos/periodo_legislatura_ingest_dag.py @@ -0,0 +1,65 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_senadores import ClienteSenadores +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("periodo_legislatura_ingest_dag"), + start_date=datetime(2025, 1, 1), + catchup=False, + default_args={ + "owner": "Luana", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["senado_federal", "dados_abertos", "MIR"], +) +def periodo_legislacao_ingest_dag() -> None: + """DAG para buscar e armazenar período de legislação parlamentares""" + + @task + def fetch_and_store_periodo_legislacao() -> None: + logging.info("Iniciando extração de legislaturas") + + api = ClienteSenadores() + db = ClientPostgresDB(get_postgres_conn("postgres_mir")) + + lista_legislaturas = api.get_periodo_legislacao() + + if not lista_legislaturas or not isinstance(lista_legislaturas, list): + logging.error(f"Esperava uma lista, mas recebi: {type(lista_legislaturas)}") + return + + registros_limpos = [] + + for leg in lista_legislaturas: + item_limpo = { + "id": int(leg.get("NumeroLegislatura")), + "data_inicio": leg.get("DataInicio"), + "data_fim": leg.get("DataFim"), + "data_eleicao": leg.get("DataEleicao"), + "dt_ingest": datetime.now().isoformat(), + } + registros_limpos.append(item_limpo) + + logging.info(f"Preparados {len(registros_limpos)} registros para o banco.") + + if registros_limpos: + db.insert_data( + registros_limpos, + "legislaturas", + conflict_fields=["id"], + primary_key=["id"], + schema="senado_federal", + ) + + + fetch_and_store_periodo_legislacao() + + + +periodo_legislacao_ingest_dag() \ No newline at end of file diff --git a/airflow_lappis/dags/data_ingest/dados_abertos/senadores_ingest_dag.py b/airflow_lappis/dags/data_ingest/dados_abertos/senadores_ingest_dag.py new file mode 100644 index 00000000..4d46f59f --- /dev/null +++ b/airflow_lappis/dags/data_ingest/dados_abertos/senadores_ingest_dag.py @@ -0,0 +1,157 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_senadores import ClienteSenadores +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("senadores_ingest_dag"), + start_date=datetime(2025, 1, 1), + catchup=False, + default_args={ + "owner": "Ingrid", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["senado_federal", "senadores", "dados_abertos", "MIR"], +) +def senadores_ingest_dag() -> None: + """DAG para buscar e armazenar dados de senadores do Senado Federal.""" + + @task + def fetch_and_store_senadores() -> None: + logging.info("[senadores_ingest_dag.py] Iniciando extração de senadores") + + api = ClienteSenadores() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + senadores_data = api.get_senadores_por_legislatura() + + if senadores_data and len(senadores_data) > 0: + registros_limpos = [] + + for item in senadores_data: + info = item.get("IdentificacaoParlamentar", {}) + mandato = item.get("Mandato", {}) + + senador_simplificado = { + "id": info.get("CodigoParlamentar"), + "nome_parlamentar": info.get("NomeParlamentar"), + "nome_completo": info.get("NomeCompletoParlamentar"), + "sexo": info.get("SexoParlamentar"), + "forma_tratamento": info.get("FormaTratamento"), + "url_foto": info.get("UrlFotoParlamentar"), + "url_pagina": info.get("UrlPaginaParlamentar"), + "email": info.get("EmailParlamentar"), + "sigla_partido": info.get("SiglaPartidoParlamentar"), + "uf": info.get("UfParlamentar"), + "id_legislatura": mandato.get("NumeroLegislatura"), + "dt_ingest": datetime.now().isoformat(), + } + registros_limpos.append(senador_simplificado) + + logging.info( + f"[senadores_ingest_dag.py] Inserindo {len(registros_limpos)} " + f"senadores simplificados no schema senado_federal" + ) + + db.insert_data( + registros_limpos, + "senadores", + conflict_fields=["id"], + primary_key=["id"], + schema="senado_federal", + ) + @task + def fetch_and_store_filiacoes() -> None: + api = ClienteSenadores() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + # 1. Busca a lista base de senadores + senadores_base = api.get_senadores_por_legislatura() + registros_filiacoes = [] + + for sen in senadores_base: + info = sen.get("IdentificacaoParlamentar", {}) + cod_id = info.get("CodigoParlamentar") + nome = info.get("NomeParlamentar") + + if cod_id: + # 2. Para cada senador, busca o histórico de filiações + filiacoes = api.get_filiacoes_senador(cod_id) + if isinstance(filiacoes, dict): + filiacoes = [filiacoes] + elif not filiacoes: + logging.debug( + f"[senadores_ingest_dag.py] Nenhuma filiação encontrada para " + f"{nome} (ID: {cod_id})" + ) + continue + for f in filiacoes: + # Extrai os dados do objeto Partido aninhado + partido = f.get("Partido", {}) + sigla_partido = partido.get("SiglaPartido") or "Sigla não disponível" + nome_partido = partido.get("NomePartido") or "Nome não disponível" + ano_filiacao = f.get("AnoFiliacao") or "Data não disponível" + ano_desfiliacao = f.get("AnoDesfiliacao") + + registro = { + "id": cod_id, + "nome_parlamentar": nome, + "sigla_partido": sigla_partido, + "nome_partido": nome_partido, + "dt_filiacao": ano_filiacao, + "dt_desfiliacao": ano_desfiliacao, + "uf": info.get("UfParlamentar"), + "dt_ingest": datetime.now().isoformat(), + } + registros_filiacoes.append(registro) + logging.debug( + f"[senadores_ingest_dag.py] Filiação processada: " + f"{nome} -> {sigla_partido} ({ano_filiacao})" + ) + + logging.info( + f"[senadores_ingest_dag.py] Total de {len(registros_filiacoes)} " + f"registros de filiações coletados da API." + ) + + # 3. Deduplicação baseada nas chaves únicas + registros_deduplicated = {} + for registro in registros_filiacoes: + chave_unica = ( + registro.get("id"), + registro.get("sigla_partido"), + registro.get("dt_filiacao"), + ) + if chave_unica not in registros_deduplicated: + registros_deduplicated[chave_unica] = registro + + registros_filiacoes = list(registros_deduplicated.values()) + logging.info( + f"[senadores_ingest_dag.py] Após deduplicação: {len(registros_filiacoes)} " + f"registros únicos de histórico partidário." + ) + + # 4. Inserção no banco + if registros_filiacoes: + logging.info(f"Inserindo {len(registros_filiacoes)} registros de histórico partidário.") + db.insert_data( + registros_filiacoes, + "senadores_filiacoes", # Nome da nova tabela única + conflict_fields=["id", "sigla_partido", "dt_filiacao"], + primary_key=["id", "sigla_partido", "dt_filiacao"], # Chave composta para permitir múltiplos registros do mesmo ID + schema="senado_federal", + ) + fetch_and_store_senadores() + + fetch_and_store_filiacoes() + + + +senadores_ingest_dag() diff --git a/airflow_lappis/dags/data_ingest/ibge/mulheres_ingest_dag.py b/airflow_lappis/dags/data_ingest/ibge/mulheres_ingest_dag.py new file mode 100644 index 00000000..19c46a24 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/ibge/mulheres_ingest_dag.py @@ -0,0 +1,398 @@ +import logging +import re +import unicodedata +from datetime import datetime, timedelta + +import pandas as pd +import yaml +from airflow.decorators import dag, task +from airflow.models import Variable + +from cliente_ibge import ClienteIBGE +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn +from schedule_loader import get_dynamic_schedule + +# Constantes +CONECTIVOS = frozenset( + {"da", "das", "de", "do", "em", "e", "na", "no", "para", "ou", "com", "x", "que", "o"} +) + +REGRAS_CORTE_TABELAS: dict[str, int] = { + "tabela_3": 10, + "tabela_7": 6, + "tabela_9": 7, +} + +MAX_COL_LEN = 63 +VALORES_NULOS = ("nan", "none", "") + + +# Helpers: encurtar_nome_coluna +def _reordenar_prefixo_numerico(partes: list[str]) -> list[str]: + """Move um prefixo numérico (ex: '12_a_14') para o final da lista.""" + if not partes or not partes[0] or not partes[0][0].isdigit(): + return partes + + idx_fim = 0 + for i, parte in enumerate(partes): + if parte and (parte[0].isdigit() or parte in ("a", "x")): + idx_fim = i + 1 + else: + break + + if 0 < idx_fim < len(partes): + return partes[idx_fim:] + partes[:idx_fim] + return partes + + +def _remover_conectivos(partes: list[str]) -> list[str]: + """Remove conectivos e partes vazias da lista.""" + filtradas = [p for p in partes if p and p.lower() not in CONECTIVOS] + return filtradas or partes + + +def _aplicar_corte_tabela(partes: list[str], num_tabela: str) -> list[str]: + """Remove prefixo fixo de partes para tabelas com regra especial.""" + tabela_key = num_tabela.lower() + corte = REGRAS_CORTE_TABELAS.get(tabela_key) + + if corte is None or len(partes) <= 7: + return partes + + cortadas = partes[corte:] + logging.info( + "[encurtar_nome_coluna] '%s' longo para %s — removendo %d partes iniciais", + "_".join(partes), + tabela_key, + corte, + ) + return cortadas if "_".join(cortadas) else partes + + +def _abreviar_partes_meio(partes: list[str]) -> str: + """Abrevia partes do meio (exceto primeira e última) para caber em max_len.""" + if len(partes) <= 2: + return "_".join(partes) + + meio_abreviado = [p[:5] if len(p) > 6 else p for p in partes[1:-1]] + nome = "_".join([partes[0]] + meio_abreviado + [partes[-1]]) + + logging.info("[encurtar_nome_coluna] Nome abreviado: %s", nome) + return nome + + +def _truncar_preservando_ultima(nome: str, ultima: str, max_len: int) -> str: + """Último recurso: trunca preservando a última palavra.""" + if ultima: + espaco = max_len - len(ultima) - 1 + if espaco > 0: + return f"{nome[:espaco]}_{ultima}"[:max_len] + return nome[:max_len] + + +def encurtar_nome_coluna( + nome: str, + max_len: int = MAX_COL_LEN, + num_tabela: str | None = None, +) -> str: + """ + Limpa e encurta o nome da coluna: + - Remove conectivos. + - Se iniciar com número, move o prefixo numérico para o final. + - Aplica regra de corte específica por tabela quando necessário. + - Abrevia partes do meio mantendo primeira e última palavra. + - Em último caso, trunca preservando a última palavra. + """ + partes = _reordenar_prefixo_numerico(nome.split("_")) + partes = _remover_conectivos(partes) + + nome_limpo = "_".join(partes) + if len(nome_limpo) <= max_len: + return nome_limpo + + if num_tabela: + partes = _aplicar_corte_tabela(partes, num_tabela) + nome_limpo = "_".join(partes) + if len(nome_limpo) <= max_len: + return nome_limpo + + nome_abreviado = _abreviar_partes_meio(partes) + if len(nome_abreviado) <= max_len: + return nome_abreviado + + return _truncar_preservando_ultima( + nome_abreviado, partes[-1] if partes else "", max_len + ) + + +# Helpers: normalização de texto e nomes de tabela +def _remover_acentos(texto: str) -> str: + return "".join( + c for c in unicodedata.normalize("NFD", texto) if unicodedata.category(c) != "Mn" + ) + + +def _normalizar_nome_coluna( + col: str, idx: int, num_tabela: str | None, table_name: str +) -> str: + """Limpa, normaliza e encurta o nome de uma coluna.""" + sem_acento = _remover_acentos(str(col)) + limpo = re.sub( + r"[^\w%]", + "", + sem_acento.lower() + .replace("%", "_porcentagem") + .replace(" ", "_") + .replace("-", "_"), + ) + encurtado = encurtar_nome_coluna(limpo, num_tabela=num_tabela) + return encurtado if encurtado != "none" else f"coluna_vazia_{idx}" + + +def _deduplicar_colunas(colunas: list[str], max_len: int = MAX_COL_LEN) -> list[str]: + """Garante unicidade adicionando sufixo numérico às colunas duplicadas.""" + contagem: dict[str, int] = {} + resultado: list[str] = [] + + for col in colunas: + if col not in contagem: + contagem[col] = 0 + resultado.append(col) + continue + + contagem[col] += 1 + sufixo = f"_{contagem[col]}" + novo = ( + f"{col[:max_len - len(sufixo)]}{sufixo}" + if len(col) + len(sufixo) > max_len + else f"{col}{sufixo}" + ) + resultado.append(novo) + + return resultado + + +def _construir_nome_tabela( + arquivo: str, sheet_name: str, tema_ibge: str, sufixo: str +) -> str: + """Deriva o nome da tabela de destino a partir dos metadados do arquivo.""" + clean_file = arquivo.split(".")[0].lower() + match = re.search(r"(tabela_\d+)", clean_file) + short_file = match.group(1) if match else clean_file[:15] + + clean_sheet = re.sub( + r"[^\w]", + "", + _remover_acentos(sheet_name).lower().replace(" ", "_").replace("-", "_"), + ) + prefixo = tema_ibge.lower().replace(" ", "_") + return f"{prefixo}_{short_file}_{clean_sheet}{sufixo}" + + +def _obter_tema_ibge() -> str: + config_str = Variable.get("ibge_censo_config", default_var='{"database": "Mulheres"}') + return yaml.safe_load(config_str).get("database", "Mulheres") + + +# Helpers: extração do Excel +def _identificar_chunks_horizontais(df_aba: pd.DataFrame) -> list[pd.DataFrame]: + """Divide o DataFrame pelas colunas totalmente vazias (separadores).""" + cols_vazias = [ + i for i, col in enumerate(df_aba.columns) if df_aba[col].isnull().all() + ] + pontos = [-1] + cols_vazias + [len(df_aba.columns)] + + chunks = [] + for i in range(len(pontos) - 1): + chunk = df_aba.iloc[:, pontos[i] + 1 : pontos[i + 1]].copy() + chunk = chunk.dropna(axis=1, how="all").dropna(axis=0, how="all") + if not chunk.empty and len(chunk.columns) > 1: + chunks.append(chunk.reset_index(drop=True)) + return chunks + + +def _extrair_nome_coluna_cabecalho(linhas_cab: pd.DataFrame, col_idx: int) -> str: + """Constrói o nome de uma coluna a partir de múltiplas linhas de cabeçalho.""" + pedacos = [] + for row_idx in range(len(linhas_cab)): + val = str(linhas_cab.iloc[row_idx, col_idx]).strip() + unicos = linhas_cab.iloc[row_idx].dropna().unique() + if len(unicos) > 1 and val.lower() not in VALORES_NULOS: + pedacos.append(val.split(" - ")[-1].strip()) + return "_".join(pedacos) if pedacos else f"coluna_vazia_{col_idx}" + + +def _construir_cabecalho(df_raw: pd.DataFrame, idx_dados: int) -> pd.DataFrame: + """Retorna as linhas de cabeçalho, descartando a primeira se for muito longa.""" + cabecalho = df_raw.iloc[:idx_dados].copy().ffill(axis=1) + primeira_linha = " ".join( + str(v).strip() + for v in cabecalho.iloc[0].tolist() + if str(v).strip().lower() not in VALORES_NULOS + ) + return cabecalho.iloc[1:] if len(primeira_linha) > 80 else cabecalho + + +def _processar_chunk_excel( + df_raw: pd.DataFrame, + idx: int, + total: int, + sheet_name: str, + arquivo: str, + tema_ibge: str, +) -> dict | None: + """Processa um chunk horizontal do Excel e devolve o dict de metadados ou None.""" + mascara_num = df_raw.apply( + lambda r: pd.to_numeric(r, errors="coerce").notna().sum() > 1, axis=1 + ) + if not mascara_num.any(): + return None + + idx_dados = mascara_num.idxmax() + cabecalho = _construir_cabecalho(df_raw, idx_dados) + nomes = [ + _extrair_nome_coluna_cabecalho(cabecalho, i) for i in range(len(df_raw.columns)) + ] + + df = df_raw.iloc[idx_dados:].copy() + df.columns = nomes + + col_dim = df.columns[0] + df = df.dropna(subset=[col_dim]) + df = df[~df[col_dim].astype(str).str.contains("Fonte:|Nota:", case=False, na=False)] + + return { + "df": df, + "sheet_name": sheet_name, + "arquivo": arquivo, + "sufixo": f"_parte_{idx + 1}" if total > 1 else "", + "tema_ibge": tema_ibge, + } + + +# Helpers: inserção no banco +def _processar_chunk_insercao( + chunk_info: dict, db: ClientPostgresDB, schema: str +) -> str | None: + """Limpa, deduplica e insere um chunk no banco. Retorna o nome da tabela ou None.""" + df: pd.DataFrame = chunk_info["df"] + arquivo: str = chunk_info["arquivo"] + sheet_name: str = chunk_info["sheet_name"] + sufixo: str = chunk_info["sufixo"] + tema_ibge: str = chunk_info["tema_ibge"] + + num_tabela_match = re.search(r"tabela[_\- ]?\d+", arquivo, re.IGNORECASE) + num_tabela = num_tabela_match.group(0) if num_tabela_match else None + + table_name = _construir_nome_tabela(arquivo, sheet_name, tema_ibge, sufixo) + + colunas = [ + _normalizar_nome_coluna(c, idx, num_tabela, table_name) + for idx, c in enumerate(df.columns) + ] + df.columns = _deduplicar_colunas(colunas) + + colunas_fantasma = [c for c in df.columns if c.startswith("coluna_vazia")] + if colunas_fantasma: + logging.info("Removendo colunas fantasmas: %s", colunas_fantasma) + df = df.drop(columns=colunas_fantasma) + + if df.empty or len(df.columns) == 0: + logging.warning("DataFrame vazio para %s. Pulando inserção.", table_name) + return None + + col_pk = df.columns[0] + df = df.drop_duplicates(subset=[col_pk]) + df["dt_ingest"] = datetime.now().isoformat() + df["nome_fonte"] = arquivo + + db.insert_data( + data=df.to_dict(orient="records"), + table_name=table_name, + schema=schema, + primary_key=[col_pk], + conflict_fields=[col_pk], + ) + logging.info("Tabela criada/atualizada: %s.%s", schema, table_name) + return table_name + + +# DAG +@dag( + schedule_interval=get_dynamic_schedule("mulheres_censo_dag"), + start_date=datetime(2026, 1, 1), + catchup=False, + default_args={ + "owner": "Rafael, Letícia", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["mulheres", "censo_demografico", "ibge"], +) +def mulheres_censo_demografico_dag() -> None: + """DAG para extrair, despivotar e armazenar dados do Censo 2022.""" + + # Task 1: Listar arquivos no FTP + @task + def listar_arquivos_ftp() -> list: + logging.info("[Task 1] Conectando ao FTP para listar arquivos...") + tema_ibge = _obter_tema_ibge() + arquivos = ClienteIBGE(database=tema_ibge).listar_arquivos_alvo() + + if not arquivos: + logging.warning("Nenhum arquivo encontrado no FTP.") + return arquivos + + # Task 2: Extrair dados do Excel + @task + def extrair_dados_excel(arquivo: str) -> list: + logging.info("[Task 2] Extraindo dados do arquivo: %s", arquivo) + tema_ibge = _obter_tema_ibge() + + buffer = ClienteIBGE(database=tema_ibge).obter_conteudo_arquivo(arquivo) + if not buffer: + raise ValueError(f"Falha ao baixar o arquivo {arquivo}") + + excel_file = pd.ExcelFile(buffer) + abas_validas = [ + a + for a in excel_file.sheet_names + if "gráfico" not in a.lower() and "grafico" not in a.lower() + ] + sheet_name = abas_validas[-1] if abas_validas else excel_file.sheet_names[0] + logging.info("Processando a aba: %s", sheet_name) + + df_aba = excel_file.parse(sheet_name, header=None) + chunks = _identificar_chunks_horizontais(df_aba) + + return [ + resultado + for idx, df_raw in enumerate(chunks) + if ( + resultado := _processar_chunk_excel( + df_raw, idx, len(chunks), sheet_name, arquivo, tema_ibge + ) + ) + ] + + # Task 3: Limpar e inserir dados no banco + @task + def limpar_e_inserir_dados(chunks_data: list) -> str: + logging.info("[Task 3] Limpando nomes de colunas e inserindo dados...") + db = ClientPostgresDB(get_postgres_conn()) + schema = "censo_demografico" + + tabelas = [ + nome + for chunk in chunks_data + if (nome := _processar_chunk_insercao(chunk, db, schema)) + ] + return f"Processadas {len(tabelas)} tabelas com sucesso" + + lista_de_arquivos = listar_arquivos_ftp() + dados_extraidos = extrair_dados_excel.expand(arquivo=lista_de_arquivos) + limpar_e_inserir_dados.expand(chunks_data=dados_extraidos) + + +mulheres_censo_demografico_dag() diff --git a/airflow_lappis/dags/data_ingest/ibge/quilombolas_alfabetizacao_ingest_dag.py b/airflow_lappis/dags/data_ingest/ibge/quilombolas_alfabetizacao_ingest_dag.py new file mode 100644 index 00000000..c476dac7 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/ibge/quilombolas_alfabetizacao_ingest_dag.py @@ -0,0 +1,168 @@ +""" +DAG de ingestão — Censo 2022: Quilombolas, alfabetização e características dos domicílios. + +Fonte FTP: +/Censos/Censo_Demografico_2022/Quilombolas_alfabetizacao_e_caracteristicas_dos_domicílios_Resultados_do_universo/ +""" + +import logging +from datetime import datetime, timedelta +from typing import Any + +import yaml +from airflow.decorators import dag, task +from airflow.models import Variable + +from cliente_ibge import ClienteIBGE +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn +from quilombolas_parser import ( + INDICES_FTP, + SUBPASTAS_DADOS, + extrair_chunks_de_excel, + parsear_arquivo_indice, + preparar_registros_insercao, +) +from schedule_loader import get_dynamic_schedule + +SCHEMA_DESTINO = "censo_demografico" +DATABASE_FTP = ( + "Quilombolas_alfabetizacao_e_caracteristicas_dos_domicílios_Resultados_do_universo" +) + + +def _obter_database_ftp() -> str: + config_str = Variable.get( + "ibge_quilombolas_config", + default_var=f'{{"database": "{DATABASE_FTP}"}}', + ) + return yaml.safe_load(config_str).get("database", DATABASE_FTP) + + +def _chunk_para_payload(chunk: Any) -> dict[str, Any]: + return { + "table_name": chunk.table_name, + "table_comment": chunk.table_comment, + "col_comments": chunk.col_comments, + "records": preparar_registros_insercao(chunk), + "primary_key": chunk.primary_key, + } + + +def _inserir_payloads(db: ClientPostgresDB, payloads: list[dict[str, Any]]) -> list[str]: + tabelas: list[str] = [] + for payload in payloads: + pk = payload["primary_key"] + db.insert_data( + data=payload["records"], + table_name=payload["table_name"], + schema=SCHEMA_DESTINO, + primary_key=pk, + conflict_fields=pk, + ) + db.apply_comments( + schema=SCHEMA_DESTINO, + table_name=payload["table_name"], + table_comment=payload.get("table_comment"), + column_comments=payload.get("col_comments"), + ) + tabelas.append(payload["table_name"]) + logging.info( + "Tabela criada/atualizada com comentários: %s.%s", + SCHEMA_DESTINO, + payload["table_name"], + ) + return tabelas + + +@dag( + schedule_interval=get_dynamic_schedule("quilombolas_censo_dag"), + start_date=datetime(2026, 1, 1), + catchup=False, + default_args={ + "owner": "Lucas Guimaraes", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["quilombolas", "censo_demografico", "ibge", "alfabetizacao"], +) +def quilombolas_alfabetizacao_censo_dag() -> None: + """Extrai dados quilombolas do Censo 2022 via FTP e carrega em censo_demografico.""" + + @task + def listar_arquivos_dados() -> list[dict[str, str]]: + logging.info("[Task 1] Listando arquivos de dados no FTP...") + cliente = ClienteIBGE(database=_obter_database_ftp()) + arquivos = cliente.listar_arquivos_em_subpastas( + list(SUBPASTAS_DADOS), + formato_preferido="xlsx", + ) + logging.info("%d arquivo(s) de dados encontrado(s).", len(arquivos)) + return arquivos + + @task + def listar_arquivos_indices() -> list[dict[str, str]]: + logging.info("[Task 1b] Listando arquivos de índice no FTP...") + indices = ClienteIBGE(database=_obter_database_ftp()).listar_arquivos_texto( + INDICES_FTP + ) + logging.info("%d arquivo(s) de índice encontrado(s).", len(indices)) + return indices + + @task + def extrair_dados_excel(entrada: dict[str, str]) -> list[dict[str, Any]]: + subcaminho = entrada["subcaminho"] + arquivo = entrada["arquivo"] + logging.info("[Task 2] Extraindo %s/%s", subcaminho, arquivo) + + buffer = ClienteIBGE(database=_obter_database_ftp()).obter_conteudo_arquivo( + arquivo, subcaminho=subcaminho + ) + if not buffer: + raise ValueError(f"Falha ao baixar {subcaminho}/{arquivo}") + + chunks = extrair_chunks_de_excel(buffer, arquivo, subcaminho) + return [_chunk_para_payload(c) for c in chunks] + + @task + def extrair_indice(entrada: dict[str, str]) -> list[dict[str, Any]]: + subcaminho = entrada["subcaminho"] + arquivo = entrada["arquivo"] + logging.info("[Task 2b] Extraindo índice %s/%s", subcaminho, arquivo) + + conteudo = ClienteIBGE(database=_obter_database_ftp()).obter_conteudo_texto( + arquivo, subcaminho=subcaminho + ) + if not conteudo: + raise ValueError(f"Falha ao baixar índice {subcaminho}/{arquivo}") + + chunk = parsear_arquivo_indice(conteudo, subcaminho) + return [_chunk_para_payload(chunk)] + + @task + def inserir_chunks(payloads: list[dict[str, Any]]) -> list[str]: + if not payloads: + return [] + db = ClientPostgresDB(get_postgres_conn()) + return _inserir_payloads(db, payloads) + + @task + def consolidar_resultado( + tabelas_dados: list[list[str]], tabelas_indices: list[list[str]] + ) -> str: + total = sum(len(t) for t in tabelas_dados) + sum(len(t) for t in tabelas_indices) + return f"Processadas {total} tabelas no schema {SCHEMA_DESTINO}" + + lista_dados = listar_arquivos_dados() + lista_indices = listar_arquivos_indices() + + payloads_excel = extrair_dados_excel.expand(entrada=lista_dados) + payloads_indices = extrair_indice.expand(entrada=lista_indices) + + tabelas_dados = inserir_chunks.expand(payloads=payloads_excel) + tabelas_indices = inserir_chunks.expand(payloads=payloads_indices) + + consolidar_resultado(tabelas_dados, tabelas_indices) + + +quilombolas_alfabetizacao_censo_dag() diff --git a/airflow_lappis/dags/data_ingest/ipea_pro/ipea_pro_to_postgres_ingest_dag.py b/airflow_lappis/dags/data_ingest/ipea_pro/ipea_pro_to_postgres_ingest_dag.py new file mode 100644 index 00000000..1150b64a --- /dev/null +++ b/airflow_lappis/dags/data_ingest/ipea_pro/ipea_pro_to_postgres_ingest_dag.py @@ -0,0 +1,91 @@ +import logging +from datetime import datetime, timedelta +from typing import Dict, List, TypedDict + +from airflow.decorators import dag, task +from airflow.models import Variable +from cliente_postgres import ClientPostgresDB +from cliente_sqlserver import ClientSQLServerDB +from postgres_helpers import get_postgres_conn +from schedule_loader import get_dynamic_schedule + + +class SQLServerTableConfig(TypedDict): + source_schema: str + source_table: str + target_schema: str + target_table: str + primary_key: List[str] + + +TABLES_TO_SYNC_VARIABLE = "ipea_pro_tables_to_sync" + + +def _load_tables_from_variable() -> List[SQLServerTableConfig]: + return Variable.get( + TABLES_TO_SYNC_VARIABLE, + default_var=[], + deserialize_json=True, + ) + + +@dag( + schedule_interval=get_dynamic_schedule("ipea_pro_to_postgres_ingest_dag"), + start_date=datetime(2024, 1, 1), + catchup=False, + max_active_runs=1, + default_args={ + "owner": "Mateus", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["sql_server", "postgres", "ipea_pro"], +) +def sql_server_to_postgres_ingest_dag_ipea_pro() -> None: + """Replica tabelas do Ipea Pro para o Postgres Analytics.""" + + @task + def replicate_table(table_cfg: SQLServerTableConfig) -> Dict[str, int]: + sql_server = ClientSQLServerDB("ipeapro") + postgres = ClientPostgresDB(get_postgres_conn()) + source_schema = table_cfg["source_schema"] + source_table = table_cfg["source_table"] + target_schema = table_cfg["target_schema"] + target_table = table_cfg["target_table"] + primary_key = table_cfg["primary_key"] + + source_name = f"{source_schema}.{source_table}" + + logging.info( + "[ipea_pro_to_postgres_ingest_dag.py] Iniciando replicacao de %s", + source_name, + ) + + rows = sql_server.fetch_table_all( + schema=source_schema, + table_name=source_table, + ) + + if rows: + postgres.insert_data( + data=rows, + table_name=target_table, + conflict_fields=primary_key, + primary_key=primary_key, + schema=target_schema, + ) + + logging.info( + "[ipea_pro_to_postgres_ingest_dag.py] Replicacao concluida " + "para %s. Registros processados=%s", + source_name, + len(rows), + ) + + return {source_name: len(rows)} + + tables_to_sync = _load_tables_from_variable() + replicate_table.expand(table_cfg=tables_to_sync) + + +sql_server_to_postgres_ingest_dag_ipea_pro() diff --git a/airflow_lappis/dags/data_ingest/pncp/itens_resultados_licitacoes.py b/airflow_lappis/dags/data_ingest/pncp/itens_resultados_licitacoes.py new file mode 100644 index 00000000..533d15b1 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/pncp/itens_resultados_licitacoes.py @@ -0,0 +1,157 @@ +import logging +from datetime import datetime, timedelta + +from airflow.decorators import dag, task + +from postgres_helpers import get_postgres_conn +from cliente_postgres import ClientPostgresDB +from cliente_pncp import ClientePNCP + + +def padronizar_colunas_json(lista_de_dicts: list[dict]) -> list[dict]: + """ + Padroniza uma lista de dicionários para garantir que todos tenham as mesmas chaves. + Caso alguma coluna esteja ausente, será preenchida com None (null no banco). + + Args: + lista_de_dicts: Lista de dicionários JSON já flattenizados. + + Returns: + Lista de dicionários padronizados com todas as chaves presentes. + """ + todas_as_chaves: set[str] = set() + for item in lista_de_dicts: + todas_as_chaves.update(item.keys()) + + for item in lista_de_dicts: + for chave in todas_as_chaves: + item.setdefault(chave, None) + + return lista_de_dicts + + +@dag( + schedule_interval="@daily", + start_date=datetime(2024, 12, 5), + catchup=False, + default_args={ + "owner": "Mateus", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["pncp", "compras_publicas", "itens_resultados"], +) +def pncp_contratacoes_itens_resultados_dag() -> None: + """ + DAG responsável por buscar **ITENS** e **RESULTADOS** das contratações no PNCP. + + Fluxo: + 1. Ler da tabela `pncp.contratacoes_publicacao` todos os valores de + `numeroControlePNCP`. + 2. Para cada controle encontrado, chamar a API PNCP para obter: + - Lista de itens de contratação + - Lista de resultados de cada item + 3. Persistir os dados em duas tabelas distintas no schema `pncp`: + - `pncp.contratacoes_itens` + - `pncp.contratacoes_resultados` + + Observações: + - Cada registro recebe o campo `dt_ingest` com a data/hora da ingestão. + - Conflitos são resolvidos via upsert (`ON CONFLICT`). + """ + + @task + def fetch_and_store_itens_resultados() -> None: + # --- Configuração de clientes --- + api = ClientePNCP() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + dt_ingest_iso = datetime.now().isoformat() + + # --- Obter lista de numeroControlePNCP --- + try: + lista_tuplas = db.execute_query( + "SELECT numerocontrolepncp FROM pncp.contratacoes_publicacao" + ) + except Exception as e: + logging.error( + "[pncp_itens_resultados_dag] Erro ao buscar numeroControlePNCP: %s", e + ) + raise + + lista_controles = [t[0] for t in lista_tuplas if t and t[0]] + + logging.info( + "[pncp_itens_resultados_dag] Iniciando coleta | total_controles=%d", + len(lista_controles), + ) + + # --- Coleta na API --- + try: + itens, resultados_brutos = api.get_itens_e_resultados(lista_controles) + # itens_flat = db._flatten_data(itens) + resultados_flat = db._flatten_data(resultados_brutos) + # itens = padronizar_colunas_json(itens_flat) + resultados = padronizar_colunas_json(resultados_flat) + except Exception as e: + logging.error( + "[pncp_itens_resultados_dag] Erro ao coletar dados da API PNCP: %s", e + ) + raise + + # --- Enriquecer com dt_ingest --- + for r in itens: + if isinstance(r, dict): + r["dt_ingest"] = dt_ingest_iso + for r in resultados: + if isinstance(r, dict): + r["dt_ingest"] = dt_ingest_iso + + # --- Persistência --- + if itens: + db.insert_data( + itens, + table_name="contratacoes_itens", + schema="pncp", + conflict_fields=["numeroItem", "numeroControlePNCP"], + primary_key=["numeroItem", "numeroControlePNCP"], + ) + logging.info("[pncp_itens_resultados_dag] Inseridos %d itens.", len(itens)) + else: + logging.warning( + "[pncp_itens_resultados_dag] Nenhum item retornado para inserção." + ) + + if resultados: + db.insert_data( + resultados, + table_name="contratacoes_resultados", + schema="pncp", + conflict_fields=[ + "numeroItem", + "numeroControlePNCPCompra", + "sequencialResultado", + "situacaoCompraItemResultadoId", + ], + primary_key=[ + "numeroItem", + "numeroControlePNCPCompra", + "sequencialResultado", + "situacaoCompraItemResultadoId", + ], + ) + logging.info( + "[pncp_itens_resultados_dag] Inseridos %d resultados.", len(resultados) + ) + else: + logging.warning( + "[pncp_itens_resultados_dag] Nenhum resultado retornado para inserção." + ) + + logging.info("[pncp_itens_resultados_dag] Processo concluído com sucesso.") + + fetch_and_store_itens_resultados() + + +dag_instance = pncp_contratacoes_itens_resultados_dag() diff --git a/airflow_lappis/dags/data_ingest/pncp/licitacoes_ingest.py b/airflow_lappis/dags/data_ingest/pncp/licitacoes_ingest.py new file mode 100644 index 00000000..00f59bc6 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/pncp/licitacoes_ingest.py @@ -0,0 +1,129 @@ +import logging +from datetime import datetime, timedelta + +from airflow.decorators import dag, task +from airflow.models import Variable +import yaml + +from postgres_helpers import get_postgres_conn +from cliente_postgres import ClientPostgresDB +from cliente_pncp import ClientePNCP + + +@dag( + schedule_interval="@daily", + start_date=datetime(2024, 12, 4), + catchup=False, + default_args={ + "owner": "Mateus", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["pncp", "compras_publicas"], +) +def pncp_publicacoes_dag() -> None: + """ + DAG para buscar publicações de contratações no PNCP e armazenar no PostgreSQL. + Pega `pncp_codigo_modalidade_contratacao` e `pncp_cnpj` das Airflow Variables. + """ + + @task + def fetch_and_store_pncp_publicacoes() -> None: + + orgao_alvo = Variable.get("airflow_orgao", default_var=None) + if not orgao_alvo: + logging.error("Variável airflow_orgao não definida!") + raise ValueError("airflow_orgao não definida") + + orgaos_config_str = Variable.get("airflow_variables", default_var="{}") + orgaos_config = yaml.safe_load(orgaos_config_str) + + orgao_cfg = orgaos_config.get(orgao_alvo, {}) + cnpj_orgao = orgao_cfg.get("orgao_pncp", []) + modalidades_list = orgao_cfg.get("modalidade_pncp", []) + + try: + cnpj_orgao_int = int(cnpj_orgao) + except ValueError: + logging.error( + "[pncp_publicacoes_dag] Variável pncp_codigo_modalidade_contratacao " + "inválida: %r", + cnpj_orgao, + ) + raise + + end_date = datetime.today() + start_date = end_date - timedelta(weeks=260) + + data_inicial = start_date.strftime("%Y%m%d") + data_final = end_date.strftime("%Y%m%d") + + logging.info( + "[pncp_publicacoes_dag] Iniciando coleta | modalidade=%s | cnpj=%s | " + "janela=[%s, %s]", + modalidades_list, + cnpj_orgao_int, + data_inicial, + data_final, + ) + + # --- Clientes/API e DB --- + api = ClientePNCP() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + for cod_modalidade in modalidades_list: + + try: + cod_modalidade_int = int(cod_modalidade) + except ValueError: + logging.error( + "[pncp_publicacoes_dag] Variável pncp_codigo_modalidade_contratacao " + "inválida: %r", + cod_modalidade, + ) + raise + # --- Coleta semestral com paginação interna do cliente --- + registros = api.get_contratacoes_publicacao_semestral( + data_inicial=data_inicial, + data_final=data_final, + codigo_modalidade_contratacao=cod_modalidade_int, + cnpj=cnpj_orgao_int, + ) + + if not registros: + logging.warning( + "[pncp_publicacoes_dag] Nenhum registro retornado do PNCP." + ) + pass + + # Enriquecer com dt_ingest + dt_ingest_iso = datetime.now().isoformat() + for r in registros: + # garante que é dict antes de setar a chave + if isinstance(r, dict): + r["dt_ingest"] = dt_ingest_iso + + # --- Persistência --- + logging.info( + "[pncp_publicacoes_dag] Inserindo %s registros no schema " + "pncp.tabela=contratacoes_publicacao", + len(registros), + ) + + # Se você conhecer a(s) PK(s) de fato, preencha em + # primary_key/conflict_fields. + db.insert_data( + registros, + table_name="contratacoes_publicacao", + conflict_fields=["numeroControlePNCP"], + primary_key=["numeroControlePNCP"], + schema="pncp", + ) + + logging.info("[pncp_publicacoes_dag] Inserção concluída com sucesso.") + + fetch_and_store_pncp_publicacoes() + + +dag_instance = pncp_publicacoes_dag() diff --git a/airflow_lappis/dags/data_ingest/sgac/projetos_sgac_ingest_dag.py b/airflow_lappis/dags/data_ingest/sgac/projetos_sgac_ingest_dag.py new file mode 100644 index 00000000..cd9e5ef8 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/sgac/projetos_sgac_ingest_dag.py @@ -0,0 +1,205 @@ +from typing import Dict, Any, Optional +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +from datetime import datetime, timedelta +import logging +import json +from schedule_loader import get_dynamic_schedule +from cliente_email import fetch_and_process_email_csv_attachment +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn +import pandas as pd +import io + +# Configurações básicas da DAG +default_args = { + "owner": "Wallyson", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +COLUMN_MAPPING = { + 0: "odata_etag", + 1: "id_interno_item", + 2: "id", + 3: "titulo", + 4: "entidades_externas", + 5: "instrumento", + 6: "instrumento_id", + 7: "diretoria_responsavel", + 8: "diretoria_responsavel_id", + 9: "objeto", + 10: "data_inicio", + 11: "data_vencimento", + 12: "total_de_recursos", + 13: "numero_do_proc", + 14: "coordenador", + 15: "coordenador_tipo_odata", + 16: "coordenador_claims", + 17: "coordenador_claims_tipo_odata", + 18: "nacionalidade", + 19: "nacionalidade_tipo_odata", + 20: "nacionalidade_id", + 21: "nacionalidade_id_tipo_odata", + 22: "recursos_orcament_x00", + 23: "recursos_orcament_x0", + 24: "status", + 25: "status_id", + 26: "eixo_tematico", + 27: "eixo_tematico_tipo_odata", + 28: "eixo_tematico_id", + 29: "eixo_tematico_id_tipo_odata", + 30: "predecessores", + 31: "predecessores_tipo_odata", + 32: "predecessores_id", + 33: "predecessores_id_tipo_odata", + 34: "prioridade", + 35: "prioridade_id", + 36: "justificativa", + 37: "objetivo_s_ge", + 38: "equipe_tecnica", + 39: "equipe_tecnica_tipo_odata", + 40: "equipe_tecnica_claims", + 41: "equipe_tecnica_claims_tipo_odata", + 42: "codigo", + 43: "unidades_envolvidas", + 44: "unidades_envolvidas_tipo_odata", + 45: "unidades_envolvidas_id", + 46: "unidades_envolvidas_id_tipo_odata", + 47: "historico_observa_x0", + 48: "a_solicitacao", + 49: "a_solicitacao_tipo_odata", + 50: "a_solicitacao_id", + 51: "a_solicitacao_id_tipo_odata", + 52: "modificado", + 53: "criado", + 54: "autor", + 55: "autor_claims", + 56: "editor", + 57: "editor_claims", + 58: "identificador", + 59: "eh_pasta", + 60: "miniatura", + 61: "link", + 62: "nome", + 63: "nome_arquivo_com_extensao", + 64: "caminho", + 65: "caminho_completo", + 66: "tipo_conteudo", + 67: "tipo_conteudo_id", + 68: "possui_anexos", + 69: "numero_versao", + 70: "aprovacao", + 71: "termos_aditivos", + 72: "equipe", + 73: "percentual_concluido", + 74: "corpo", + 75: "fiscal_e_substituto", + 76: "numero_siafi", + 77: "apostilamentos", + 78: "prorrogacao_de_oficio", + 79: "atribuido_a", + 80: "atribuido_a_claims", +} + +EMAIL_SUBJECT = "SGAC" +SKIPROWS = 1 + +# Configurações da DAG +with DAG( + dag_id="email_projetos_sgac_ingest", + default_args=default_args, + description="Processa anexos do email de dados do SGAC e insere no db", + schedule_interval=get_dynamic_schedule("email_projetos_sgac_ingest"), + start_date=datetime(2023, 12, 1), + catchup=False, + tags=["email", "projetos", "sgac"], +) as dag: + + def process_email_data(**context: Dict[str, Any]) -> Optional[Any]: + creds = json.loads(Variable.get("email_credentials")) + + EMAIL = creds["email"] + PASSWORD = creds["password"] + IMAP_SERVER = creds["imap_server"] + SENDER_EMAIL = Variable.get("sender_email_sgac", default_var=creds["sender_email"],) + + try: + logging.info("Iniciando o processamento dos emails...") + csv_data = fetch_and_process_email_csv_attachment( + IMAP_SERVER, + EMAIL, + PASSWORD, + SENDER_EMAIL, + EMAIL_SUBJECT, + COLUMN_MAPPING, + skiprows=SKIPROWS, + ) + if not csv_data: + logging.warning("Nenhum e-mail encontrado com o assunto esperado.") + return None + + total_linhas = max(len(csv_data.splitlines()) - 1, 0) + logging.info( + "CSV processado com sucesso. Dados encontrados: %s", total_linhas + ) + return csv_data + except Exception as e: + logging.error("Erro no processamento dos emails: %s", str(e)) + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + """Insere no Postgres os dados retornados pela task de processamento do e-mail.""" + try: + task_instance: Any = context["ti"] + csv_data: Any = task_instance.xcom_pull(task_ids="process_emails") + + if not csv_data: + logging.warning("Nenhum dado para inserir no banco.") + return + + df = pd.read_csv(io.StringIO(csv_data)) + if df.empty: + logging.warning("CSV recebido sem registros para insercao.") + return + + data = df.to_dict(orient="records") + # Adiciona timestamp de ingestão a cada registro + for record in data: + record["dt_ingest"] = datetime.now().isoformat() + + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + unique_key = ["id"] + + db.insert_data( + data, + "projetos_sgac", + conflict_fields=unique_key, + primary_key=unique_key, + schema="sgac", + ) + logging.info("Dados inseridos com sucesso no banco de dados.") + except Exception as e: + logging.error("Erro ao inserir dados no banco: %s", str(e)) + raise + + #tarefa 1: processar os e-mails e extrair o CSV + process_emails_task = PythonOperator( + task_id="process_emails", + python_callable=process_email_data, + provide_context=True, + ) + #tarefa 2: inserir os dados no banco de dados + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + provide_context=True, + ) + #Fluxo da DAG + process_emails_task >> insert_to_db_task + + diff --git a/airflow_lappis/dags/data_ingest/siafi/nota_credito_siafi_ingest_dag.py b/airflow_lappis/dags/data_ingest/siafi/nota_credito_siafi_ingest_dag.py new file mode 100644 index 00000000..242fe379 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siafi/nota_credito_siafi_ingest_dag.py @@ -0,0 +1,59 @@ +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from cliente_siafi import ClienteSiafi +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn + + +@dag( + schedule_interval=get_dynamic_schedule("nota_credito_siafi_ingest_dag"), + start_date=datetime(2024, 3, 12), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["nota_credito", "siafi_api"], +) +def nota_credito_siafi_dag() -> None: + @task + def fetch_and_store_nota_credito() -> None: + cliente_siafi = ClienteSiafi() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + notas_credito = db.get_nota_credito() + + for nota_credito in notas_credito: + cd_ug_emitente_nota = nota_credito[0] + cd_gestao_emitente_nota = nota_credito[1] + tx_numero_nota = nota_credito[2] + + if not all([cd_ug_emitente_nota, cd_gestao_emitente_nota, tx_numero_nota]): + continue + + ano = tx_numero_nota[:4] + numero = tx_numero_nota[-6:] + + response = cliente_siafi.consultar_nota_credito( + ug=cd_ug_emitente_nota, + gestao=cd_gestao_emitente_nota, + ano=ano, + numero=numero, + ) + if response: + response["ano"] = ano + response["dt_ingest"] = datetime.now().isoformat() + db.insert_data( + [response], + "nota_credito", + conflict_fields=["numero", "ano"], + primary_key=["numero", "ano"], + schema="siafi", + ) + + fetch_and_store_nota_credito() + + +dag_instance = nota_credito_siafi_dag() diff --git a/airflow_lappis/dags/data_ingest/siafi/nota_empenho_siafi_ingest_dag.py b/airflow_lappis/dags/data_ingest/siafi/nota_empenho_siafi_ingest_dag.py new file mode 100644 index 00000000..32a9668a --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siafi/nota_empenho_siafi_ingest_dag.py @@ -0,0 +1,93 @@ +import logging +import yaml +from airflow.decorators import dag, task +from airflow.models import Variable +from airflow.models.param import Param +from datetime import datetime, timedelta +from typing import Dict, Any +from schedule_loader import get_dynamic_schedule +from cliente_siafi import ClienteSiafi +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn + + +@dag( + schedule_interval=get_dynamic_schedule("nota_empenho_siafi_ingest_dag"), + start_date=datetime(2023, 3, 17), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + params={ + "ano_inicio": Param( + default=None, + type=["integer", "null"], + title="Ano de Início", + description="Backfill: Ano inicio para busca de notas de empenho. (type=int)", + ), + "ano_fim": Param( + default=None, + type=["integer", "null"], + title="Ano de Fim", + description="Backfill: Ano final para busca de notas de empenho. (type=int)", + ), + }, + tags=["nota_empenho", "siafi_api"], +) +def nota_empenho_siafi_ingest_dag() -> None: + @task + def fetch_and_store_notas_empenho(**context: Dict[str, Any]) -> None: + logging.info("Iniciando fetch_and_store_notas_empenho") + + orgao_alvo = Variable.get("airflow_orgao", default_var=None) + if not orgao_alvo: + logging.error("Variável airflow_orgao não definida!") + raise ValueError("airflow_orgao não definida") + + orgaos_config_str = Variable.get("airflow_variables", default_var="{}") + orgaos_config = yaml.safe_load(orgaos_config_str) + + ugs_emitentes = orgaos_config.get(orgao_alvo, {}).get("codigos_ug", []) + + if not ugs_emitentes: + logging.warning(f"Nenhum código UG encontrado para o órgão '{orgao_alvo}'") + return + + cliente = ClienteSiafi() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + params = context["params"] + ano_inicio = params.get("ano_inicio") + ano_fim = params.get("ano_fim") + + ano_atual = datetime.now().year + ano_inicio = ano_inicio or ano_atual + ano_fim = ano_fim or ano_atual + + anos_consulta = list(range(ano_inicio, ano_fim + 1)) + + for ug in ugs_emitentes: + for ano in anos_consulta: + num_empenho = 1 + while True: + num_empenho_str = str(num_empenho).zfill(6) + resultado = cliente.consultar_nota_empenho(ug, ano, num_empenho_str) + if not resultado: + break + resultado["dt_ingest"] = datetime.now().isoformat() + db.insert_data( + [resultado], + "notas_empenho", + conflict_fields=["numEmpenho", "anoEmpenho"], + primary_key=["numEmpenho", "anoEmpenho"], + schema="siafi", + ) + num_empenho += 1 + + fetch_and_store_notas_empenho() + + +dag_instance = nota_empenho_siafi_ingest_dag() diff --git a/airflow_lappis/dags/data_ingest/siafi/programacao_financeira_siafi_ingest_dag.py b/airflow_lappis/dags/data_ingest/siafi/programacao_financeira_siafi_ingest_dag.py new file mode 100644 index 00000000..4c277135 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siafi/programacao_financeira_siafi_ingest_dag.py @@ -0,0 +1,50 @@ +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from cliente_siafi import ClienteSiafi +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn + + +@dag( + schedule_interval=get_dynamic_schedule("programacao_financeira_siafi_ingest_dag"), + start_date=datetime(2024, 3, 12), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["programacao_financeira", "siafi_api"], +) +def programacao_financeira_siafi_dag() -> None: + @task + def fetch_and_store_programacao_financeira() -> None: + cliente_siafi = ClienteSiafi() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + programacoes = db.get_programacao_financeira() + + for programacao in programacoes: + tx_numero_programacao, ug_emitente_programacao = programacao + ano = int(str(tx_numero_programacao)[:4]) + num_lista = int(str(tx_numero_programacao)[-6:]) + ug_emitente = int(ug_emitente_programacao) + + response = cliente_siafi.consultar_programacao_financeira( + ug_emitente, ano, num_lista + ) + if response: + response["dt_ingest"] = datetime.now().isoformat() + db.insert_data( + [response], + "programacao_financeira_siafi", + conflict_fields=["TRF__numeroDocumento"], + primary_key=["TRF__numeroDocumento"], + schema="siafi", + ) + + fetch_and_store_programacao_financeira() + + +dag_instance = programacao_financeira_siafi_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/dados_afastamento_historico_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/dados_afastamento_historico_siape_ingest_dag.py new file mode 100644 index 00000000..290cb179 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/dados_afastamento_historico_siape_ingest_dag.py @@ -0,0 +1,95 @@ +import os +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule( + "dados_afastamento_historico_siape_ingest_dag" + ), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "afastamento_historico"], +) +def siape_afastamento_historico_dag() -> None: + """ + DAG que consome o endpoint consultaDadosAfastamentoHistorico da API SIAPE + e armazena os dados de afastamentos antigos dos servidores no schema 'siape'. + """ + + @task + def fetch_and_store_afastamento_historico() -> None: + logging.info("Iniciando extração de dados de afastamento histórico por CPF") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + query = "SELECT DISTINCT cpf FROM siape.lista_servidores WHERE cpf IS NOT NULL" + cpfs = [row[0] for row in db.execute_query(query)] + logging.info(f"Total de CPFs encontrados: {len(cpfs)}") + + for cpf in cpfs: + try: + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": cpf, + "codOrgao": "45206", + "parmExistPag": "b", + "parmTipoVinculo": "c", + "anoInicial": "2024", + "mesInicial": "01", + "anoFinal": "2025", + "mesFinal": "12", + } + + resposta_xml = cliente_siape.call( + "consultaDadosAfastamentoHistorico.xml.j2", context + ) + + dados = ClienteSiape.parse_afastamento_historico(resposta_xml) + + if not dados: + logging.info(f"Nenhum dado de afastamento histórico para CPF {cpf}") + continue + + for row in dados: + row["cpf"] = cpf + row["dt_ingest"] = datetime.now().isoformat() + + if dados: + db.alter_table( + data=dados[0], + table_name="afastamento_historico", + schema="siape", + ) + + db.insert_data( + dados, + table_name="afastamento_historico", + conflict_fields=None, + primary_key=None, + schema="siape", + ) + + logging.info(f"{len(dados)} registros inseridos para CPF {cpf}") + + except Exception as e: + logging.error(f"Erro ao processar CPF {cpf}: {e}") + continue + + fetch_and_store_afastamento_historico() + + +dag_instance = siape_afastamento_historico_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/dados_afastamento_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/dados_afastamento_siape_ingest_dag.py new file mode 100644 index 00000000..46acd096 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/dados_afastamento_siape_ingest_dag.py @@ -0,0 +1,87 @@ +import os +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("dados_afastamento_siape_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "dados_afastamento"], +) +def siape_dados_afastamento_dag() -> None: + """ + DAG que consome o endpoint consultaDadosAfastamento da API SIAPE + e armazena dados de afastamento atuais no schema 'siape'. + """ + + @task + def fetch_and_store_dados_afastamento() -> None: + logging.info("Iniciando extração de dados de afastamento por CPF") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + query = "SELECT DISTINCT cpf FROM siape.lista_servidores WHERE cpf IS NOT NULL" + cpfs = [row[0] for row in db.execute_query(query)] + logging.info(f"Total de CPFs encontrados: {len(cpfs)}") + + for cpf in cpfs: + try: + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": cpf, + "codOrgao": "45206", + "parmExistPag": "b", + "parmTipoVinculo": "c", + } + + resposta_xml = cliente_siape.call( + "consultaDadosAfastamento.xml.j2", context + ) + dados = ClienteSiape.parse_xml_to_dict(resposta_xml) + + if not dados: + logging.warning(f"Nenhum dado de afastamento para CPF {cpf}") + continue + + dados["cpf"] = cpf + dados["dt_ingest"] = datetime.now().isoformat() + + if dados: + db.alter_table( + data=dados, + table_name="dados_afastamento", + schema="siape", + ) + + db.insert_data( + [dados], + table_name="dados_afastamento", + conflict_fields=["cpf"], + primary_key=["cpf"], + schema="siape", + ) + + logging.info(f"Dado de afastamento inserido para CPF {cpf}") + + except Exception as e: + logging.error(f"Erro ao processar CPF {cpf}: {e}") + continue + + fetch_and_store_dados_afastamento() + + +dag_instance = siape_dados_afastamento_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/dados_curriculo_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/dados_curriculo_siape_ingest_dag.py new file mode 100644 index 00000000..ab8fcde2 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/dados_curriculo_siape_ingest_dag.py @@ -0,0 +1,85 @@ +import os +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("dados_curriculo_siape_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "dados_curriculo"], +) +def siape_dados_curriculo_dag() -> None: + """ + DAG que consome o endpoint consultaDadosCurriculo da API SIAPE + e armazena os dados de currículo dos servidores no schema 'siape'. + """ + + @task + def fetch_and_store_dados_curriculo() -> None: + logging.info("Iniciando extração de dados de currículo por CPF") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + query = "SELECT DISTINCT cpf FROM siape.lista_servidores WHERE cpf IS NOT NULL" + cpfs = [row[0] for row in db.execute_query(query)] + logging.info(f"Total de CPFs encontrados: {len(cpfs)}") + + for cpf in cpfs: + try: + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": cpf, + "codOrgao": "45206", + "parmExistPag": "b", + "parmTipoVinculo": "c", + } + + resposta_xml = cliente_siape.call( + "consultaDadosCurriculo.xml.j2", context + ) + dados = ClienteSiape.parse_xml_to_dict(resposta_xml) + + if not dados: + logging.warning(f"Nenhum dado de currículo encontrado para CPF {cpf}") + continue + + dados["dt_ingest"] = datetime.now().isoformat() + + db.alter_table( + data=dados, + table_name="dados_curriculo", + schema="siape", + ) + + db.insert_data( + [dados], + table_name="dados_curriculo", + conflict_fields=["cpf"], + primary_key=["cpf"], + schema="siape", + ) + + logging.info(f"Dado de currículo inserido para CPF {cpf}") + + except Exception as e: + logging.error(f"Erro ao processar CPF {cpf}: {e}") + continue + + fetch_and_store_dados_curriculo() + + +dag_instance = siape_dados_curriculo_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/dados_dependentes_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/dados_dependentes_siape_ingest_dag.py new file mode 100644 index 00000000..dc198bc1 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/dados_dependentes_siape_ingest_dag.py @@ -0,0 +1,87 @@ +import os +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("dados_dependentes_siape_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "dados_dependentes"], +) +def siape_dados_dependentes_dag() -> None: + """ + DAG que consome o endpoint consultaDadosDependentes da API SIAPE + e armazena os dados de dependentes dos servidores no schema 'siape'. + """ + + @task + def fetch_and_store_dados_dependentes() -> None: + logging.info("Iniciando extração de dados de dependentes por CPF") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + query = "SELECT DISTINCT cpf FROM siape.lista_servidores WHERE cpf IS NOT NULL" + cpfs = [row[0] for row in db.execute_query(query)] + logging.info(f"Total de CPFs encontrados: {len(cpfs)}") + + for cpf in cpfs: + try: + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": cpf, + "codOrgao": "45206", + "parmExistPag": "b", + "parmTipoVinculo": "c", + } + + resposta_xml = cliente_siape.call( + "consultaDadosDependentes.xml.j2", context + ) + dados = ClienteSiape.parse_dependentes(resposta_xml) + + if not dados: + logging.warning(f"Nenhum dependente encontrado para CPF {cpf}") + continue + + for row in dados: + row["cpf"] = cpf + row["dt_ingest"] = datetime.now().isoformat() + + db.alter_table( + data=dados[0], + table_name="dados_dependentes", + schema="siape", + ) + + db.insert_data( + dados, + table_name="dados_dependentes", + conflict_fields=["cpf", "nome"], + primary_key=["cpf", "nome"], + schema="siape", + ) + + logging.info(f"{len(dados)} dependente(s) inserido(s) para CPF {cpf}") + + except Exception as e: + logging.error(f"Erro ao processar CPF {cpf}: {e}") + continue + + fetch_and_store_dados_dependentes() + + +dag_instance = siape_dados_dependentes_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/dados_escolares_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/dados_escolares_siape_ingest_dag.py new file mode 100644 index 00000000..09912607 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/dados_escolares_siape_ingest_dag.py @@ -0,0 +1,86 @@ +import os +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("dados_escolares_siape_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "dados_escolares"], +) +def siape_dados_escolares_dag() -> None: + """ + DAG que consome o endpoint consultaDadosEscolares da API SIAPE + e armazena os dados de escolaridade dos servidores no schema 'siape'. + """ + + @task + def fetch_and_store_dados_escolares() -> None: + logging.info("Iniciando extração de dados escolares por CPF") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + query = "SELECT DISTINCT cpf FROM siape.lista_servidores WHERE cpf IS NOT NULL" + cpfs = [row[0] for row in db.execute_query(query)] + logging.info(f"Total de CPFs encontrados: {len(cpfs)}") + + for cpf in cpfs: + try: + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": cpf, + "codOrgao": "45206", + "parmExistPag": "b", + "parmTipoVinculo": "c", + } + + resposta_xml = cliente_siape.call( + "consultaDadosEscolares.xml.j2", context + ) + dados = ClienteSiape.parse_xml_to_dict(resposta_xml) + + if not dados: + logging.warning(f"Nenhum dado escolar encontrado para CPF {cpf}") + continue + + dados["cpf"] = cpf + dados["dt_ingest"] = datetime.now().isoformat() + + db.alter_table( + data=dados, + table_name="dados_escolares", + schema="siape", + ) + + db.insert_data( + [dados], + table_name="dados_escolares", + conflict_fields=["cpf"], + primary_key=["cpf"], + schema="siape", + ) + + logging.info(f"Dado escolar inserido para CPF {cpf}") + + except Exception as e: + logging.error(f"Erro ao processar CPF {cpf}: {e}") + continue + + fetch_and_store_dados_escolares() + + +dag_instance = siape_dados_escolares_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/dados_financeiros_siape_dag.py b/airflow_lappis/dags/data_ingest/siape/dados_financeiros_siape_dag.py new file mode 100644 index 00000000..cc20626b --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/dados_financeiros_siape_dag.py @@ -0,0 +1,86 @@ +import os +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("dados_financeiros_siape_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "dados_financeiros"], +) +def siape_dados_financeiros_dag() -> None: + """ + DAG que consome o endpoint consultaDadosFinanceiros da API SIAPE + e armazena os dados financeiros dos servidores no schema 'siape'. + """ + + @task + def fetch_and_store_dados_financeiros() -> None: + logging.info("Iniciando extração de dados financeiros por CPF") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + query = "SELECT DISTINCT cpf FROM siape.lista_servidores WHERE cpf IS NOT NULL" + cpfs = [row[0] for row in db.execute_query(query)] + logging.info(f"Total de CPFs encontrados: {len(cpfs)}") + + for cpf in cpfs: + try: + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": cpf, + "codOrgao": "45206", + "parmExistPag": "b", + "parmTipoVinculo": "c", + } + + resposta_xml = cliente_siape.call( + "consultaDadosFinanceiros.xml.j2", context + ) + dados = ClienteSiape.parse_xml_to_dict(resposta_xml) + + if not dados: + logging.warning(f"Nenhum dado financeiro encontrado para CPF {cpf}") + continue + + dados["cpf"] = cpf + dados["dt_ingest"] = datetime.now().isoformat() + + db.alter_table( + data=dados, + table_name="dados_financeiros", + schema="siape", + ) + + db.insert_data( + [dados], + table_name="dados_financeiros", + conflict_fields=["cpf"], + primary_key=["cpf"], + schema="siape", + ) + + logging.info(f"Dado financeiro inserido para CPF {cpf}") + + except Exception as e: + logging.error(f"Erro ao processar CPF {cpf}: {e}") + continue + + fetch_and_store_dados_financeiros() + + +dag_instance = siape_dados_financeiros_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/dados_funcionais_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/dados_funcionais_siape_ingest_dag.py new file mode 100644 index 00000000..8d5485a8 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/dados_funcionais_siape_ingest_dag.py @@ -0,0 +1,82 @@ +import os +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("dados_funcionais_siape_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "dados_funcionais"], +) +def siape_dados_funcionais_dag() -> None: + + @task + def fetch_and_store_dados_funcionais() -> None: + logging.info("Iniciando extração de dados funcionais por CPF") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + query = "SELECT DISTINCT cpf FROM siape.lista_servidores WHERE cpf IS NOT NULL" + cpfs = [row[0] for row in db.execute_query(query)] + logging.info(f"Total de CPFs encontrados: {len(cpfs)}") + + for cpf in cpfs: + try: + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": cpf, + "codOrgao": "45206", + "parmExistPag": "b", + "parmTipoVinculo": "c", + } + + resposta_xml = cliente_siape.call( + "consultaDadosFuncionais.xml.j2", context + ) + dados = ClienteSiape.parse_dado_funcional(resposta_xml) + + if not dados: + logging.warning(f"Nenhum dado funcional encontrado para CPF {cpf}") + continue + + dados["cpf"] = cpf + dados["dt_ingest"] = datetime.now().isoformat() + + db.alter_table( + data=dados, + table_name="dados_funcionais", + schema="siape", + ) + + db.insert_data( + [dados], + table_name="dados_funcionais", + conflict_fields=["cpf"], + primary_key=["cpf"], + schema="siape", + ) + + logging.info(f"Dado funcional inserido para CPF {cpf}") + + except Exception as e: + logging.error(f"Erro ao processar CPF {cpf}: {e}") + continue + + fetch_and_store_dados_funcionais() + + +dag_instance = siape_dados_funcionais_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/dados_pa_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/dados_pa_siape_ingest_dag.py new file mode 100644 index 00000000..4fb35850 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/dados_pa_siape_ingest_dag.py @@ -0,0 +1,121 @@ +import os +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("dados_pa_siape_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "dados_pa"], +) +def siape_dados_pa_dag() -> None: + """ + DAG que consome o endpoint consultaDadosPA da API SIAPE + e armazena o plano de atuação dos servidores no schema 'siape'. + """ + + @task + def fetch_and_store_dados_pa() -> None: + logging.info("Iniciando extração de dados de plano de atuação por CPF") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + # Garante que schema, tabela e chave primária existam + ddl = """ + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.schemata WHERE schema_name = 'siape' + ) THEN + EXECUTE 'CREATE SCHEMA siape'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'siape' AND table_name = 'dados_pa' + ) THEN + EXECUTE ' + CREATE TABLE siape.dados_pa ( + cpf_servidor TEXT PRIMARY KEY + ) + '; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'siape' + AND table_name = 'dados_pa' + AND constraint_type = 'PRIMARY KEY' + ) THEN + EXECUTE 'ALTER TABLE siape.dados_pa ADD CONSTRAINT dados_pa_pkey ' || + 'PRIMARY KEY (cpf_servidor)'; + END IF; + END + $$; + """ + db.execute_non_query(ddl) # Assumindo que esse método executa sem fetch + logging.info("Estrutura da tabela verificada/criada com sucesso.") + + query = "SELECT DISTINCT cpf FROM siape.lista_servidores WHERE cpf IS NOT NULL" + cpfs = [row[0] for row in db.execute_query(query)] + logging.info(f"Total de CPFs encontrados: {len(cpfs)}") + + for cpf in cpfs: + try: + logging.info(f"Processando CPF: {cpf}") + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": cpf, + "codOrgao": "45206", + "parmExistPag": "b", + "parmTipoVinculo": "c", + } + + resposta_xml = cliente_siape.call("consultaDadosPA.xml.j2", context) + dados = ClienteSiape.parse_xml_to_dict(resposta_xml) + + if not dados: + logging.warning(f"Nenhum dado PA encontrado para CPF {cpf}") + continue + + dados["cpf_servidor"] = cpf + dados["dt_ingest"] = datetime.now().isoformat() + + db.alter_table( + data=dados, + table_name="dados_pa", + schema="siape", + ) + + db.insert_data( + [dados], + table_name="dados_pa", + conflict_fields=["cpf_servidor"], + primary_key=["cpf_servidor"], + schema="siape", + ) + + logging.info(f"Plano de atuação inserido para CPF {cpf}") + + except Exception as e: + logging.error(f"Erro ao processar CPF {cpf}: {e}", exc_info=True) + continue + + fetch_and_store_dados_pa() + + +dag_instance = siape_dados_pa_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/dados_pessoais_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/dados_pessoais_siape_ingest_dag.py new file mode 100644 index 00000000..d8447e5e --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/dados_pessoais_siape_ingest_dag.py @@ -0,0 +1,141 @@ +import os +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("dados_pessoais_siape_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "dados_pessoais"], +) +def siape_dados_pessoais_dag() -> None: + """ + DAG que consome o endpoint consultaDadosPessoais da API SIAPE + e armazena os dados pessoais de servidores públicos no schema 'siape'. + """ + + @task + def fetch_and_store_dados_pessoais() -> None: + logging.info("Iniciando extração de dados pessoais por CPF") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + # --- Garantia de schema, tabela e PRIMARY KEY --- + logging.info("Verificando existência da tabela e constraint PRIMARY KEY") + ddl = """ + DO $$ + BEGIN + -- Cria schema se não existir + IF NOT EXISTS ( + SELECT 1 FROM information_schema.schemata WHERE schema_name = 'siape' + ) THEN + EXECUTE 'CREATE SCHEMA siape'; + END IF; + + -- Cria tabela se não existir + IF NOT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'siape' AND table_name = 'dados_pessoais' + ) THEN + EXECUTE ' + CREATE TABLE siape.dados_pessoais ( + cpf TEXT PRIMARY KEY -- Define cpf como PK já na criação + ) + '; + END IF; + + -- Adiciona PK se a tabela existe mas ainda não tem + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'siape' + AND table_name = 'dados_pessoais' + AND constraint_type = 'PRIMARY KEY' + ) THEN + EXECUTE ( + 'ALTER TABLE siape.dados_pessoais ADD CONSTRAINT dados_pessoais_pkey ' + 'PRIMARY KEY (cpf)' + ); + END IF; + END + $$; + """ + db.execute_non_query(ddl) + logging.info("Estrutura da tabela verificada/criada com sucesso.") + + # --- Continua fluxo normal --- + query = "SELECT DISTINCT cpf FROM siape.lista_servidores WHERE cpf IS NOT NULL" + cpfs = [row[0] for row in db.execute_query(query)] + total_cpfs = len(cpfs) + logging.info(f"Total de CPFs encontrados: {total_cpfs}") + + for idx, cpf in enumerate(cpfs, 1): + try: + logging.info(f"Consultando CPF {cpf} [{idx}/{total_cpfs}]") + + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": cpf, + "codOrgao": "45206", + "parmExistPag": "b", + "parmTipoVinculo": "c", + } + + resposta_xml = cliente_siape.call("consultaDadosPessoais.xml.j2", context) + logging.debug(f"XML bruto para CPF {cpf}:\n{resposta_xml}") + + dados = ClienteSiape.parse_xml_to_dict(resposta_xml) + logging.debug(f"Dados parseados para CPF {cpf}: {dados}") + + if not dados: + logging.warning( + f"Nenhum dado encontrado para CPF {cpf} [{idx}/{total_cpfs}]" + ) + continue + + dados["cpf"] = cpf + dados["dt_ingest"] = datetime.now().isoformat() + logging.info(f"Dados finais prontos para inserção: {dados}") + + db.alter_table( + data=dados, + table_name="dados_pessoais", + schema="siape", + ) + + db.insert_data( + [dados], + table_name="dados_pessoais", + conflict_fields=["cpf"], + primary_key=["cpf"], + schema="siape", + ) + + logging.info( + f"Dado inserido com sucesso para CPF {cpf} [{idx}/{total_cpfs}]" + ) + + except Exception as e: + logging.error( + f"Erro ao processar CPF {cpf} [{idx}/{total_cpfs}]: {e}", + exc_info=True, + ) + continue + + fetch_and_store_dados_pessoais() + + +dag_instance = siape_dados_pessoais_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/dados_uorg_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/dados_uorg_siape_ingest_dag.py new file mode 100644 index 00000000..1220ab79 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/dados_uorg_siape_ingest_dag.py @@ -0,0 +1,84 @@ +import os +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("dados_uorg_siape_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "dados_uorg"], +) +def siape_dados_uorg_dag() -> None: + """ + DAG que consome o endpoint consultaDadosUorg da API SIAPE + e armazena os dados da unidade organizacional do servidor no schema 'siape'. + """ + + @task + def fetch_and_store_dados_uorg() -> None: + logging.info("Iniciando extração de dados de UORG por CPF") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + query = "SELECT DISTINCT cpf FROM siape.lista_servidores WHERE cpf IS NOT NULL" + cpfs = [row[0] for row in db.execute_query(query)] + logging.info(f"Total de CPFs encontrados: {len(cpfs)}") + + for cpf in cpfs: + try: + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": cpf, + "codOrgao": "45206", + "parmExistPag": "b", + "parmTipoVinculo": "c", + } + + resposta_xml = cliente_siape.call("consultaDadosUorg.xml.j2", context) + dados = ClienteSiape.parse_xml_to_dict(resposta_xml) + + if not dados: + logging.warning(f"Nenhum dado de UORG encontrado para CPF {cpf}") + continue + + dados["cpf"] = cpf + dados["dt_ingest"] = datetime.now().isoformat() + + db.alter_table( + data=dados, + table_name="dados_uorg", + schema="siape", + ) + + db.insert_data( + [dados], + table_name="dados_uorg", + conflict_fields=["cpf"], + primary_key=["cpf"], + schema="siape", + ) + + logging.info(f"Dado de UORG inserido para CPF {cpf}") + + except Exception as e: + logging.error(f"Erro ao processar CPF {cpf}: {e}") + continue + + fetch_and_store_dados_uorg() + + +dag_instance = siape_dados_uorg_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/lista_aposentadoria_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/lista_aposentadoria_siape_ingest_dag.py new file mode 100644 index 00000000..4afbb3be --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/lista_aposentadoria_siape_ingest_dag.py @@ -0,0 +1,90 @@ +import os +import time +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("lista_aposentadoria_siape_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "aposentadoria"], +) +def siape_lista_info_aposentadoria_dag() -> None: + """ + DAG que consome o endpoint listaInformacoesAposentadoria da API SIAPE + e armazena os dados no schema 'siape'. + """ + + @task + def fetch_and_store_aposentadoria_info() -> None: + logging.info("Iniciando extração de informações de aposentadoria por CPF") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + query = """ + SELECT DISTINCT cpf, matriculasiape + FROM siape.dados_funcionais + WHERE cpf IS NOT NULL AND matriculasiape IS NOT NULL + """ + registros = db.execute_query(query) + logging.info(f"Total de registros encontrados: {len(registros)}") + + for cpf, matricula in registros: + try: + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": cpf, + "orgao": "45206", + "matricula": matricula, + } + + resposta_xml = cliente_siape.call( + "listaInformacoesAposentadoria.xml.j2", context + ) + dados = ClienteSiape.parse_xml_to_dict(resposta_xml) + + if not dados: + logging.warning(f"Nenhum dado encontrado para CPF {cpf}") + continue + + dados["dt_ingest"] = datetime.now().isoformat() + + db.alter_table( + data=dados, + table_name="info_aposentadoria", + schema="siape", + ) + db.insert_data( + [dados], + table_name="info_aposentadoria", + conflict_fields=["cpf"], + primary_key=["cpf"], + schema="siape", + ) + + logging.info(f"Dado de aposentadoria inserido para CPF {cpf}") + + except Exception as e: + logging.error(f"Erro ao processar CPF {cpf}: {e}") + continue + + time.sleep(0.1) + + fetch_and_store_aposentadoria_info() + + +dag_instance = siape_lista_info_aposentadoria_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/lista_servidores_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/lista_servidores_siape_ingest_dag.py new file mode 100644 index 00000000..69eb340f --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/lista_servidores_siape_ingest_dag.py @@ -0,0 +1,82 @@ +import os +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("lista_servidores_siape_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "lista_servidores"], +) +def siape_lista_servidores_dag() -> None: + + @task + def fetch_and_store_lista_servidores() -> None: + logging.info("Iniciando extração de servidores por UORG") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + query = "SELECT codigo FROM siape.lista_uorgs" + codigos_uorg = [row[0] for row in db.execute_query(query)] + logging.info(f"Total de UORGs encontradas: {len(codigos_uorg)}") + + ns = { + "soapenv": "http://schemas.xmlsoap.org/soap/envelope/", + "ns1": "http://servico.wssiapenet", + "ns2": "http://entidade.wssiapenet", + } + + for cod in codigos_uorg: + try: + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": os.getenv("SIAPE_CPF_USER"), + "codOrgao": "45206", + "codUorg": cod, + } + + resposta_xml = cliente_siape.call("listaServidores.xml.j2", context) + dados = ClienteSiape.parse_xml_to_list( + xml_string=resposta_xml, element_tag="ns2:Servidor", namespaces=ns + ) + + if not dados: + logging.info(f"Nenhum servidor encontrado para UORG {cod}") + continue + + for row in dados: + row["codUorg"] = str(cod) + row["dt_ingest"] = datetime.now().isoformat() + + db.insert_data( + dados, + table_name="lista_servidores", + conflict_fields=["cpf", "codUorg"], + primary_key=["cpf", "codUorg"], + schema="siape", + ) + + logging.info(f"{len(dados)} servidores inseridos para UORG {cod}") + + except Exception as e: + logging.error(f"Erro ao processar UORG {cod}: {e}") + continue + + fetch_and_store_lista_servidores() + + +dag_instance = siape_lista_servidores_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/lista_uorgs_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/lista_uorgs_siape_ingest_dag.py new file mode 100644 index 00000000..539aa34b --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/lista_uorgs_siape_ingest_dag.py @@ -0,0 +1,113 @@ +import os +import logging +from datetime import datetime +from airflow.decorators import dag, task +from datetime import timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("lista_uorgs_siape_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Joyce", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "lista_uorgs"], +) +def siape_lista_uorgs_dag() -> None: + """ + DAG que extrai a lista de UORGs do SIAPE via API SOAP + e insere no schema 'siape', tabela 'lista_uorgs'. + """ + + @task + def fetch_and_store_lista_uorgs() -> None: + logging.info("[siape_lista_uorgs_dag] Iniciando extração da lista de UORGs") + + cliente_siape = ClienteSiape() + + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": os.getenv("SIAPE_CPF_USER"), + "codOrgao": "45206", + } + + resposta_xml = cliente_siape.call("listaUorgs.xml.j2", context) + + ns = { + "soapenv": "http://schemas.xmlsoap.org/soap/envelope/", + "ns1": "http://servico.wssiapenet", + "ns2": "http://entidade.wssiapenet", + } + + dados_lista = ClienteSiape.parse_xml_to_list( + xml_string=resposta_xml, element_tag="ns2:Uorg", namespaces=ns + ) + + if not dados_lista: + logging.warning("Nenhum dado retornado da API listaUorgs") + return + + # Adicionar dt_ingest a cada registro + for registro in dados_lista: + registro["dt_ingest"] = datetime.now().isoformat() + + for item in dados_lista: + if "dataUltimaTransacao" in item: + valor_bruto = item.pop("dataUltimaTransacao") + try: + if valor_bruto and valor_bruto.isdigit() and len(valor_bruto) == 8: + item["dt_ultima_transacao"] = ( + datetime.strptime(valor_bruto, "%d%m%Y").date().isoformat() + ) + else: + item["dt_ultima_transacao"] = None + except Exception as e: + logging.warning(f"Erro ao converter data: {valor_bruto} - {e}") + item["dt_ultima_transacao"] = None + + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + logging.info("Inserindo dados no banco de dados") + + ddl = """ + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'siape' + AND table_name = 'lista_uorgs' + AND constraint_type = 'PRIMARY KEY' + ) THEN + ALTER TABLE siape.lista_uorgs + ADD CONSTRAINT lista_uorgs_pkey + PRIMARY KEY (codigo); + END IF; + END + $$; + """ + db.execute_non_query(ddl) + + db.insert_data( + dados_lista, + table_name="lista_uorgs", + conflict_fields=["codigo"], + primary_key=["codigo"], + schema="siape", + ) + + logging.info("Dados inseridos com sucesso") + + fetch_and_store_lista_uorgs() + + +dag_instance = siape_lista_uorgs_dag() diff --git a/airflow_lappis/dags/data_ingest/siape/pensoes_instituidas_siape_ingest_dag.py b/airflow_lappis/dags/data_ingest/siape/pensoes_instituidas_siape_ingest_dag.py new file mode 100644 index 00000000..0caa044e --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siape/pensoes_instituidas_siape_ingest_dag.py @@ -0,0 +1,108 @@ +import os +import logging +import requests +from datetime import datetime, timedelta +from airflow.decorators import dag, task +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siape import ClienteSiape +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("pensoes_instituidas_siape_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siape", "pensoes_instituidas"], +) +def siape_pensoes_instituidas_dag() -> None: + """ + DAG que consome o endpoint consultaPensoesInstituidas da API SIAPE + e armazena dados de pensões instituídas no schema 'siape'. + """ + + @task + def fetch_and_store_pensoes_instituidas() -> None: + logging.info("Iniciando extração de dados de pensões instituídas por CPF") + cliente_siape = ClienteSiape() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + query = "SELECT DISTINCT cpf FROM siape.lista_servidores WHERE cpf IS NOT NULL" + cpfs = [row[0] for row in db.execute_query(query)] + logging.info(f"Total de CPFs encontrados: {len(cpfs)}") + + for cpf in cpfs: + try: + context = { + "siglaSistema": "PETRVS-IPEA", + "nomeSistema": "PDG-PETRVS-IPEA", + "senha": os.getenv("SIAPE_PASSWORD_USER"), + "cpf": cpf, + "codOrgao": "45206", + "parmExistPag": "b", + "parmTipoVinculo": "c", + } + + resposta_xml = cliente_siape.call( + "consultaPensoesInstituidas.xml.j2", context + ) + dados = ClienteSiape.parse_pensoes_instituidas(resposta_xml) + + if not dados: + logging.warning(f"Nenhum dado de pensão instituída para CPF {cpf}") + continue + + # Adiciona CPF a cada registro e gera ID único + for i, registro in enumerate(dados): + registro["cpf"] = cpf + # Cria ID único baseado em CPF + índice + campos únicos + base_id = f"{cpf}_{i}" + cpf_pensionista = registro.get("cpfPensionista", "") + matricula_pensionista = registro.get("matriculaPensionista", "") + identificador = f"{base_id}_{cpf_pensionista}_{matricula_pensionista}" + registro["id_registro"] = identificador + + if dados: + # Adicionar dt_ingest a cada registro + for registro in dados: + registro["dt_ingest"] = datetime.now().isoformat() + + # Usa o primeiro registro para criar/ajustar a estrutura da tabela + db.alter_table( + data=dados[0], + table_name="pensoes_instituidas", + schema="siape", + ) + + db.insert_data( + dados, + table_name="pensoes_instituidas", + conflict_fields=["id_registro"], + primary_key=["id_registro"], + schema="siape", + ) + + logging.info( + f"Inseridos {len(dados)} registros de pensões para CPF {cpf}" + ) + + except requests.exceptions.HTTPError as e: + if "500" in str(e): + logging.warning(f"Servidor retornou erro 500 para CPF {cpf}") + else: + logging.error(f"Erro HTTP ao processar CPF {cpf}: {e}") + continue + except Exception as e: + logging.error(f"Erro ao processar CPF {cpf}: {e}") + continue + + fetch_and_store_pensoes_instituidas() + + +dag_instance = siape_pensoes_instituidas_dag() diff --git a/airflow_lappis/dags/data_ingest/siconv/sincov_ingest_dag.py b/airflow_lappis/dags/data_ingest/siconv/sincov_ingest_dag.py new file mode 100644 index 00000000..eb80342c --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siconv/sincov_ingest_dag.py @@ -0,0 +1,150 @@ +import logging +from datetime import datetime, timedelta +from airflow.decorators import dag, task +import psycopg2 +from postgres_helpers import get_postgres_conn +from cliente_postgres import ClientPostgresDB +from cliente_siconv import ClienteSiconv +from tabelas_siconv import TABELAS_SICONV + + +@dag( + schedule_interval=None, + start_date=datetime(2024, 1, 1), + catchup=False, + default_args={ + "owner": "Luana", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["siconv", "MIR"], +) +def siconv_ingestao_dag() -> None: + + @task + def baixar_siconv() -> str: + cliente = ClienteSiconv() + cliente.baixar_zip() + return cliente.ZIP_PATH + + @task + def ingerir_tabela( + zip_path: str, + nome_tabela: str, + nome_csv: str, + conflict_fields: list, + primary_key: list, + skip_rows: int, + colunas: list, + truncate_before_insert: bool = False, + ) -> None: + + postgres_conn_str = get_postgres_conn("postgres_mir") + + logging.info(f"Iniciando ingestão da tabela {nome_tabela}") + + db = ClientPostgresDB(postgres_conn_str) + cliente = ClienteSiconv() + + gerador_registros = cliente.ler_csv( + nome_csv, skip_rows, colunas_esperadas=colunas + ) + + lote = [] + tamanho_lote = 5000 + total_inserido = 0 + + conn = psycopg2.connect(postgres_conn_str) + try: + if truncate_before_insert: + logging.info(f"Truncando tabela siconv.{nome_tabela}...") + with conn.cursor() as cursor: + cursor.execute(f""" + DO $$ BEGIN + IF EXISTS ( + SELECT FROM pg_tables + WHERE schemaname = 'siconv' + AND tablename = '{nome_tabela}' + ) THEN + TRUNCATE TABLE siconv.{nome_tabela}; + END IF; + END $$; + """) + + for registro in gerador_registros: + lote.append(registro) + + if len(lote) >= tamanho_lote: + lote = [dict(t) for t in {tuple(d.items()) for d in lote}] + + db.insert_data( + lote, + nome_tabela, + conflict_fields=conflict_fields, + primary_key=primary_key, + schema="siconv", + conn=conn, + ) + + total_inserido += len(lote) + logging.info(f"{total_inserido} registros processados...") + lote = [] + + if lote: + lote = [dict(t) for t in {tuple(d.items()) for d in lote}] + + db.insert_data( + lote, + nome_tabela, + conflict_fields=conflict_fields, + primary_key=primary_key, + schema="siconv", + conn=conn, + ) + + total_inserido += len(lote) + + conn.commit() + finally: + conn.close() + + if total_inserido == 0: + logging.warning(f"Nenhum registro processado para {nome_tabela}") + else: + logging.info( + f"Ingestão finalizada: {total_inserido} registros em {nome_tabela}" + ) + + @task + def deletar_zip(zip_path: str) -> None: + import os + + if os.path.exists(zip_path): + os.remove(zip_path) + logging.info(f"Arquivo {zip_path} deletado com sucesso") + else: + logging.warning(f"Arquivo {zip_path} não encontrado") + + path_zip = baixar_siconv() + + ultima_task = path_zip + + for tabela in TABELAS_SICONV: + task_atual = ingerir_tabela.override(task_id=f"ingerir_{tabela['nome_tabela']}")( + zip_path=path_zip, + nome_tabela=tabela["nome_tabela"], + nome_csv=tabela["nome_csv"], + conflict_fields=tabela["conflict_fields"], + primary_key=tabela["primary_key"], + skip_rows=tabela["skip_rows"], + colunas=tabela["colunas"], + truncate_before_insert=tabela.get("truncate_before_insert", False), + ) + + ultima_task >> task_atual + ultima_task = task_atual + + ultima_task >> deletar_zip(path_zip) + + +siconv_ingestao_dag() diff --git a/airflow_lappis/dags/data_ingest/siorg/cargos_funcao_ingest_dag.py b/airflow_lappis/dags/data_ingest/siorg/cargos_funcao_ingest_dag.py new file mode 100644 index 00000000..b065b2f5 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siorg/cargos_funcao_ingest_dag.py @@ -0,0 +1,58 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siorg import ClienteSiorg +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule( + "dados_afastamento_historico_siape_ingest_dag" + ), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "retries": 1, + "retry_delay": timedelta(minutes=5), + "owner": "Davi", + }, + tags=["estrutura_organizacional", "siorg"], +) +def api_cargos_funcao_dag() -> None: + @task + def fetch_and_store_cargos_funcao() -> None: + api = ClienteSiorg() + db = ClientPostgresDB(get_postgres_conn()) + try: + cargos_funcao_data = api.get_cargos_funcao() + if not cargos_funcao_data: + logging.warning("Nenhum dado retornado pela API de cargos/função.") + return + + registros = [] + for tipo in cargos_funcao_data: + tipo_base = {k: v for k, v in tipo.items() if k != "cargosFuncoes"} + if "cargosFuncoes" in tipo and "cargoFuncao" in tipo["cargosFuncoes"]: + for cargo in tipo["cargosFuncoes"]["cargoFuncao"]: + registro = {**tipo_base, **cargo} + registro["dt_ingest"] = datetime.now().isoformat() + registros.append(registro) + if registros: + db.insert_data( + registros, + "cargos_funcao", + conflict_fields=["codigoCargoFuncao"], + primary_key=["codigoCargoFuncao"], + schema="siorg", + ) + else: + logging.warning("Nenhum cargo/função encontrado para inserir.") + except Exception as e: + logging.error(f"Erro ao buscar/inserir cargos função: {e}") + + fetch_and_store_cargos_funcao() + + +dag_instance = api_cargos_funcao_dag() diff --git a/airflow_lappis/dags/data_ingest/siorg/estrutura_organizacional_cargos_ingest_dag.py b/airflow_lappis/dags/data_ingest/siorg/estrutura_organizacional_cargos_ingest_dag.py new file mode 100644 index 00000000..861f1d71 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siorg/estrutura_organizacional_cargos_ingest_dag.py @@ -0,0 +1,67 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siorg import ClienteSiorg +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("estrutura_organizacional_cargos_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={"retries": 1, "retry_delay": timedelta(minutes=5), "owner": "Davi"}, + tags=["estrutura_organizacional", "siorg"], +) +def api_estrutura_organizacional_cargos_dag() -> None: + """Busca dados da estrutura organizacional via API e armazena no PostgreSQL.""" + + @task + def fetch_estrutura_organizacional_cargos() -> None: + try: + api = ClienteSiorg() + db = ClientPostgresDB(get_postgres_conn()) + codigo_unidades = db.get_codigo_unidade() + + if not codigo_unidades: + logging.warning("Nenhum código de unidade encontrado.") + return + + for unidade in codigo_unidades: + codigo_unidade = unidade["codigounidade"] + ordem_grandeza = unidade["ordem_grandeza"] + + try: + estrutura_cargos = api.get_estrutura_organizacional_cargos( + codigo_unidade + ) + + if estrutura_cargos: + estrutura_cargos["ordem_grandeza"] = ordem_grandeza + estrutura_cargos["dt_ingest"] = datetime.now().isoformat() + + db.insert_data( + [estrutura_cargos], + "estrutura_organizacional_cargos", + conflict_fields=["codigoUnidade"], + primary_key=["codigoUnidade"], + schema="siorg", + ) + else: + logging.debug(f"Sem dados para codigoUnidade {codigo_unidade}.") + + except Exception as e: + logging.error( + f"Erro ao processar codigoUnidade {codigo_unidade}: {e}", + exc_info=True, + ) + + except Exception as e: + logging.error(f"Erro geral na tarefa: {e}", exc_info=True) + raise + + fetch_estrutura_organizacional_cargos() + + +dag_instance = api_estrutura_organizacional_cargos_dag() diff --git a/airflow_lappis/dags/data_ingest/siorg/unidade_organizacional_ingest_dag.py b/airflow_lappis/dags/data_ingest/siorg/unidade_organizacional_ingest_dag.py new file mode 100755 index 00000000..e88ea27b --- /dev/null +++ b/airflow_lappis/dags/data_ingest/siorg/unidade_organizacional_ingest_dag.py @@ -0,0 +1,82 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_siorg import ClienteSiorg +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("unidade_organizacional_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["estrutura_organizacional", "siorg"], +) +def api_unidade_organizacional_dag() -> None: + """DAG para buscar e armazenar dados da Estrutura Organizacional + de uma API no PostgreSQL.""" + + @task + def fetch_estrutura_organizacional_resumida() -> None: + logging.info( + "[unidade_organizacional_ingest_dag.py] " + "Starting fetch_estrutura_organizacional_resumida task" + ) + api = ClienteSiorg() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + codigo_poder = "1" + codigo_esfera = "1" + codigo_unidade = "7" + + try: + logging.info( + "[unidade_organizacional_ingest_dag.py] " + "Fetching estrutura organizacional resumida for " + f"codigoUnidade: {codigo_unidade}" + ) + estrutura_resumida = api.get_estrutura_organizacional_resumida( + codigo_poder=codigo_poder, + codigo_esfera=codigo_esfera, + codigo_unidade=codigo_unidade, + ) + if estrutura_resumida: + # Adicionar dt_ingest a cada item + for item in estrutura_resumida: + item["dt_ingest"] = datetime.now().isoformat() + + logging.info( + "[unidade_organizacional_ingest_dag.py] " + "Inserting estrutura organizacional resumida for " + f"codigoUnidade: {codigo_unidade} into PostgreSQL" + ) + db.insert_data( + estrutura_resumida, + "unidade_organizacional", + conflict_fields=["codigoUnidade"], + primary_key=["codigoUnidade"], + schema="siorg", + ) + else: + logging.warning( + "[unidade_organizacional_ingest_dag.py] " + "No estrutura organizacional resumida found for " + f"codigoUnidade: {codigo_unidade}" + ) + except Exception as e: + logging.error( + "[unidade_organizacional_ingest_dag.py] " + "Error fetching estrutura organizacional resumida for " + f"codigoUnidade {codigo_unidade}: {e}" + ) + + fetch_estrutura_organizacional_resumida() + + +dag_instance = api_unidade_organizacional_dag() diff --git a/airflow_lappis/dags/data_ingest/sisbolsas/sisbolsas_to_postgres_ingest_dag.py b/airflow_lappis/dags/data_ingest/sisbolsas/sisbolsas_to_postgres_ingest_dag.py new file mode 100644 index 00000000..5b4f1d66 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/sisbolsas/sisbolsas_to_postgres_ingest_dag.py @@ -0,0 +1,90 @@ +import logging +from datetime import datetime, timedelta +from typing import Dict, List, TypedDict +from airflow.decorators import dag, task +from airflow.models import Variable +from cliente_postgres import ClientPostgresDB +from cliente_sqlserver import ClientSQLServerDB +from postgres_helpers import get_postgres_conn +from schedule_loader import get_dynamic_schedule + + +class SQLServerTableConfig(TypedDict): + source_schema: str + source_table: str + target_schema: str + target_table: str + primary_key: List[str] + + +TABLES_TO_SYNC_VARIABLE = "sisbolsas_tables_to_sync" + + +def _load_tables_from_variable() -> List[SQLServerTableConfig]: + return Variable.get( + TABLES_TO_SYNC_VARIABLE, + default_var=[], + deserialize_json=True, + ) + + +@dag( + schedule_interval=get_dynamic_schedule("sisbolsas_to_postgres_ingest_dag"), + start_date=datetime(2024, 1, 1), + catchup=False, + max_active_runs=1, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["sql_server", "postgres", "sisbolsas"], +) +def sql_server_to_postgres_ingest_dag_sisbolsas() -> None: + """Replica tabelas do SisBolsas para o Postgres Analytics.""" + + @task + def replicate_table(table_cfg: SQLServerTableConfig) -> Dict[str, int]: + sql_server = ClientSQLServerDB("mssql_conn_id_sisbolsas") + postgres = ClientPostgresDB(get_postgres_conn()) + source_schema = table_cfg["source_schema"] + source_table = table_cfg["source_table"] + target_schema = table_cfg["target_schema"] + target_table = table_cfg["target_table"] + primary_key = table_cfg["primary_key"] + + source_name = f"{source_schema}.{source_table}" + + logging.info( + "[sql_server_to_postgres_ingest_dag.py] Iniciando replicacao de %s", + source_name, + ) + + rows = sql_server.fetch_table_all( + schema=source_schema, + table_name=source_table, + ) + + if rows: + postgres.insert_data( + data=rows, + table_name=target_table, + conflict_fields=primary_key, + primary_key=primary_key, + schema=target_schema, + ) + + logging.info( + "[sql_server_to_postgres_ingest_dag.py] Replicacao concluida " + "para %s. Registros processados=%s", + source_name, + len(rows), + ) + + return {source_name: len(rows)} + + tables_to_sync = _load_tables_from_variable() + replicate_table.expand(table_cfg=tables_to_sync) + + +sql_server_to_postgres_ingest_dag_sisbolsas() diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/bolsas_pagas_ingest_dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/bolsas_pagas_ingest_dag.py new file mode 100644 index 00000000..cf46cc47 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/bolsas_pagas_ingest_dag.py @@ -0,0 +1,152 @@ +from typing import Dict, Any, Optional +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +from datetime import datetime, timedelta +import logging +import json +import pandas as pd +import io +from schedule_loader import get_dynamic_schedule +from cliente_email import fetch_and_process_email +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn + +# Configuracoes basicas da DAG +default_args = { + "owner": "Davi", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +COLUMN_MAPPING = { + 0: "credor_codigo", + 1: "credor_nome", + 2: "dia_emissao", + 3: "mes_emissao", + 4: "ano_emissao", + 5: "emissao_ano", + 6: "mes_lancamento", + 7: "fonte_recursos_codigo", + 8: "fonte_recursos_descricao", + 9: "pi_codigo", + 10: "pi_descricao", + 11: "ptres", + 12: "natureza_codigo", + 13: "natureza_descricao", + 14: "processo", + 15: "valor", + 16: "observacao", + 17: "ne_ccor", + 18: "documento_habil", + 19: "item_informacao", + 20: "despesa_paga", + 21: "rp_processados", + 22: "rp_nao_processados", + 23: "pagamentos_totais", +} + +EMAIL_SUBJECT = "bolsas_pagas_ipea" +SKIPROWS = 11 + + +with DAG( + dag_id="email_bolsas_pagas_tesouro_ingest", + default_args=default_args, + description=( + "Processa anexos de bolsas pagas do Tesouro Gerencial recebidos por email " + "e insere no banco" + ), + schedule_interval=get_dynamic_schedule("bolsas_pagas_ingest_dag"), + start_date=datetime(2023, 12, 1), + catchup=False, + tags=["email", "tesouro", "bolsas_pagas"], +) as dag: + + def process_email_data(**context: Dict[str, Any]) -> Optional[Any]: + creds = json.loads(Variable.get("email_credentials")) + + email = creds["email"] + password = creds["password"] + imap_server = creds["imap_server"] + sender_email = creds["sender_email"] + + try: + logging.info("Iniciando o processamento dos emails...") + csv_data = fetch_and_process_email( + imap_server, + email, + password, + sender_email, + EMAIL_SUBJECT, + COLUMN_MAPPING, + skiprows=SKIPROWS, + ) + + if not csv_data: + logging.warning("Nenhum e-mail encontrado com o assunto esperado.") + return None + + logging.info( + "CSV processado com sucesso. Dados encontrados: %s", len(csv_data) + ) + return csv_data + except Exception as e: + logging.error("Erro no processamento dos emails: %s", str(e)) + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + """Insere os dados processados no banco de dados.""" + try: + task_instance: Any = context["ti"] + csv_data: Any = task_instance.xcom_pull(task_ids="process_emails") + + if not csv_data: + logging.warning("Nenhum dado para inserir no banco.") + return + + df = pd.read_csv(io.StringIO(csv_data)) + data = df.to_dict(orient="records") + + for record in data: + record["dt_ingest"] = datetime.now().isoformat() + + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + db.insert_data(data, "bolsas_pagas", schema="siafi") + + logging.info("Dados inseridos com sucesso no banco de dados.") + except Exception as e: + logging.error("Erro ao inserir dados no banco: %s", str(e)) + raise + + def clean_duplicates(**context: Dict[str, Any]) -> None: + """Remove duplicados da tabela siafi.bolsas_pagas.""" + try: + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + db.remove_duplicates("bolsas_pagas", COLUMN_MAPPING, schema="siafi") + except Exception as e: + logging.error(f"Erro ao executar a limpeza de duplicados: {str(e)}") + raise + + process_emails_task = PythonOperator( + task_id="process_emails", + python_callable=process_email_data, + provide_context=True, + ) + + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + provide_context=True, + ) + + clean_duplicates_task = PythonOperator( + task_id="clean_duplicates", + python_callable=clean_duplicates, + provide_context=True, + ) + + process_emails_task >> insert_to_db_task >> clean_duplicates_task \ No newline at end of file diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/empenhos_tesouro_ingest_dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/empenhos_tesouro_ingest_dag.py new file mode 100755 index 00000000..fc91e8a4 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/empenhos_tesouro_ingest_dag.py @@ -0,0 +1,154 @@ +from typing import Dict, Any, Optional +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +from datetime import datetime, timedelta +import logging +import json +from schedule_loader import get_dynamic_schedule +from cliente_email import fetch_and_process_email +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn +import pandas as pd +import io + +# Configurações básicas da DAG +default_args = { + "owner": "Davi", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +COLUMN_MAPPING = { + 0: "emissao_mes", + 1: "emissao_dia", + 2: "ne_ccor", + 3: "ne_num_processo", + 4: "ne_info_complementar", + 5: "ne_ccor_descricao", + 6: "doc_observacao", + 7: "natureza_despesa", + 8: "natureza_despesa_descricao", + 9: "ne_ccor_favorecido", + 10: "ne_ccor_favorecido_descricao", + 11: "ne_ccor_ano_emissao", + 12: "ptres", + 13: "fonte_recursos_detalhada", + 14: "fonte_recursos_detalhada_descricao", + 15: "despesas_empenhadas", + 16: "despesas_liquidadas", + 17: "despesas_pagas", + 18: "restos_a_pagar_inscritos", + 19: "restos_a_pagar_pagos", +} + +EMAIL_SUBJECT = "notas_de_empenhos_a_partir_de_2024" +SKIPROWS = 9 + +# Configurações da DAG +with DAG( + dag_id="email_empenhos_tesouro_ingest", + default_args=default_args, + description="Processa anexos dos empenhos vindo do email, formata e insere no db", + schedule_interval=get_dynamic_schedule("empenhos_tesouro_ingest_dag"), + start_date=datetime(2023, 12, 1), + catchup=False, + tags=["email", "empenhos", "tesouro"], +) as dag: + + def process_email_data(**context: Dict[str, Any]) -> Optional[Any]: + creds = json.loads(Variable.get("email_credentials")) + + EMAIL = creds["email"] + PASSWORD = creds["password"] + IMAP_SERVER = creds["imap_server"] + SENDER_EMAIL = creds["sender_email"] + + try: + logging.info("Iniciando o processamento dos emails...") + csv_data = fetch_and_process_email( + IMAP_SERVER, + EMAIL, + PASSWORD, + SENDER_EMAIL, + EMAIL_SUBJECT, + COLUMN_MAPPING, + skiprows=SKIPROWS, + ) + if not csv_data: + logging.warning("Nenhum e-mail encontrado com o assunto esperado.") + return None + + logging.info( + "CSV processado com sucesso. Dados encontrados: %s", len(csv_data) + ) + return csv_data + except Exception as e: + logging.error("Erro no processamento dos emails: %s", str(e)) + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + """ + Função para inserir os dados no banco de dados. + Os dados do CSV são recuperados do XCom. + """ + try: + task_instance: Any = context["ti"] + csv_data: Any = task_instance.xcom_pull(task_ids="process_emails") + + if not csv_data: + logging.warning("Nenhum dado para inserir no banco.") + return + + df = pd.read_csv(io.StringIO(csv_data)) + df = df[df["ne_ccor_ano_emissao"].astype(str).str.startswith("20")] + data = df.to_dict(orient="records") + + # Adicionar dt_ingest a cada registro + for record in data: + record["dt_ingest"] = datetime.now().isoformat() + + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + unique_key = [ + "ne_ccor", + "natureza_despesa", + "doc_observacao", + "ne_ccor_ano_emissao", + "emissao_dia", + "emissao_mes", + "despesas_empenhadas", + "despesas_liquidadas", + "despesas_pagas", + ] + + db.insert_data( + data, + "empenhos_tesouro", + conflict_fields=unique_key, + primary_key=unique_key, + schema="siafi", + ) + logging.info("Dados inseridos com sucesso no banco de dados.") + except Exception as e: + logging.error("Erro ao inserir dados no banco: %s", str(e)) + raise + + # Tarefa 1: Processar os e-mails e retornar CSV + process_emails_task = PythonOperator( + task_id="process_emails", + python_callable=process_email_data, + provide_context=True, + ) + + # Tarefa 2: Inserir os dados no banco de dados + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + provide_context=True, + ) + + # Fluxo da DAG + process_emails_task >> insert_to_db_task diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/mcid/dotacao_execucao_outras_fontes_mcid_ingest_dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mcid/dotacao_execucao_outras_fontes_mcid_ingest_dag.py new file mode 100644 index 00000000..85525dae --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mcid/dotacao_execucao_outras_fontes_mcid_ingest_dag.py @@ -0,0 +1,183 @@ +import io +import json +import logging +from datetime import datetime, timedelta +from typing import Any, Dict, List, Optional + +import cliente_email +import pandas as pd +from airflow import DAG +from airflow.exceptions import AirflowSkipException +from airflow.models import Variable +from airflow.operators.python import PythonOperator +from cliente_email import fetch_and_process_email +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn +from schedule_loader import get_dynamic_schedule + +default_args = { + "owner": "Lucas", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +COLUMN_MAPPING = { + 0: "unidade_orcamentaria_codigo", + 1: "unidade_orcamentaria_nome", + 2: "acao_governo_codigo", + 3: "acao_governo_nome", + 4: "programa_governo_codigo", + 5: "programa_governo_nome", + 6: "plano_orcamentario_codigo", + 7: "plano_orcamentario_funcao", + 8: "plano_orcamentario_subfuncao", + 9: "plano_orcamentario_programa", + 10: "plano_orcamentario_acao", + 11: "plano_orcamentario_medida", + 12: "plano_orcamentario_descricao", + 13: "elemento_despesa_codigo", + 14: "elemento_despesa_nome", + 15: "orgao_uge_codigo", + 16: "orgao_uge_nome", + 17: "uge_matriz_filial", + 18: "ug_executora_codigo", + 19: "ug_executora_nome", + 20: "fixacao_despesa_loa", + 21: "dotacao_inicial", + 22: "dotacao_atualizada", + 23: "credito_disponivel", + 24: "despesas_empenhadas", + 25: "despesas_empenhadas_a_liquidar", + 26: "despesas_liquidadas_a_pagar", + 27: "despesas_pagas", + 28: "restos_a_pagar_inscritos", + 29: "restos_a_pagar_pagos", +} + +EMAIL_SUBJECT = "dotacao_execucao_outras_fontes_mcid" +SKIPROWS = 12 + + +def _patched_format_csv( + csv_data: str, + column_mapping: Optional[Dict[int, str]], + skiprows: int, +) -> pd.DataFrame: + """Substitui o format_csv do cliente_email com suporte a UTF-16 e TSV.""" + # Decodifica UTF-16 se ainda vier como bytes + if isinstance(csv_data, bytes): + csv_data = csv_data.decode("utf-16") + + if column_mapping: + df = pd.read_csv( + io.StringIO(csv_data), + skiprows=skiprows, + header=None, + sep="\t", + engine="python", + on_bad_lines="skip", + ) + column_names: List[str] = [ + column_mapping.get(i, f"col_{i}") for i in range(len(df.columns)) + ] + df.columns = pd.Index(column_names) + else: + df = pd.read_csv( + io.StringIO(csv_data), + skiprows=skiprows, + header=0, + sep="\t", + engine="python", + on_bad_lines="skip", + ) + return df + + +with DAG( + dag_id="dotacao_execucao_outras_fontes_mcid_ingest_dag", + default_args=default_args, + description="Processa e ingere dados de execução de outras fontes de MCID", + schedule_interval=get_dynamic_schedule("dotacao_execucao_outras_fontes_mcid"), + catchup=False, + start_date=datetime(2026, 3, 27), + tags=["email", "mcid", "tesouro", "dotacao", "execucao"], +) as dag: + + def process_email_data(**context: Dict[str, Any]) -> Optional[Any]: + creds = json.loads(Variable.get("email_credentials")) + + EMAIL = creds["email"] + PASSWORD = creds["password"] + IMAP_SERVER = creds["imap_server"] + SENDER_EMAIL = creds["sender_email"] + + cliente_email.format_csv = _patched_format_csv + + try: + logging.info("Iniciando o processamento dos emails") + csv_data = fetch_and_process_email( + IMAP_SERVER, + EMAIL, + PASSWORD, + SENDER_EMAIL, + EMAIL_SUBJECT, + COLUMN_MAPPING, + skiprows=SKIPROWS, + ) + if not csv_data: + logging.warning("Nenhum e-mail encontrado com o assunto esperado.") + raise AirflowSkipException("Nenhum e-mail encontrado. Task ignorada.") + + logging.info( + "CSV processado com sucesso. Registros encontrados: %s", len(csv_data) + ) + return csv_data + except Exception as e: + logging.error("Erro no processamento dos emails: %s", str(e)) + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + try: + task_instance: Any = context["ti"] + csv_data: Any = task_instance.xcom_pull(task_ids="process_emails") + + if not csv_data: + logging.warning("Nenhum dado para inserir no banco.") + raise AirflowSkipException( + "Nenhum dado foi encontrado para inserção no BD" + ) + + df = pd.read_csv(io.StringIO(csv_data)) + data = df.to_dict(orient="records") + + for record in data: + record["dt_ingest"] = datetime.now().isoformat() + + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + + db.insert_data( + data, + "dotacao_execucao_outras_fontes_mcid", + schema="siafi", + ) + logging.info("Dados inseridos com sucesso no banco de dados.") + except Exception as e: + logging.error("Erro ao inserir dados no banco: %s", str(e)) + raise + + process_emails_task = PythonOperator( + task_id="process_emails", + python_callable=process_email_data, + provide_context=True, + ) + + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + provide_context=True, + ) + + process_emails_task >> insert_to_db_task diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/mcid/empenho_emendas_parlamentares_ingest_dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mcid/empenho_emendas_parlamentares_ingest_dag.py new file mode 100644 index 00000000..21524328 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mcid/empenho_emendas_parlamentares_ingest_dag.py @@ -0,0 +1,155 @@ +import io +import json +import logging +from datetime import datetime, timedelta +from typing import Any, Dict, Optional + +import pandas as pd +from airflow import DAG +from airflow.exceptions import AirflowSkipException +from airflow.models import Variable +from airflow.operators.python import PythonOperator +from cliente_email import fetch_and_process_email +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn +from schedule_loader import get_dynamic_schedule + +default_args = { + "owner": "Lucas", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +COLUMN_MAPPING = { + 0: "emissao_mes", + 1: "emissao_dia", + 2: "programa_governo_numero", + 3: "programa_governo_nome", + 4: "acao_governo_codigo", + 5: "acao_governo_nome", + 6: "autor_emendas_orcamento_codigo", + 7: "autor_emendas_orcamento_nome", + 8: "uf_codigo", + 9: "uf_nome", + 10: "municipio", + 11: "ne_ccor", + 12: "ne_num_processo", + 13: "ne_info_complementar", + 14: "ne_ccor_descricao", + 15: "doc_observacao", + 16: "grupo_despesa_codigo", + 17: "grupo_despesa_nome", + 18: "natureza_despesa_codigo", + 19: "natureza_despesa_nome", + 20: "modalidade_aplicacao_codigo", + 21: "modalidade_aplicacao_nome", + 22: "ne_ccor_favorecido_codigo", + 23: "ne_ccor_favorecido_nome", + 24: "ne_ccor_ano_emissao", + 25: "ptres", + 26: "fonte_recursos_detalhada", + 27: "fonte_recursos_detalhada_descricao", + 28: "restos_a_pagar_inscritos", + 29: "restos_a_pagar_pagos", +} + +EMAIL_SUBJECT = "notas_empenho_emendas_parlamentares_mcid" +SKIPROWS = 9 + +with DAG( + dag_id="empenho_emendas_parlamentares_ingest_dag", + default_args=default_args, + description="Processa e ingere dados de empenho de emendas parlamentares do MCID", + schedule_interval=get_dynamic_schedule("empenho_emendas_parlamentares_ingest_dag"), + start_date=datetime(2026, 3, 25), + catchup=False, + tags=["email", "empenhos", "tesouro", "emendas"], +) as dag: + + def process_email_data(**context: Dict[str, Any]) -> Optional[Any]: + creds = json.loads(Variable.get("email_credentials")) + + EMAIL = creds["email"] + PASSWORD = creds["password"] + IMAP_SERVER = creds["imap_server"] + SENDER_EMAIL = creds["sender_email"] + + try: + logging.info("Iniciando o processamento dos emails") + csv_data = fetch_and_process_email( + IMAP_SERVER, + EMAIL, + PASSWORD, + SENDER_EMAIL, + EMAIL_SUBJECT, + COLUMN_MAPPING, + skiprows=SKIPROWS, + ) + if not csv_data: + logging.warning("Nenhum e-mail encontrado com o assunto esperado.") + raise AirflowSkipException("Nenhum e-mail encontrado. Task ignorada.") + + logging.info( + "CSV processado com sucesso. Dados encontrados: %s", len(csv_data) + ) + return csv_data + except Exception as e: + logging.error("Erro no processamento dos emails: %s", str(e)) + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + try: + task_instance: Any = context["ti"] + csv_data: Any = task_instance.xcom_pull(task_ids="process_emails") + + if not csv_data: + logging.warning("Nenhum dado para inserir no banco.") + raise AirflowSkipException( + "Nenhum dado foi encontrado para inserção no BD" + ) + + df = pd.read_csv(io.StringIO(csv_data), skiprows=[1, 2, 3]) + df = df[df["ne_ccor_ano_emissao"].astype(str).str.startswith("20")] + data = df.to_dict(orient="records") + + for record in data: + record["dt_ingest"] = datetime.now().isoformat() + + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + unique_key = [ + "ne_ccor", + "emissao_dia", + "emissao_mes", + "programa_governo_numero", + "programa_governo_nome", + "ne_num_processo", + ] + + db.insert_data( + data, + "empenho_emendas_parlamentares", + conflict_fields=unique_key, + primary_key=unique_key, + schema="siafi", + ) + logging.info("Dados inseridos com sucesso no banco de dados.") + except Exception as e: + logging.error("Erro ao inserir dados no banco: %s", str(e)) + raise + + process_emails_task = PythonOperator( + task_id="process_emails", + python_callable=process_email_data, + provide_context=True, + ) + + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + provide_context=True, + ) + + process_emails_task >> insert_to_db_task diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/mcid/orcamento_mcid_por_acao_ingest_dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mcid/orcamento_mcid_por_acao_ingest_dag.py new file mode 100644 index 00000000..7df16e06 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mcid/orcamento_mcid_por_acao_ingest_dag.py @@ -0,0 +1,194 @@ +import io +import json +import logging +from datetime import datetime, timedelta +from typing import Any, Dict, List, Optional + +import cliente_email # importar o módulo, não só a função +import pandas as pd +from airflow import DAG +from airflow.exceptions import AirflowSkipException +from airflow.models import Variable +from airflow.operators.python import PythonOperator +from cliente_email import fetch_and_process_email +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn +from schedule_loader import get_dynamic_schedule + +default_args = { + "owner": "Lucas", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +COLUMN_MAPPING = { + 0: "acao_governo_codigo", + 1: "acao_governo_nome", + 2: "programa_governo_codigo", + 3: "programa_governo_nome", + 4: "ne_ccor", + 5: "ne_ccor_favorecido_codigo", + 6: "ne_ccor_favorecido_nome", + 7: "favorecido_cep", + 8: "favorecido_municipio_codigo", + 9: "favorecido_municipio_nome", + 10: "favorecido_regiao", + 11: "favorecido_ug_uf_codigo", + 12: "favorecido_ug_uf_nome", + 13: "fonte_recursos_detalhada", + 14: "fonte_recursos_detalhada_descricao", + 15: "pt", + 16: "ptres", + 17: "plano_orcamentario_ug_executora_codigo", + 18: "plano_orcamentario_cod1", + 19: "plano_orcamentario_cod2", + 20: "plano_orcamentario_programa", + 21: "plano_orcamentario_acao_orcamentaria", + 22: "plano_orcamentario_medida", + 23: "plano_orcamentario_descricao", + 24: "ug_executora_codigo", + 25: "ug_executora_nome", + 26: "ug_responsavel_codigo", + 27: "ug_responsavel_nome", + 28: "pl_codigo", + 29: "pl_nome", + 30: "natureza_despesa_codigo", + 31: "natureza_despesa_nome", + 32: "dotacao_inicial", + 33: "dotacao_atualizada", + 34: "despesas_empenhadas", + 35: "despesas_empenhadas_a_liquidar", + 36: "despesas_liquidadas_a_pagar", + 37: "despesas_pagas", +} + +EMAIL_SUBJECT = "orcamento_mcid_por_acao" +SKIPROWS = 10 + + +# A formatação do CSV estava como utf-16. +# Função criada para consumo sem erro de formatação +def _patched_format_csv( + csv_data: str, + column_mapping: Optional[Dict[int, str]], + skiprows: int, +) -> pd.DataFrame: + """Substitui o format_csv do cliente_email com suporte a UTF-16 e TSV.""" + # Decodifica UTF-16 se ainda vier como bytes + if isinstance(csv_data, bytes): + csv_data = csv_data.decode("utf-16") + + if column_mapping: + df = pd.read_csv( + io.StringIO(csv_data), + skiprows=skiprows, + header=None, + sep="\t", + engine="python", + on_bad_lines="skip", + ) + column_names: List[str] = [ + column_mapping.get(i, f"col_{i}") for i in range(len(df.columns)) + ] + df.columns = pd.Index(column_names) + else: + df = pd.read_csv( + io.StringIO(csv_data), + skiprows=skiprows, + header=0, + sep="\t", + engine="python", + on_bad_lines="skip", + ) + return df + + +with DAG( + dag_id="orcamento_mcid_por_acao_ingest_dag", + default_args=default_args, + description="Processa e ingere dados de orcamento por acao do MCID do Tesouro", + schedule_interval=get_dynamic_schedule("orcamento_mcid_por_acao_ingest_dag"), + start_date=datetime(2026, 3, 23), + catchup=False, + tags=["email", "orcamento", "tesouro", "mcid"], +) as dag: + + def process_email_data(**context: Dict[str, Any]) -> Optional[Any]: + creds = json.loads(Variable.get("email_credentials")) + + EMAIL = creds["email"] + PASSWORD = creds["password"] + IMAP_SERVER = creds["imap_server"] + SENDER_EMAIL = creds["sender_email"] + + # Monkey-patch: substitui format_csv do cliente_email pela versão corrigida + cliente_email.format_csv = _patched_format_csv + + try: + logging.info("Iniciando o processamento dos emails") + csv_data = fetch_and_process_email( + IMAP_SERVER, + EMAIL, + PASSWORD, + SENDER_EMAIL, + EMAIL_SUBJECT, + COLUMN_MAPPING, + skiprows=SKIPROWS, + ) + if not csv_data: + logging.warning("Nenhum e-mail encontrado com o assunto esperado.") + raise AirflowSkipException("Nenhum e-mail encontrado. Task ignorada.") + + logging.info( + "CSV processado com sucesso. Registros encontrados: %s", len(csv_data) + ) + return csv_data + except Exception as e: + logging.error("Erro no processamento dos emails: %s", str(e)) + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + try: + task_instance: Any = context["ti"] + csv_data: Any = task_instance.xcom_pull(task_ids="process_emails") + + if not csv_data: + logging.warning("Nenhum dado para inserir no banco.") + raise AirflowSkipException( + "Nenhum dado foi encontrado para inserção no BD" + ) + + df = pd.read_csv(io.StringIO(csv_data)) + data = df.to_dict(orient="records") + + for record in data: + record["dt_ingest"] = datetime.now().isoformat() + + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + + db.insert_data( + data, + "orcamento_mcid_por_acao", + schema="siafi", + ) + logging.info("Dados inseridos com sucesso no banco de dados.") + except Exception as e: + logging.error("Erro ao inserir dados no banco: %s", str(e)) + raise + + process_emails_task = PythonOperator( + task_id="process_emails", + python_callable=process_email_data, + provide_context=True, + ) + + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + provide_context=True, + ) + + process_emails_task >> insert_to_db_task diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/nc_tesouro_ingest_2025_mir_dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/nc_tesouro_ingest_2025_mir_dag.py new file mode 100644 index 00000000..91a5fb76 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/nc_tesouro_ingest_2025_mir_dag.py @@ -0,0 +1,232 @@ +from typing import Dict, Any, cast +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +from datetime import datetime, timedelta +import logging +import json +import pandas as pd +import io +from schedule_loader import get_dynamic_schedule +from cliente_email import fetch_and_process_email +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn + +default_args = { + "owner": "Mateus", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +COLUMN_MAPPING = { + 0: "programa_governo", + 1: "programa_governo_descricao", + 2: "acao_governo", + 3: "acao_governo_descricao", + 4: "nc", + 5: "nc_transferencia", + 6: "nc_fonte_recursos", + 7: "nc_fonte_recursos_descricao", + 8: "ptres", + 9: "nc_evento", + 10: "nc_evento_descricao", + 11: "nc_ug_responsavel", + 12: "nc_ug_responsavel_descricao", + 13: "nc_natureza_despesa", + 14: "nc_natureza_despesa_descricao", + 15: "nc_plano_interno", + 16: "nc_plano_interno_descricao1", + 17: "nc_plano_interno_descricao2", + 18: "favorecido_doc", + 19: "favorecido_doc_descricao", + 20: "favorecido_municipio", + 21: "favorecido_municipio_descricao", + 22: "nc_valor_linha", + 23: "movimento_liquido_moeda_origem", +} + +# Configurações dos emails +EMAIL_CONFIGS = { + "enviadas": { + "subject": "notas_credito_enviadas_devolvidas_ate_2025", + "column_mapping": COLUMN_MAPPING, + "skiprows": 3, + }, + "recebidas": { + "subject": "notas_credito_recebidas_ate_2025", + "column_mapping": None, + "skiprows": 6, + }, +} +expected_columns = list(COLUMN_MAPPING.values()) +with DAG( + dag_id="email_notas_credito_ingest_mir_ate_2025", + default_args=default_args, + schedule_interval=get_dynamic_schedule("email_notas_credito_ingest_mir_ate_2025"), + start_date=datetime(2023, 12, 1), + catchup=False, + tags=["MIR", "SIAFI", "notas_credito"], +) as dag: + + def process_email_data(email_type: str, **context: Dict[str, Any]) -> pd.DataFrame: + """ + Função genérica para processar emails de notas de crédito. + """ + config = EMAIL_CONFIGS[email_type] + creds_data = json.loads(Variable.get("email_credentials")) + creds = cast(Dict[str, str], creds_data) + config = cast(Dict[str, Any], config) + + try: + logging.info(f"Iniciando o processamento das NCs {email_type}") + csv_data = fetch_and_process_email( + creds["imap_server"], + creds["email"], + creds["password"], + creds["sender_email"], + config["subject"], + config["column_mapping"], + skiprows=config["skiprows"], + ) + + if not csv_data: + logging.warning(f"Nenhum e-mail encontrado para NCs {email_type}") + return pd.DataFrame() + + df = pd.read_csv(io.StringIO(csv_data)) + + # Se não tem mapeamento de colunas (recebidas), aplicar o mapeamento padrão + if config["column_mapping"] is None and not df.empty: + expected_columns = list(COLUMN_MAPPING.values()) + if len(df.columns) == len(expected_columns): + df.columns = pd.Index(expected_columns) + else: + logging.warning( + f"N coluna incompatível:{len(expected_columns)},{len(df.columns)}" + ) + + logging.info( + f"CSV de NCs {email_type} processado com sucesso: {len(df)} registros" + ) + return df + + except Exception as e: + logging.error( + f"Erro no processamento dos emails de NCs {email_type}: {str(e)}" + ) + raise + + def process_email_data_enviadas(**context: Dict[str, Any]) -> pd.DataFrame: + """Wrapper para processar emails enviadas.""" + return process_email_data("enviadas", **context) + + def process_email_data_recebidas(**context: Dict[str, Any]) -> pd.DataFrame: + """Wrapper para processar emails recebidas.""" + return process_email_data("recebidas", **context) + + def combine_data(**context: Dict[str, Any]) -> pd.DataFrame: + """ + Função para combinar os dados dos dois emails. + """ + try: + task_instance: Any = context["ti"] + df_enviadas = cast( + pd.DataFrame, task_instance.xcom_pull(task_ids="process_emails_enviadas") + ) + df_recebidas = cast( + pd.DataFrame, task_instance.xcom_pull(task_ids="process_emails_recebidas") + ) + + dfs = [ + df + for df in [df_enviadas, df_recebidas] + if df is not None and not df.empty + ] + + if not dfs: + logging.warning("Nenhum dado foi encontrado para combinar.") + return pd.DataFrame() + + combined_df = pd.concat(dfs, ignore_index=True) + combined_df["dt_ingest"] = datetime.now().isoformat() + + logging.info(f"Dados combinados: {len(combined_df)} registros no total.") + return combined_df + + except Exception as e: + logging.error(f"Erro ao combinar os dados: {str(e)}") + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + """ + Função para inserir os dados no banco de dados. + """ + try: + task_instance: Any = context["ti"] + combined_df = task_instance.xcom_pull(task_ids="combine_data") + + if combined_df is None or combined_df.empty: + logging.warning("Nenhum dado para inserir no banco.") + return + + data = combined_df.to_dict(orient="records") + + postgres_conn_str = get_postgres_conn('postgres_mir') + db = ClientPostgresDB(postgres_conn_str) + + db.insert_data(data, "nc_tesouro_pre_2026", schema="siafi") + logging.info("Dados inseridos com sucesso no banco de dados.") + except Exception as e: + logging.error("Erro ao inserir dados no banco: %s", str(e)) + raise + + def clean_duplicates(**context: Dict[str, Any]) -> None: + """ + Task para remover duplicados da tabela 'siafi.pf_tesouro'. + """ + try: + postgres_conn_str = get_postgres_conn('postgres_mir') + db = ClientPostgresDB(postgres_conn_str) + db.remove_duplicates("nc_tesouro_pre_2026", COLUMN_MAPPING, schema="siafi") + + except Exception as e: + logging.error(f"Erro ao executar a limpeza de duplicados: {str(e)}") + raise + + process_emails_enviadas_task = PythonOperator( + task_id="process_emails_enviadas", + python_callable=process_email_data_enviadas, + provide_context=True, + ) + + process_emails_recebidas_task = PythonOperator( + task_id="process_emails_recebidas", + python_callable=process_email_data_recebidas, + provide_context=True, + ) + + combine_data_task = PythonOperator( + task_id="combine_data", + python_callable=combine_data, + provide_context=True, + ) + + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + provide_context=True, + ) + + clean_duplicates_task = PythonOperator( + task_id="clean_duplicates", + python_callable=clean_duplicates, + provide_context=True, + ) + + ( + [process_emails_enviadas_task, process_emails_recebidas_task] + >> combine_data_task + >> insert_to_db_task + >> clean_duplicates_task + ) diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/nc_tesouro_ingest_2026_mir_dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/nc_tesouro_ingest_2026_mir_dag.py new file mode 100644 index 00000000..0a42e541 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/nc_tesouro_ingest_2026_mir_dag.py @@ -0,0 +1,142 @@ +from typing import Dict, Any, Optional +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +from datetime import datetime, timedelta +import logging +import json +import pandas as pd +import io +from schedule_loader import get_dynamic_schedule +from cliente_email import fetch_and_process_email +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn +from functools import partial + +default_args = { + "owner": "Mateus", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +pd.read_csv = partial(pd.read_csv, sep='\t', on_bad_lines='skip') + +COLUMN_MAPPING_NC = { + 0: "emissao_dia", + 1: "nc", + 2: "emitente_codigo", + 3: "emitente_nome", + 4: "ptres", + 5: "fonte_codigo", + 6: "fonte_nome", + 7: "gnd_codigo", + 8: "gnd_nome", + 9: "pi_codigo", + 10: "pi_nome", + 11: "descricao", + 12: "ugr_codigo", + 13: "ugr_nome", + 14: "tipo_nc", + 15: "nc_item_detalhamento", + 16: "favorecido_codigo", + 17: "favorecido_nome", + 18: "ro", + 19: "nc_transferencia", + 20: "dc", + 21: "item_total", + 22: "total_lista", + 23: "valor_celula", + 24: "esfera_orcamentaria_codigo", + 25: "esfera_orcamentaria_nome", + 26: "emissao_ano", + 27: "emissao_mes", +} + +EMAIL_SUBJECT = "notas_credito_enviadas_devolvidas_a_partir_2026" +SKIPROWS = 3 + +with DAG( + dag_id="email_notas_credito_ingest_mir_pos_2026", + default_args=default_args, + schedule_interval=get_dynamic_schedule("email_notas_credito_ingest_mir_post_2026"), + start_date=datetime(2024, 1, 1), + catchup=False, + tags=["MIR", "email", "notas_credito"], +) as dag: + + def process_email_data(**context: Dict[str, Any]) -> Optional[Any]: + creds = json.loads(Variable.get("email_credentials")) + + try: + logging.info("Iniciando coleta de emails para o assunto: %s", EMAIL_SUBJECT) + csv_data = fetch_and_process_email( + creds["imap_server"], + creds["email"], + creds["password"], + creds["sender_email"], + EMAIL_SUBJECT, + COLUMN_MAPPING_NC, + skiprows=SKIPROWS, + ) + + if not csv_data: + logging.warning("Nenhum CSV extraído.") + return None + + return csv_data + except Exception as e: + logging.error("Erro no processamento: %s", str(e)) + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + try: + ti = context["ti"] + csv_data = ti.xcom_pull(task_ids="process_emails") + + if not csv_data: + return + + df = pd.read_csv(io.StringIO(csv_data), sep=',') + data = df.to_dict(orient="records") + + for record in data: + record["dt_ingest"] = datetime.now().isoformat() + + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + unique_key = [ + "nc", + "emissao_dia", + "emissao_mes", + "emissao_ano", + "ptres", + "ugr_codigo", + "valor_celula", + "dc", + ] + + db.insert_data( + data, + "nc_tesouro_pos__2026", + conflict_fields=unique_key, + primary_key=unique_key, + schema="siafi", + ) + logging.info("Carga finalizada com sucesso.") + except Exception as e: + logging.error("Erro na inserção: %s", str(e)) + raise + + process_emails_task = PythonOperator( + task_id="process_emails", + python_callable=process_email_data, + ) + + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + ) + + process_emails_task >> insert_to_db_task \ No newline at end of file diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/ne_tesouro_emendas_mir_ingest_dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/ne_tesouro_emendas_mir_ingest_dag.py new file mode 100644 index 00000000..1feaad6f --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/ne_tesouro_emendas_mir_ingest_dag.py @@ -0,0 +1,200 @@ +from typing import Dict, Any, Optional +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +from airflow.models.param import Param +from datetime import datetime, timedelta +import logging +import json +from schedule_loader import get_dynamic_schedule +from cliente_email import fetch_and_process_email +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn +import pandas as pd +import io + +# Configurações básicas da DAG +default_args = { + "owner": "Tiago", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +COLUMN_MAPPING = { + 0: "emissao_mes", + 1: "emissao_dia", + 2: "programa_governo", + 3: "programa_governo_descricao", + 4: "acao_governo", + 5: "acao_governo_descricao", + 6: "autor_emendas_orcamento", + 7: "autor_emendas_orcamento_descricao", + 8: "localizador_gasto", + 9: "localizador_gasto_descricao", + 10: "regiao_pt", + 11: "uf_pt", + 12: "uf_pt_descricao", + 13: "municipio_pt", + 14: "ne_ccor", + 15: "ne_num_processo", + 16: "ne_info_complementar", + 17: "ne_ccor_descricao", + 18: "doc_observacao", + 19: "grupo_despesa", + 20: "grupo_despesa_descricao", + 21: "natureza_despesa", + 22: "natureza_despesa_descricao", + 23: "modalidade_aplicacao", + 24: "modalidade_aplicacao_descricao", + 25: "ne_ccor_favorecido", + 26: "ne_ccor_favorecido_descricao", + 27: "ne_ccor_ano_emissao", + 28: "ptres", + 29: "item_informacao", + 30: "item_informacao_descricao", + 31: "despesas_empenhadas", + 32: "despesas_liquidadas", + 33: "despesas_pagas", + 34: "restos_a_pagar_inscritos", + 35: "restos_a_pagar_pagos", +} + +EMAIL_SUBJECT = "notas_de_empenhos_emendas_parlamentares" +SKIPROWS = 12 + +# Configurações da DAG +with DAG( + dag_id="email_tesouro_emendas_ingest", + default_args=default_args, + description="Processa anexos dos empenhos vindo do email, formata e insere no db", + schedule_interval=get_dynamic_schedule("empenhos_tesouro_emendas_ingest_dag"), + start_date=datetime(2023, 12, 1), + catchup=False, + params={ + "data_referencia": Param( + default=None, + type=["string", "null"], + title="Data de Referencia", + description=( + "Data para filtrar os e-mails recebidos (formato YYYY-MM-DD). " + "Se nao informado, usa o dia atual." + ), + ) + }, + tags=["MIR", "email", "empenhos", "tesouro", "emendas"], +) as dag: + + def process_email_data(**context: Dict[str, Any]) -> Optional[Any]: + creds = json.loads(Variable.get("email_credentials")) + + EMAIL = creds["email"] + PASSWORD = creds["password"] + IMAP_SERVER = creds["imap_server"] + SENDER_EMAIL = creds["sender_email"] + params = context.get("params", {}) + data_referencia = params.get("data_referencia") + + target_date = None + if data_referencia: + try: + target_date = datetime.strptime(data_referencia, "%Y-%m-%d").date() + except ValueError as exc: + raise ValueError( + "Parametro 'data_referencia' invalido. Use o formato YYYY-MM-DD." + ) from exc + + try: + logging.info( + "Iniciando o processamento dos emails para a data: %s", + target_date.isoformat() if target_date else "dia atual", + ) + csv_data = fetch_and_process_email( + IMAP_SERVER, + EMAIL, + PASSWORD, + SENDER_EMAIL, + EMAIL_SUBJECT, + COLUMN_MAPPING, + skiprows=SKIPROWS, + target_date=target_date, + ) + if not csv_data: + logging.warning( + "Nenhum CSV valido foi extraido dos e-mails encontrados " + "para o assunto esperado." + ) + return None + + logging.info( + "CSV processado com sucesso. Dados encontrados: %s", len(csv_data) + ) + return csv_data + except Exception as e: + logging.error("Erro no processamento dos emails: %s", str(e)) + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + """ + Função para inserir os dados no banco de dados. + Os dados do CSV são recuperados do XCom. + """ + try: + task_instance: Any = context["ti"] + csv_data: Any = task_instance.xcom_pull(task_ids="process_emails") + + if not csv_data: + logging.warning("Nenhum dado para inserir no banco.") + return + + df = pd.read_csv(io.StringIO(csv_data)) + df = df[df["ne_ccor_ano_emissao"].astype(str).str.startswith("20")] + data = df.to_dict(orient="records") + + # Adicionar dt_ingest a cada registro + for record in data: + record["dt_ingest"] = datetime.now().isoformat() + + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + unique_key = [ + "ne_ccor", + "natureza_despesa", + "doc_observacao", + "ne_ccor_ano_emissao", + "emissao_dia", + "emissao_mes", + "despesas_empenhadas", + "despesas_liquidadas", + "despesas_pagas", + ] + + db.insert_data( + data, + "ne_tesouro_emendas", + conflict_fields=unique_key, + primary_key=unique_key, + schema="siafi", + ) + logging.info("Dados inseridos com sucesso no banco de dados.") + except Exception as e: + logging.error("Erro ao inserir dados no banco: %s", str(e)) + raise + + # Tarefa 1: Processar os e-mails e retornar CSV + process_emails_task = PythonOperator( + task_id="process_emails", + python_callable=process_email_data, + provide_context=True, + ) + + # Tarefa 2: Inserir os dados no banco de dados + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + provide_context=True, + ) + + # Fluxo da DAG + process_emails_task >> insert_to_db_task diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/ne_tesouro_mir_ingest_dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/ne_tesouro_mir_ingest_dag.py new file mode 100644 index 00000000..607f2622 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/ne_tesouro_mir_ingest_dag.py @@ -0,0 +1,184 @@ +from typing import Dict, Any, List +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +from airflow.models.param import Param +from datetime import datetime, timedelta +import logging +import json +from schedule_loader import get_dynamic_schedule +from cliente_email import fetch_email_with_zip, extract_csv_from_zip +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn + +# Configurações básicas da DAG +default_args = { + "owner": "Tiago", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +COLUMN_MAPPING = { + 0: "programa_governo", + 1: "programa_governo_descricao", + 2: "acao_governo", + 3: "acao_governo_descricao", + 4: "emissao_mes", + 5: "emissao_dia", + 6: "ne_ccor", + 7: "ne_num_processo", + 8: "ne_info_complementar", + 9: "ne_ccor_descricao", + 10: "doc_observacao", + 11: "natureza_despesa", + 12: "natureza_despesa_descricao", + 13: "ne_ccor_favorecido", + 14: "ne_ccor_favorecido_descricao", + 15: "ne_ccor_ano_emissao", + 16: "ptres", + 17: "fonte_recursos_detalhada", + 18: "fonte_recursos_detalhada_descricao", + 19: "despesas_empenhadas", + 20: "despesas_liquidadas", + 21: "despesas_pagas", + 22: "restos_a_pagar_inscritos", + 23: "restos_a_pagar_pagos", +} + +UNIQUE_KEY = [ + "ne_ccor", + "natureza_despesa", + "doc_observacao", + "ne_ccor_ano_emissao", + "emissao_dia", + "emissao_mes", + "despesas_empenhadas", + "despesas_liquidadas", + "despesas_pagas", +] + +EMAIL_SUBJECT = "notas_de_empenho_ano_atual" +SKIPROWS = 8 +TABLE_NAME = "ne_tesouro" +SCHEMA_NAME = "siafi" +OPTIONAL_COLUMNS = ["restos_a_pagar_inscritos", "restos_a_pagar_pagos"] + +# Configurações da DAG +with DAG( + dag_id="email_tesouro_teds_notas_empenhadas_ingest_dag", + default_args=default_args, + description="Processa anexos dos empenhos vindo do email, formata e insere no db", + schedule_interval=get_dynamic_schedule("empenhos_tesouro_parlamentares_ingest_dag"), + start_date=datetime(2023, 12, 1), + catchup=False, + params={ + "data_referencia": Param( + default=None, + type=["string", "null"], + title="Data de Referencia", + description=( + "Data para filtrar os e-mails recebidos (formato YYYY-MM-DD). " + "Se nao informado, usa o dia atual." + ), + ) + }, + tags=["MIR", "email", "empenhos", "tesouro"], +) as dag: + + def _get_db_client() -> ClientPostgresDB: + return ClientPostgresDB(get_postgres_conn("postgres_mir")) + + def _table_exists(db: ClientPostgresDB) -> bool: + result = db.execute_query( + f"SELECT to_regclass('{SCHEMA_NAME}.{TABLE_NAME}') IS NOT NULL;" + ) + return bool(result and result[0][0]) + + def _normalize_optional_columns(df): + for column in OPTIONAL_COLUMNS: + if column not in df.columns: + df[column] = None + return df + + def _ensure_optional_columns_in_table(db: ClientPostgresDB) -> None: + db.alter_table( + {column: None for column in OPTIONAL_COLUMNS}, + TABLE_NAME, + schema=SCHEMA_NAME, + ) + + def _insert_dataframe(df, db: ClientPostgresDB) -> int: + df = _normalize_optional_columns(df) + df = df[df["ne_ccor_ano_emissao"].astype(str).str.startswith("20")] + records = df.to_dict(orient="records") + for r in records: + r["dt_ingest"] = datetime.now().isoformat() + + if _table_exists(db): + _ensure_optional_columns_in_table(db) + + db.insert_data( + records, + TABLE_NAME, + conflict_fields=UNIQUE_KEY, + primary_key=UNIQUE_KEY, + schema=SCHEMA_NAME, + ) + return len(records) + + def fetch_and_ingest(**context: Dict[str, Any]) -> Dict[str, int]: + """Processa cada anexo e ingere imediatamente, evitando acúmulo em memória.""" + creds = json.loads(Variable.get("email_credentials")) + params = context.get("params", {}) + data_referencia = params.get("data_referencia") + + target_date = None + if data_referencia: + try: + target_date = datetime.strptime(data_referencia, "%Y-%m-%d").date() + except ValueError as exc: + raise ValueError( + "Parametro 'data_referencia' invalido. Use o formato YYYY-MM-DD." + ) from exc + + logging.info( + "Buscando e-mails para a data: %s", + target_date.isoformat() if target_date else "dia atual", + ) + + zip_payloads: List[bytes] = fetch_email_with_zip( + creds["imap_server"], + creds["email"], + creds["password"], + creds["sender_email"], + EMAIL_SUBJECT, + target_date=target_date, + ) + + if not zip_payloads: + logging.warning("Nenhum anexo ZIP encontrado.") + return {"attachments": 0, "records": 0} + + logging.info("Total de anexos ZIP encontrados: %s", len(zip_payloads)) + + db = _get_db_client() + total_records = 0 + + for idx, payload in enumerate(zip_payloads, 1): + df = extract_csv_from_zip(payload, COLUMN_MAPPING, SKIPROWS) + if df is not None: + count = _insert_dataframe(df, db) + total_records += count + logging.info("Anexo %s: %s registros inseridos", idx, count) + else: + logging.warning("Anexo %s ignorado (CSV inválido)", idx) + del df # Libera memória imediatamente + + logging.info("Total: %s anexos, %s registros", len(zip_payloads), total_records) + return {"attachments": len(zip_payloads), "records": total_records} + + PythonOperator( + task_id="fetch_and_ingest", + python_callable=fetch_and_ingest, + ) diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/pf_tesouro_mir_ingest_dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/pf_tesouro_mir_ingest_dag.py new file mode 100644 index 00000000..cd446634 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/pf_tesouro_mir_ingest_dag.py @@ -0,0 +1,157 @@ +from typing import Dict, Any +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +from datetime import datetime, timedelta +import logging +import json +import pandas as pd +import io +from schedule_loader import get_dynamic_schedule +from cliente_email import fetch_and_process_email +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn + +# Configurações básicas da DAG +default_args = { + "owner": "Tiago", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +# Mapeamento das colunas para as programações financeiras (recebidas) +COLUMN_MAPPING = { + 0: "emissao_mes", + 1: "emissao_dia", + 2: "ug_emitente", + 3: "ug_emitente_descricao", + 4: "ug_favorecido", + 5: "ug_favorecido_descricao", + 6: "pf_evento", + 7: "pf_evento_descricao", + 8: "pf", + 9: "pf_inscricao", + 10: "pf_acao", + 11: "pf_acao_descricao", + 12: "pf_fonte_recursos", + 13: "pf_fonte_recursos_descricao", + 14: "doc_observacao", + 15: "pf_valor_linha", +} + +# Assunto do email a ser processado +EMAIL_SUBJECT = "programacoes_financeiras" +SKIPROWS = 7 + +# Configurações da DAG +with DAG( + dag_id="email_programacoes_financeiras_mir_ingest", + default_args=default_args, + description="Processa anexo consolidado de PFs por email e insere no db", + schedule_interval=get_dynamic_schedule("pf_tesouro_mir_ingest_dag"), + start_date=datetime(2023, 12, 1), + catchup=False, + tags=["email", "pfs", "tesouro", "MIR"], +) as dag: + + def process_email_data(**context: Dict[str, Any]) -> str: + """ + Função para processar o email com programações financeiras. + """ + creds = json.loads(Variable.get("email_credentials")) + + EMAIL = creds["email"] + PASSWORD = creds["password"] + IMAP_SERVER = creds["imap_server"] + SENDER_EMAIL = creds["sender_email"] + + try: + logging.info("Iniciando o processamento do email de programações financeiras") + csv_data = fetch_and_process_email( + IMAP_SERVER, + EMAIL, + PASSWORD, + SENDER_EMAIL, + EMAIL_SUBJECT, + column_mapping=COLUMN_MAPPING, + skiprows=SKIPROWS, + ) + if not csv_data: + logging.warning("Nenhum e-mail encontrado com o assunto configurado") + return "" + + logging.info("CSV de PFs processado com sucesso.") + return csv_data + except Exception as e: + logging.error( + "Erro no processamento do email de programações financeiras: %s", + str(e), + ) + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + """ + Função para inserir os dados no banco de dados. + Os dados processados são recuperados do XCom. + """ + try: + task_instance: Any = context["ti"] + processed_data = task_instance.xcom_pull(task_ids="process_email") + + if not processed_data: + logging.warning("Nenhum dado para inserir no banco.") + return + + df = pd.read_csv(io.StringIO(processed_data)) + data = df.to_dict(orient="records") + + # Adicionar dt_ingest a cada registro + for record in data: + record["dt_ingest"] = datetime.now().isoformat() + + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + db.insert_data(data, "pf_tesouro", schema="siafi") + logging.info("Dados inseridos com sucesso no banco de dados.") + except Exception as e: + logging.error("Erro ao inserir dados no banco: %s", str(e)) + raise + + def clean_duplicates(**context: Dict[str, Any]) -> None: + """ + Task para remover duplicados da tabela 'siafi.pf_tesouro'. + """ + try: + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + db.remove_duplicates("pf_tesouro", COLUMN_MAPPING, schema="siafi") + + except Exception as e: + logging.error(f"Erro ao executar a limpeza de duplicados: {str(e)}") + raise + + # Tarefa 1: Processar o email com os dados consolidados + process_email_task = PythonOperator( + task_id="process_email", + python_callable=process_email_data, + provide_context=True, + ) + + # Tarefa 2: Inserir os dados no db + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + provide_context=True, + ) + + # Tarefa 3: Limpar duplicados no banco de dados + clean_duplicates_task = PythonOperator( + task_id="clean_duplicates", + python_callable=clean_duplicates, + provide_context=True, + ) + + # Fluxo da DAG + process_email_task >> insert_to_db_task >> clean_duplicates_task diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/programacao_acao_ptres_ingest_dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/programacao_acao_ptres_ingest_dag.py new file mode 100644 index 00000000..cd0ced74 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/mir/programacao_acao_ptres_ingest_dag.py @@ -0,0 +1,158 @@ +from typing import Dict, Any +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +from datetime import datetime, timedelta +import logging +import json +import pandas as pd +import io +from schedule_loader import get_dynamic_schedule +from cliente_email import fetch_and_process_email +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn + +# Configurações básicas da DAG +default_args = { + "owner": "Tiago", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +# Mapeamento das colunas para as programações financeiras (recebidas) +COLUMN_MAPPING = { + 0: "programa_governo", + 1: "programa_governo_descricao", + 2: "plano_orcamentario", + 3: "plano_orcamentario_descricao_1", + 4: "plano_orcamentario_descricao_2", + 5: "plano_orcamentario_descricao_3", + 6: "plano_orcamentario_descricao_4", + 7: "plano_orcamentario_descricao_5", + 8: "plano_orcamentario_descricao_6", + 9: "acao_governo", + 10: "acao_governo_descricao", + 11: "ptres", + 12: "natureza_despesa", + 13: "natureza_despesa_descricao", + 14: "dotacao_inicial", + 15: "dotacao_suplementar", + 16: "dotacao_atualizada", +} + +# Assunto do email a ser processado +EMAIL_SUBJECT = "programacao_acao_por_PTRES" +SKIPROWS = 5 + +# Configurações da DAG +with DAG( + dag_id="email_programacao_acao_por_PTRES_ingest", + default_args=default_args, + description="Processa anexo consolidado de programações de ação por PTRES por email e insere no db", + schedule_interval=get_dynamic_schedule("programacao_acao_ptres_ingest_dag"), + start_date=datetime(2023, 12, 1), + catchup=False, + tags=["email", "ptres", "tesouro", "MIR"], +) as dag: + + def process_email_data(**context: Dict[str, Any]) -> str: + """ + Função para processar o email com programações financeiras. + """ + creds = json.loads(Variable.get("email_credentials")) + + EMAIL = creds["email"] + PASSWORD = creds["password"] + IMAP_SERVER = creds["imap_server"] + SENDER_EMAIL = creds["sender_email"] + + try: + logging.info("Iniciando o processamento do email de programações financeiras") + csv_data = fetch_and_process_email( + IMAP_SERVER, + EMAIL, + PASSWORD, + SENDER_EMAIL, + EMAIL_SUBJECT, + column_mapping=COLUMN_MAPPING, + skiprows=SKIPROWS, + ) + if not csv_data: + logging.warning("Nenhum e-mail encontrado com o assunto configurado") + return "" + + logging.info("CSV de programações de ação por PTRES processado com sucesso.") + return csv_data + except Exception as e: + logging.error( + "Erro no processamento do email de programações de ação por PTRES: %s", + str(e), + ) + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + """ + Função para inserir os dados no banco de dados. + Os dados processados são recuperados do XCom. + """ + try: + task_instance: Any = context["ti"] + processed_data = task_instance.xcom_pull(task_ids="process_email") + + if not processed_data: + logging.warning("Nenhum dado para inserir no banco.") + return + + df = pd.read_csv(io.StringIO(processed_data)) + data = df.to_dict(orient="records") + + # Adicionar dt_ingest a cada registro + for record in data: + record["dt_ingest"] = datetime.now().isoformat() + + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + db.insert_data(data, "programacao_acao_ptres", schema="siafi") + logging.info("Dados inseridos com sucesso no banco de dados.") + except Exception as e: + logging.error("Erro ao inserir dados no banco: %s", str(e)) + raise + + def clean_duplicates(**context: Dict[str, Any]) -> None: + """ + Task para remover duplicados da tabela 'siafi.programacao_acao_ptres'. + """ + try: + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + db.remove_duplicates("programacao_acao_ptres", COLUMN_MAPPING, schema="siafi") + + except Exception as e: + logging.error(f"Erro ao executar a limpeza de duplicados: {str(e)}") + raise + + # Tarefa 1: Processar o email com os dados consolidados + process_email_task = PythonOperator( + task_id="process_email", + python_callable=process_email_data, + provide_context=True, + ) + + # Tarefa 2: Inserir os dados no db + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + provide_context=True, + ) + + # Tarefa 3: Limpar duplicados no banco de dados + clean_duplicates_task = PythonOperator( + task_id="clean_duplicates", + python_callable=clean_duplicates, + provide_context=True, + ) + + # Fluxo da DAG + process_email_task >> insert_to_db_task >> clean_duplicates_task diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/nc_tesouro_ingest.dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/nc_tesouro_ingest.dag.py new file mode 100644 index 00000000..db850e75 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/nc_tesouro_ingest.dag.py @@ -0,0 +1,240 @@ +from typing import Dict, Any, cast +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +from datetime import datetime, timedelta +import logging +import json +import pandas as pd +import io +from schedule_loader import get_dynamic_schedule +from cliente_email import fetch_and_process_email +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn + +# Configurações básicas da DAG +default_args = { + "owner": "Davi", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +# Mapeamento das colunas para as notas de crédito +COLUMN_MAPPING = { + 0: "emissao_mes", + 1: "emissao_dia", + 2: "nc", + 3: "nc_transferencia", + 4: "nc_fonte_recursos", + 5: "nc_fonte_recursos_descricao", + 6: "ptres", + 7: "nc_evento", + 8: "nc_evento_descricao", + 9: "ug_responsavel", + 10: "ug_responsavel_descricao", + 11: "natureza_despesa", + 12: "natureza_despesa_detalhada", + 13: "plano_interno", + 14: "plano_detalhado_descricao1", + 15: "plano_detalhado_descricao2", + 16: "favorecido_doc", + 17: "favorecido_doc_descricao", + 18: "nc_valor_linha", + 19: "movimento_liquido", +} + +# Configurações dos emails +EMAIL_CONFIGS = { + "enviadas": { + "subject": "notas_credito_enviadas_devolvidas_a_partir_de_2024", + "column_mapping": COLUMN_MAPPING, + "skiprows": 10, + }, + "recebidas": { + "subject": "notas_credito_recebidas_a_partir_de_2024", + "column_mapping": None, + "skiprows": 6, + }, +} + +# Configurações da DAG +with DAG( + dag_id="email_notas_credito_ingest", + default_args=default_args, + description="Processa anexos das NCs vindo de dois emails, formata e insere no db", + schedule_interval=get_dynamic_schedule("nc_tesouro_ingest_dag"), + start_date=datetime(2023, 12, 1), + catchup=False, + tags=["email", "ncs", "tesouro"], +) as dag: + + def process_email_data(email_type: str, **context: Dict[str, Any]) -> pd.DataFrame: + """ + Função genérica para processar emails de notas de crédito. + """ + config = EMAIL_CONFIGS[email_type] + creds_data = json.loads(Variable.get("email_credentials")) + creds = cast(Dict[str, str], creds_data) + config = cast(Dict[str, Any], config) + + try: + logging.info(f"Iniciando o processamento das NCs {email_type}") + csv_data = fetch_and_process_email( + creds["imap_server"], + creds["email"], + creds["password"], + creds["sender_email"], + config["subject"], + config["column_mapping"], + skiprows=config["skiprows"], + ) + + if not csv_data: + logging.warning(f"Nenhum e-mail encontrado para NCs {email_type}") + return pd.DataFrame() + + df = pd.read_csv(io.StringIO(csv_data)) + + # Se não tem mapeamento de colunas (recebidas), aplicar o mapeamento padrão + if config["column_mapping"] is None and not df.empty: + expected_columns = list(COLUMN_MAPPING.values()) + if len(df.columns) == len(expected_columns): + df.columns = pd.Index(expected_columns) + else: + logging.warning( + f"N coluna incompatível:{len(expected_columns)},{len(df.columns)}" + ) + + logging.info( + f"CSV de NCs {email_type} processado com sucesso: {len(df)} registros" + ) + return df + + except Exception as e: + logging.error( + f"Erro no processamento dos emails de NCs {email_type}: {str(e)}" + ) + raise + + def process_email_data_enviadas(**context: Dict[str, Any]) -> pd.DataFrame: + """Wrapper para processar emails enviadas.""" + return process_email_data("enviadas", **context) + + def process_email_data_recebidas(**context: Dict[str, Any]) -> pd.DataFrame: + """Wrapper para processar emails recebidas.""" + return process_email_data("recebidas", **context) + + def combine_data(**context: Dict[str, Any]) -> pd.DataFrame: + """ + Função para combinar os dados dos dois emails. + """ + try: + task_instance: Any = context["ti"] + df_enviadas = cast( + pd.DataFrame, task_instance.xcom_pull(task_ids="process_emails_enviadas") + ) + df_recebidas = cast( + pd.DataFrame, task_instance.xcom_pull(task_ids="process_emails_recebidas") + ) + + # Combinar DataFrames válidos + dfs = [ + df + for df in [df_enviadas, df_recebidas] + if df is not None and not df.empty + ] + + if not dfs: + logging.warning("Nenhum dado foi encontrado para combinar.") + return pd.DataFrame() + + # Combinar os DataFrames e adicionar dt_ingest + combined_df = pd.concat(dfs, ignore_index=True) + combined_df["dt_ingest"] = datetime.now().isoformat() + + logging.info(f"Dados combinados: {len(combined_df)} registros no total.") + return combined_df + + except Exception as e: + logging.error(f"Erro ao combinar os dados: {str(e)}") + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + """ + Função para inserir os dados no banco de dados. + """ + try: + task_instance: Any = context["ti"] + combined_df = task_instance.xcom_pull(task_ids="combine_data") + + if combined_df is None or combined_df.empty: + logging.warning("Nenhum dado para inserir no banco.") + return + + data = combined_df.to_dict(orient="records") + + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + db.insert_data(data, "nc_tesouro", schema="siafi") + logging.info("Dados inseridos com sucesso no banco de dados.") + except Exception as e: + logging.error("Erro ao inserir dados no banco: %s", str(e)) + raise + + def clean_duplicates(**context: Dict[str, Any]) -> None: + """ + Task para remover duplicados da tabela 'siafi.pf_tesouro'. + """ + try: + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + db.remove_duplicates("nc_tesouro", COLUMN_MAPPING, schema="siafi") + + except Exception as e: + logging.error(f"Erro ao executar a limpeza de duplicados: {str(e)}") + raise + + # Tarefa 1: Processar os e-mails de notas de crédito enviadas/devolvidas + process_emails_enviadas_task = PythonOperator( + task_id="process_emails_enviadas", + python_callable=process_email_data_enviadas, + provide_context=True, + ) + + # Tarefa 2: Processar os e-mails de notas de crédito recebidas + process_emails_recebidas_task = PythonOperator( + task_id="process_emails_recebidas", + python_callable=process_email_data_recebidas, + provide_context=True, + ) + + # Tarefa 3: Combinar os dados dos dois emails + combine_data_task = PythonOperator( + task_id="combine_data", + python_callable=combine_data, + provide_context=True, + ) + + # Tarefa 4: Inserir os dados no banco de dados + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + provide_context=True, + ) + + # Tarefa 5: Limpar duplicados no banco de dados + clean_duplicates_task = PythonOperator( + task_id="clean_duplicates", + python_callable=clean_duplicates, + provide_context=True, + ) + + # Fluxo da DAG + ( + [process_emails_enviadas_task, process_emails_recebidas_task] + >> combine_data_task + >> insert_to_db_task + >> clean_duplicates_task + ) diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/pf_tesouro_ingest_dag.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/pf_tesouro_ingest_dag.py new file mode 100644 index 00000000..dd094853 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/pf_tesouro_ingest_dag.py @@ -0,0 +1,249 @@ +from typing import Dict, Any +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +from datetime import datetime, timedelta +import logging +import json +import pandas as pd +import io +from schedule_loader import get_dynamic_schedule +from cliente_email import fetch_and_process_email +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn + +# Configurações básicas da DAG +default_args = { + "owner": "Davi", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +# Mapeamento das colunas para as programações financeiras +COLUMN_MAPPING = { + 0: "emissao_mes", + 1: "emissao_dia", + 2: "ug_emitente", + 3: "ug_emitente_descricao", + 4: "ug_favorecido", + 5: "ug_favorecido_descricao", + 6: "pf_evento", + 7: "pf_evento_descricao", + 8: "pf", + 9: "pf_inscricao", + 10: "pf_acao", + 11: "pf_acao_descricao", + 12: "pf_fonte_recursos", + 13: "pf_fonte_recursos_descricao", + 14: "pf_vinculacao_pagamento", + 15: "pf_vinculacao_pagamento_descricao", + 16: "pf_categoria_gasto", + 17: "pf_recurso", + 18: "pf_recurso_descricao", + 19: "doc_observacao", + 20: "pf_valor_linha", +} + +# Assuntos dos emails a serem processados +EMAIL_SUBJECT_ENVIADAS = "programacoes_financeiras_enviadas_devolvidas_a_partir_de_2024" +EMAIL_SUBJECT_RECEBIDAS = "programacoes_financeiras_recebidas_a_partir_de_2024" +SKIPROWS = 6 + +# Configurações da DAG +with DAG( + dag_id="email_programacoes_financeiras_ingest", + default_args=default_args, + description="Processa anexos das PFs vindo de dois emails, formata e insere no db", + schedule_interval=get_dynamic_schedule("pf_tesouro_ingest_dag"), + start_date=datetime(2023, 12, 1), + catchup=False, + tags=["email", "pfs", "tesouro"], +) as dag: + + def process_email_data_enviadas(**context: Dict[str, Any]) -> str: + """ + Função para processar os emails com programações financeiras enviadas. + """ + creds = json.loads(Variable.get("email_credentials")) + + EMAIL = creds["email"] + PASSWORD = creds["password"] + IMAP_SERVER = creds["imap_server"] + SENDER_EMAIL = creds["sender_email"] + + try: + logging.info( + "Iniciando o processamento dos emails de programações enviadas/devolvidas" + ) + csv_data = fetch_and_process_email( + IMAP_SERVER, + EMAIL, + PASSWORD, + SENDER_EMAIL, + EMAIL_SUBJECT_ENVIADAS, + COLUMN_MAPPING, + skiprows=SKIPROWS, + ) + if not csv_data: + logging.warning( + "Nenhum e-mail encontrado com o assunto de programações enviadas" + ) + return "" + + logging.info("CSV de PFs enviadas processado com sucesso.") + return csv_data + except Exception as e: + logging.error( + "Erro no processamento dos emails de programações enviadas: %s", + str(e), + ) + raise + + def process_email_data_recebidas(**context: Dict[str, Any]) -> str: + """ + Função para processar os emails com programações financeiras recebidas. + """ + creds = json.loads(Variable.get("email_credentials")) + + EMAIL = creds["email"] + PASSWORD = creds["password"] + IMAP_SERVER = creds["imap_server"] + SENDER_EMAIL = creds["sender_email"] + + try: + logging.info( + "Iniciando o processamento dos emails de programações recebidas..." + ) + csv_data = fetch_and_process_email( + IMAP_SERVER, + EMAIL, + PASSWORD, + SENDER_EMAIL, + EMAIL_SUBJECT_RECEBIDAS, + column_mapping=None, + skiprows=SKIPROWS, + ) + if not csv_data: + logging.warning( + "Nenhum e-mail encontrado com o assunto de programações recebidas." + ) + return "" + + logging.info("CSV de PFs recebidas processado com sucesso.") + return csv_data + except Exception as e: + logging.error( + "Erro no processamento dos emails de programações recebidas: %s", str(e) + ) + raise + + def combine_data(**context: Dict[str, Any]) -> str: + """ + Função para combinar os dados dos dois emails. + """ + try: + task_instance: Any = context["ti"] + enviadas_data = ( + task_instance.xcom_pull(task_ids="process_emails_enviadas") or "" + ) + recebidas_data = ( + task_instance.xcom_pull(task_ids="process_emails_recebidas") or "" + ) + + combined_data = enviadas_data + recebidas_data + + if combined_data: + logging.info("Dados combinados com sucesso.") + else: + logging.warning("Nenhum dado encontrado em ambos os emails.") + + return combined_data + except Exception as e: + logging.error(f"Erro ao combinar os dados: {str(e)}") + raise + + def insert_data_to_db(**context: Dict[str, Any]) -> None: + """ + Função para inserir os dados no banco de dados. + Os dados combinados são recuperados do XCom. + """ + try: + task_instance: Any = context["ti"] + combined_data = task_instance.xcom_pull(task_ids="combine_data") + + if not combined_data: + logging.warning("Nenhum dado para inserir no banco.") + return + + df = pd.read_csv(io.StringIO(combined_data)) + data = df.to_dict(orient="records") + + # Adicionar dt_ingest a cada registro + for record in data: + record["dt_ingest"] = datetime.now().isoformat() + + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + db.insert_data(data, "pf_tesouro", schema="siafi") + logging.info("Dados inseridos com sucesso no banco de dados.") + except Exception as e: + logging.error("Erro ao inserir dados no banco: %s", str(e)) + raise + + def clean_duplicates(**context: Dict[str, Any]) -> None: + """ + Task para remover duplicados da tabela 'siafi.pf_tesouro'. + """ + try: + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + db.remove_duplicates("pf_tesouro", COLUMN_MAPPING, schema="siafi") + + except Exception as e: + logging.error(f"Erro ao executar a limpeza de duplicados: {str(e)}") + raise + + # Tarefa 1: Processar os e-mails de programações enviadas/devolvidas + process_emails_enviadas_task = PythonOperator( + task_id="process_emails_enviadas", + python_callable=process_email_data_enviadas, + provide_context=True, + ) + + # Tarefa 2: Processar os e-mails de programações recebidas + process_emails_recebidas_task = PythonOperator( + task_id="process_emails_recebidas", + python_callable=process_email_data_recebidas, + provide_context=True, + ) + + # Tarefa 3: Combinar os dados dos dois emails + combine_data_task = PythonOperator( + task_id="combine_data", + python_callable=combine_data, + provide_context=True, + ) + + # Tarefa 4: Inserir os dados no db + insert_to_db_task = PythonOperator( + task_id="insert_to_db", + python_callable=insert_data_to_db, + provide_context=True, + ) + + # Tarefa 5: Limpar duplicados no banco de dados + clean_duplicates_task = PythonOperator( + task_id="clean_duplicates", + python_callable=clean_duplicates, + provide_context=True, + ) + + # Fluxo da DAG + ( + [process_emails_enviadas_task, process_emails_recebidas_task] + >> combine_data_task + >> insert_to_db_task + >> clean_duplicates_task + ) diff --git a/airflow_lappis/dags/data_ingest/tesouro_gerencial/visao_orcamentaria_ingest.py b/airflow_lappis/dags/data_ingest/tesouro_gerencial/visao_orcamentaria_ingest.py new file mode 100644 index 00000000..5819781a --- /dev/null +++ b/airflow_lappis/dags/data_ingest/tesouro_gerencial/visao_orcamentaria_ingest.py @@ -0,0 +1,324 @@ +from typing import Dict, Any, Optional, List +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +from datetime import datetime, timedelta +import logging +import json +import pandas as pd +import io +import re +import zipfile +from schedule_loader import get_dynamic_schedule +from cliente_email import fetch_email_with_zip +from cliente_postgres import ClientPostgresDB +from postgres_helpers import get_postgres_conn + +# Configurações básicas da DAG +default_args = { + "owner": "Davi", + "depends_on_past": False, + "retries": 1, + "retry_delay": timedelta(minutes=5), +} + +# Mapeamento das colunas para visão orçamentária +COLUMN_MAPPING = { + 0: "unidade_orcamentaria", + 1: "unidade_orcamentaria_desc", + 2: "acao_governo", + 3: "acao_governo_desc", + 4: "programa_governo", + 5: "programa_governo_desc", + 6: "unidade_plano_orcamentario", + 7: "plano_orcamentario_1", + 8: "plano_orcamentario_2", + 9: "programa_plano_orcamentario", + 10: "acao_plano_orcamentario", + 11: "plano_orcamentario_6", + 12: "plano_orcamentario_desc", + 13: "elemento_despesa", + 14: "elemento_despesa_desc", + 15: "orgao_uge", + 16: "orgao_uge_desc", + 17: "uge_matriz_filial", + 18: "ug_executora", + 19: "ug_executora_desc", + 20: "projeto_inicial_loa", + 21: "dotacao_inicial", + 22: "dotacao_atualizada", + 23: "credito_disponivel", + 24: "despesas_empenhadas", + 25: "despesas_a_liquidar", + 26: "despesar_a_pagar", + 27: "despesas_pagas", + 28: "restos_a_pagar_inscritos", + 29: "restos_a_pagar_pagos", +} + +EMAIL_SUBJECT = "visao_orcamentaria_total_ipea" + +# Configurações da DAG +with DAG( + dag_id="visao_orcamentaria_ingest", + default_args=default_args, + description=( + "DAG processa anexos da visão orçamentária total IPEA " + "vindo do email, formata e insere no db" + ), + schedule_interval=get_dynamic_schedule("visao_orcamentaria_ingest"), + start_date=datetime(2023, 12, 1), + catchup=False, + tags=["email", "visao_orcamentaria", "tesouro"], +) as dag: + + def _is_valid_data_line(line: str, columns: List[str]) -> bool: + """Verifica se a linha contém dados válidos para processamento.""" + # Verifica se é linha de cabeçalho, separador ou vazia + header_indicators = [ + "Páginas:", + "Unidade Orçamentária", + '"Unidade Orçamentária"', + '"UG Executora"', + '"PROJETO INICIAL DA LOA"', + ] + + if ( + not line + or line.startswith(" ") + or len(columns) < 20 + or any(indicator in line for indicator in header_indicators) + ): + return False + + # Verifica se tem dados essenciais preenchidos + unidade_orc = columns[0].strip('"').strip() if columns else "" + elemento_desp = columns[13].strip('"').strip() if len(columns) > 13 else "" + + return bool( + unidade_orc + and unidade_orc not in ["", '""'] + and elemento_desp + and elemento_desp not in ["", '""'] + ) + + def _clean_value(value: str) -> str: + """ + Limpa e padroniza valores para inserção no banco. + Mantém todos os dados como TEXT para evitar problemas de conversão. + """ + if not value or value in ["nan", "NaN", "None", "null", ""]: + return "" + + # Remove aspas desnecessárias + cleaned = str(value).strip('"').strip() + + # Trata valores especiais + if cleaned in ["nan", "NaN", "None", "null"]: + return "" + + return cleaned + + def _process_data_block(data_lines: List[str], year: str) -> List[Dict[str, Any]]: + """Processa um bloco de dados de um ano específico.""" + block_data = [] + + for line in data_lines: + line = line.strip() + columns = line.split("\t") + + if not _is_valid_data_line(line, columns): + continue + + # Remove aspas e limpa colunas + columns = [col.strip('"').strip() for col in columns] + + # Cria registro com mapeamento de colunas e limpeza de valores + row_data = {} + for col_index, col_name in COLUMN_MAPPING.items(): + raw_value = columns[col_index] if col_index < len(columns) else "" + row_data[col_name] = _clean_value(raw_value) + + row_data["ano_exercicio"] = str(year) + block_data.append(row_data) + + return block_data + + def _parse_csv_by_year_blocks(csv_content: str) -> List[Dict[str, Any]]: + """ + Processa CSV organizando dados por blocos de ano. + + Lógica: + 1. Encontra linhas com "Ano Lançamento: XXXX" + 2. Pula as próximas 5 linhas (cabeçalhos) + 3. Processa os dados até encontrar o próximo bloco ou fim do arquivo + 4. Adiciona a coluna ano_exercicio com o ano extraído + """ + lines = csv_content.strip().split("\n") + processed_data = [] + current_year = None + data_start_index = None + + logging.info(f"Iniciando processamento do CSV com {len(lines)} linhas") + + for i, line in enumerate(lines): + year_match = re.search(r"Ano Lançamento:\s*(\d{4})", line) + + if year_match: + # Processa bloco anterior se existir + if current_year and data_start_index is not None: + data_lines = lines[ + data_start_index : i - 2 + ] # Deixa margem para linhas vazias + year_data = _process_data_block(data_lines, current_year) + logging.info( + f"Processados {len(year_data)} registros para o ano " + f"{current_year}" + ) + processed_data.extend(year_data) + + # Inicia novo bloco + current_year = year_match.group(1) + data_start_index = i + 6 # Pula 5 linhas após "Ano Lançamento:" + logging.info( + f"Iniciando processamento do ano {current_year} a partir da " + f"linha {data_start_index}" + ) + + # Processa o último bloco + if current_year and data_start_index is not None: + data_lines = lines[data_start_index:] + year_data = _process_data_block(data_lines, current_year) + logging.info( + f"Processados {len(year_data)} registros para o último ano " + f"{current_year}" + ) + processed_data.extend(year_data) + + logging.info( + f"Processamento concluído. Total de registros: {len(processed_data)}" + ) + return processed_data + + def process_email_data(**context: Dict[str, Any]) -> Optional[str]: + """Processa o email e retorna os dados formatados.""" + creds = json.loads(Variable.get("email_credentials")) + + try: + logging.info("Iniciando o processamento dos emails...") + + # Busca o email com attachments ZIP + zip_payload = fetch_email_with_zip( + creds["imap_server"], + creds["email"], + creds["password"], + creds["sender_email"], + EMAIL_SUBJECT, + ) + + if not zip_payload: + logging.warning("Nenhum e-mail encontrado com o assunto esperado.") + return None + + # Extrai o CSV do ZIP (UTF-16) + with zipfile.ZipFile(io.BytesIO(zip_payload)) as zip_file: + for file_name in zip_file.namelist(): + if file_name.endswith(".csv"): + raw_data = zip_file.read(file_name) + csv_content = raw_data.decode("utf-16") + break + else: + logging.warning("Nenhum arquivo CSV encontrado no ZIP.") + return None + + # Processa o CSV com a lógica de blocos por ano + processed_data = _parse_csv_by_year_blocks(csv_content) + + if not processed_data: + logging.warning("Nenhum dado foi processado do CSV.") + return None + + df = pd.DataFrame(processed_data) + + # Adicionar dt_ingest a cada registro + df["dt_ingest"] = datetime.now().isoformat() + + # Garantir que todos os valores sejam strings para evitar problemas de tipo + for col in df.columns: + df[col] = df[col].astype(str) + + csv_string = df.to_csv(index=False) + + logging.info( + f"CSV processado com sucesso. Dados encontrados: " + f"{len(processed_data)} registros" + ) + return csv_string + + except Exception as e: + logging.error(f"Erro no processamento dos emails: {str(e)}") + raise + + def insert_and_clean_data(**context: Dict[str, Any]) -> None: + """Insere os dados no banco e limpa duplicados.""" + try: + task_instance: Any = context["ti"] + csv_data: Any = task_instance.xcom_pull(task_ids="process_emails") + + if not csv_data: + logging.warning("Nenhum dado para inserir no banco.") + return + + df = pd.read_csv(io.StringIO(csv_data)) + + # Garantir que todos os valores sejam strings para evitar problemas de tipo + for col in df.columns: + df[col] = df[col].astype(str) + df[col] = df[col].replace(["nan", "NaN", "None"], "") + + data = df.to_dict(orient="records") + + if not data: + logging.warning( + "DataFrame está vazio após o processamento. Nada será inserido." + ) + return + + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + + # Dropa a tabela antes de inserir novos dados + db.drop_table_if_exists("visao_orcamentaria_total", schema="siafi") + logging.info("Tabela siafi.visao_orcamentaria_total dropada com sucesso.") + + # Insere os dados + db.insert_data( + data, + "visao_orcamentaria_total", + schema="siafi", + ) + + logging.info( + f"Dados inseridos com sucesso no banco de dados. Total: " + f"{len(data)} registros" + ) + + except Exception as e: + logging.error(f"Erro ao inserir dados: {str(e)}") + raise + + # Definição das tarefas + process_emails_task = PythonOperator( + task_id="process_emails", + python_callable=process_email_data, + provide_context=True, + ) + + insert_and_clean_task = PythonOperator( + task_id="insert_and_clean", + python_callable=insert_and_clean_data, + provide_context=True, + ) + + # Fluxo da DAG + process_emails_task >> insert_and_clean_task diff --git a/airflow_lappis/dags/data_ingest/transfere_gov/mir/notas_de_credito_ingest_mir_dag.py b/airflow_lappis/dags/data_ingest/transfere_gov/mir/notas_de_credito_ingest_mir_dag.py new file mode 100644 index 00000000..a68379bc --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transfere_gov/mir/notas_de_credito_ingest_mir_dag.py @@ -0,0 +1,51 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_postgres import ClientPostgresDB +from cliente_ted import ClienteTed + + +@dag( + schedule_interval=get_dynamic_schedule("notas_de_credito_ingest_mir_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Mateus", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["notas de credito", "ted_api", "MIR"], +) +def notas_de_credito_mir_dag() -> None: + @task + def fetch_and_store_notas_de_credito() -> None: + logging.info("Iniciando fetch_and_store_notas_de_credito") + + api = ClienteTed() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + id_planos_acao = db.get_id_planos_acao() + + for id_plano_acao in id_planos_acao: + notas_de_credito = api.get_notas_de_credito_by_id_plano_acao(id_plano_acao) + if notas_de_credito: + for nota in notas_de_credito: + nota["dt_ingest"] = datetime.now().isoformat() + + db.insert_data( + notas_de_credito, + "notas_de_credito", + conflict_fields=["id_nota"], + primary_key=["id_nota"], + schema="transfere_gov", + ) + else: + logging.warning( + f"Nenhuma nota de crédito encontrada plano de ação {id_plano_acao}" + ) + + fetch_and_store_notas_de_credito() + +notas_de_credito_mir_dag() diff --git a/airflow_lappis/dags/data_ingest/transfere_gov/mir/plano_acao_ingest_mir_dag.py b/airflow_lappis/dags/data_ingest/transfere_gov/mir/plano_acao_ingest_mir_dag.py new file mode 100644 index 00000000..b07eba23 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transfere_gov/mir/plano_acao_ingest_mir_dag.py @@ -0,0 +1,56 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_ted import ClienteTed +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("plano_acao_ingest_mir_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Mateus", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["ted_api", "planos_acao", "MIR"], +) +def api_planos_acao_mir_dag() -> None: + + @task + def fetch_and_store_planos_acao() -> None: + logging.info("Starting api_planos_acao_dag DAG") + api = ClienteTed() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + id_programas = db.get_id_programas() + + total_processed = 0 + for id_programa in id_programas: + planos_acao_data = api.get_planos_acao_by_id_programa(id_programa) + if planos_acao_data: + for plano in planos_acao_data: + plano["dt_ingest"] = datetime.now().isoformat() + + db.insert_data( + planos_acao_data, + "planos_acao", + primary_key=["id_plano_acao"], + conflict_fields=["id_plano_acao"], + schema="transfere_gov", + ) + + total_processed += 1 + if total_processed % 10 == 0: + logging.info(f"Processed {total_processed} planos de ação") + else: + logging.warning(f"No planos de ação found for id_programa: {id_programa}") + + logging.info(f"Completed processing {total_processed} planos de ação") + + fetch_and_store_planos_acao() + +api_planos_acao_mir_dag() \ No newline at end of file diff --git a/airflow_lappis/dags/data_ingest/transfere_gov/mir/programa_financeira_ingest_mir_dag.py b/airflow_lappis/dags/data_ingest/transfere_gov/mir/programa_financeira_ingest_mir_dag.py new file mode 100644 index 00000000..7d1a19f9 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transfere_gov/mir/programa_financeira_ingest_mir_dag.py @@ -0,0 +1,55 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_postgres import ClientPostgresDB +from cliente_ted import ClienteTed + + +@dag( + schedule_interval=get_dynamic_schedule("programacao_financeira_ingest_mir_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Mateus", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["programacao_financeira", "ted_api", "MIR"], +) +def programacao_financeira_mir_dag() -> None: + @task + def fetch_and_store_programacao_financeira() -> None: + logging.info("Iniciando fetch_and_store_programacao_financeira") + + api = ClienteTed() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + id_planos_acao = db.get_id_planos_acao() + + for id_plano_acao in id_planos_acao: + programacao_financeira = api.get_programacao_financeira_by_id_plano_acao( + id_plano_acao + ) + if programacao_financeira: + # Adicionar dt_ingest a cada item + for item in programacao_financeira: + item["dt_ingest"] = datetime.now().isoformat() + + db.insert_data( + programacao_financeira, + "programacao_financeira", + conflict_fields=["id_programacao"], + primary_key=["id_programacao"], + schema="transfere_gov", + ) + else: + logging.warning( + f"Nenhuma programação financeira encontrada " + f"plano de ação {id_plano_acao}" + ) + + fetch_and_store_programacao_financeira() + +programacao_financeira_mir_dag() \ No newline at end of file diff --git a/airflow_lappis/dags/data_ingest/transfere_gov/mir/programas_ingest_mir_dag.py b/airflow_lappis/dags/data_ingest/transfere_gov/mir/programas_ingest_mir_dag.py new file mode 100644 index 00000000..27ed8436 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transfere_gov/mir/programas_ingest_mir_dag.py @@ -0,0 +1,55 @@ +import logging +from airflow.decorators import dag, task +from airflow.models import Variable +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_ted import ClienteTed +from cliente_postgres import ClientPostgresDB + +@dag( + schedule_interval=get_dynamic_schedule("api_programas_ted_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Mateus", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["ted_api", "programas", "MIR"], +) +def api_programas_ted_dag() -> None: + + @task + def fetch_and_ingest_programas() -> None: + logging.info("Iniciando extração de programas") + + api = ClienteTed() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + sigla_alvo = Variable.get("airflow_orgao_ted", default_var="MIR") + + logging.info(f"Filtrando programas pela sigla: {sigla_alvo}") + programas_data = api.get_programas_by_sigla_unidade_descentralizadora(sigla_alvo) + + if not programas_data: + logging.warning(f"Nenhum dado retornado para a sigla {sigla_alvo}") + return + + for programa in programas_data: + programa["dt_ingest"] = datetime.now().isoformat() + + db.insert_data( + programas_data, + "programas", + primary_key=["id_programa"], + conflict_fields=["id_programa"], + schema="transfere_gov", + ) + + logging.info(f"Sucesso: {len(programas_data)} programas inseridos/atualizados para {sigla_alvo}") + + fetch_and_ingest_programas() + +api_programas_ted_dag() \ No newline at end of file diff --git a/airflow_lappis/dags/data_ingest/transfere_gov/notas_de_credito_ingest_dag.py b/airflow_lappis/dags/data_ingest/transfere_gov/notas_de_credito_ingest_dag.py new file mode 100644 index 00000000..b8af4b6e --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transfere_gov/notas_de_credito_ingest_dag.py @@ -0,0 +1,53 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_postgres import ClientPostgresDB +from cliente_ted import ClienteTed + + +@dag( + schedule_interval=get_dynamic_schedule("notas_de_credito_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["notas de credito", "ted_api"], +) +def notas_de_credito_dag() -> None: + @task + def fetch_and_store_notas_de_credito() -> None: + logging.info("Iniciando fetch_and_store_notas_de_credito") + + api = ClienteTed() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + id_planos_acao = db.get_id_planos_acao() + + for id_plano_acao in id_planos_acao: + notas_de_credito = api.get_notas_de_credito_by_id_plano_acao(id_plano_acao) + if notas_de_credito: + # Adicionar dt_ingest a cada nota + for nota in notas_de_credito: + nota["dt_ingest"] = datetime.now().isoformat() + + db.insert_data( + notas_de_credito, + "notas_de_credito", + conflict_fields=["id_nota"], + primary_key=["id_nota"], + schema="transfere_gov", + ) + else: + logging.warning( + f"Nenhuma nota de crédito encontrada plano de ação {id_plano_acao}" + ) + + fetch_and_store_notas_de_credito() + + +dag_instance = notas_de_credito_dag() diff --git a/airflow_lappis/dags/data_ingest/transfere_gov/plano_acao_ingest_dag.py b/airflow_lappis/dags/data_ingest/transfere_gov/plano_acao_ingest_dag.py new file mode 100644 index 00000000..4ef35978 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transfere_gov/plano_acao_ingest_dag.py @@ -0,0 +1,58 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_ted import ClienteTed +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("plano_acao_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["ted_api", "planos_acao"], +) +def api_planos_acao_dag() -> None: + + @task + def fetch_and_store_planos_acao() -> None: + logging.info("Starting api_planos_acao_dag DAG") + api = ClienteTed() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + id_programas = db.get_id_programas() + + total_processed = 0 + for id_programa in id_programas: + planos_acao_data = api.get_planos_acao_by_id_programa(id_programa) + if planos_acao_data: + # Adicionar dt_ingest a cada plano + for plano in planos_acao_data: + plano["dt_ingest"] = datetime.now().isoformat() + + db.insert_data( + planos_acao_data, + "planos_acao", + primary_key=["id_plano_acao"], + conflict_fields=["id_plano_acao"], + schema="transfere_gov", + ) + + total_processed += 1 + if total_processed % 10 == 0: + logging.info(f"Processed {total_processed} planos de ação") + else: + logging.warning(f"No planos de ação found for id_programa: {id_programa}") + + logging.info(f"Completed processing {total_processed} planos de ação") + + fetch_and_store_planos_acao() + + +dag_instance = api_planos_acao_dag() diff --git a/airflow_lappis/dags/data_ingest/transfere_gov/programa_beneficiario_ingest_dag.py b/airflow_lappis/dags/data_ingest/transfere_gov/programa_beneficiario_ingest_dag.py new file mode 100644 index 00000000..c8e936f8 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transfere_gov/programa_beneficiario_ingest_dag.py @@ -0,0 +1,57 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_ted import ClienteTed +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("programa_beneficiario_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["ted_api", "programa_beneficiario"], +) +def api_programa_beneficiario_dag() -> None: + + @task + def fetch_and_store_programa_beneficiario() -> None: + logging.info("Starting api_programa_beneficiario_dag DAG") + api = ClienteTed() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + tx_codigo_siorgs = "7" + + beneficiario = api.get_ted_by_programa_beneficiario(tx_codigo_siorgs) + if beneficiario: + logging.info( + f"Tipo de beneficiario: {type(beneficiario)}, Conteúdo: {beneficiario}" + ) + logging.info("Inserting beneficiario into PostgreSQL") + unique_id_programas = [ + {"id_programa": id_prog} + for id_prog in {b["id_programa"] for b in beneficiario} + ] + + db.insert_data( + unique_id_programas, + "programas", + primary_key=["id_programa"], + conflict_fields=["id_programa"], + schema="transfere_gov", + ) + else: + logging.warning( + f"No beneficiario found for tx_codigo_siorg: {tx_codigo_siorgs}" + ) + + fetch_and_store_programa_beneficiario() + + +dag_instace = api_programa_beneficiario_dag() diff --git a/airflow_lappis/dags/data_ingest/transfere_gov/programacao_financeira_ingest_dag.py b/airflow_lappis/dags/data_ingest/transfere_gov/programacao_financeira_ingest_dag.py new file mode 100644 index 00000000..a86a0e26 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transfere_gov/programacao_financeira_ingest_dag.py @@ -0,0 +1,56 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_postgres import ClientPostgresDB +from cliente_ted import ClienteTed + + +@dag( + schedule_interval=get_dynamic_schedule("programacao_financeira_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["programacao_financeira", "ted_api"], +) +def programacao_financeira_dag() -> None: + @task + def fetch_and_store_programacao_financeira() -> None: + logging.info("Iniciando fetch_and_store_programacao_financeira") + + api = ClienteTed() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + id_planos_acao = db.get_id_planos_acao() + + for id_plano_acao in id_planos_acao: + programacao_financeira = api.get_programacao_financeira_by_id_plano_acao( + id_plano_acao + ) + if programacao_financeira: + # Adicionar dt_ingest a cada item + for item in programacao_financeira: + item["dt_ingest"] = datetime.now().isoformat() + + db.insert_data( + programacao_financeira, + "programacao_financeira", + conflict_fields=["id_programacao"], + primary_key=["id_programacao"], + schema="transfere_gov", + ) + else: + logging.warning( + f"Nenhuma programação financeira encontrada " + f"plano de ação {id_plano_acao}" + ) + + fetch_and_store_programacao_financeira() + + +dag_instance = programacao_financeira_dag() diff --git a/airflow_lappis/dags/data_ingest/transfere_gov/programas_ingest_dag.py b/airflow_lappis/dags/data_ingest/transfere_gov/programas_ingest_dag.py new file mode 100644 index 00000000..5aeaaba1 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transfere_gov/programas_ingest_dag.py @@ -0,0 +1,88 @@ +import logging +from airflow.decorators import dag, task +from airflow.models import Variable +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_ted import ClienteTed +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("programas_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Davi", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["ted_api", "programas"], +) +def api_programas_dag() -> None: + + @task + def fetch_and_update_programas() -> None: + logging.info("Starting api_programas_dag - Update Programs") + api = ClienteTed() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + id_programas = db.get_id_programas() + + total_processed = 0 + for id_programa in id_programas: + programas_data = api.get_programa_by_id_programa(id_programa) + if programas_data and len(programas_data) > 0: + programa = programas_data[0] + programa["dt_ingest"] = datetime.now().isoformat() + + # Alter table to add any new columns needed + db.alter_table(programa, "programas", schema="transfere_gov") + + # Insert/update the program data + db.insert_data( + programas_data, + "programas", + primary_key=["id_programa"], + conflict_fields=["id_programa"], + schema="transfere_gov", + ) + + total_processed += 1 + if total_processed % 10 == 0: + logging.info(f"Processed {total_processed} programs") + else: + logging.warning(f"No program data found for id_programa: {id_programa}") + + logging.info(f"Completed processing {total_processed} programs") + + @task + def fetch_and_update_programas_by_sigla() -> None: + logging.info("Starting fetch_and_update_programas_by_sigla - IPEA") + api = ClienteTed() + postgres_conn_str = get_postgres_conn() + db = ClientPostgresDB(postgres_conn_str) + sigla = Variable.get("airflow_orgao", default_var="IPEA").upper() + programas_data = api.get_programas_by_sigla_unidade_descentralizadora(sigla) + if programas_data and len(programas_data) > 0: + for programa in programas_data: + programa["dt_ingest"] = datetime.now().isoformat() + db.alter_table(programa, "programas", schema="transfere_gov") + db.insert_data( + programas_data, + "programas", + primary_key=["id_programa"], + conflict_fields=["id_programa"], + schema="transfere_gov", + ) + logging.info(f"Inserted/updated {len(programas_data)} programas for IPEA") + else: + logging.warning( + "No programas data found for sigla_unidade_descentralizadora=IPEA" + ) + + fetch_and_update_programas() + fetch_and_update_programas_by_sigla() + + +dag_instance = api_programas_dag() diff --git a/airflow_lappis/dags/data_ingest/transferegov_emendas/documentos_habeis_especias_ingest_dag.py b/airflow_lappis/dags/data_ingest/transferegov_emendas/documentos_habeis_especias_ingest_dag.py new file mode 100644 index 00000000..3acee287 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transferegov_emendas/documentos_habeis_especias_ingest_dag.py @@ -0,0 +1,71 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_transferegov_emendas import ClienteTransfereGov +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("documentos_habeis_especiais_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Tiago", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["transfere_gov_api", "documentos_especiais", "MIR"], +) +def api_documentos_habeis_especiais_dag() -> None: + """DAG para buscar e armazenar documentos hábeis especiais do Transfere Gov.""" + + @task + def fetch_and_store_documentos_habeis_especiais() -> None: + logging.info( + "[documentos_habeis_especiais_ingest_dag.py] Iniciando extração documentos " + "hábeis especiais" + ) + + api = ClienteTransfereGov() + postgres_conn_str = get_postgres_conn('postgres_mir') + db = ClientPostgresDB(postgres_conn_str) + + # Busca todos os documentos hábeis especiais com paginação automática + documentos_data = api.get_all_documentos_habeis_especiais(page_size=1000) + + if documentos_data and len(documentos_data) > 0: + # Adicionar dt_ingest a cada documento + for documento in documentos_data: + documento["dt_ingest"] = datetime.now().isoformat() + + # Inserir/atualizar dados no banco + logging.info( + f"[documentos_habeis_especiais_ingest_dag.py] Inserindo " + f"{len(documentos_data)} documentos hábeis especiais no " + f"schema transfere_gov" + ) + db.insert_data( + documentos_data, + "documentos_habeis_especiais", + conflict_fields=["id_dh"], + primary_key=["id_dh"], + schema="transferegov_emendas", + ) + + logging.info( + f"[documentos_habeis_especiais_ingest_dag.py] Concluído. " + f"Total de {len(documentos_data)} documentos hábeis especiais " + f"inseridos/atualizados" + ) + else: + logging.warning( + "[documentos_habeis_especiais_ingest_dag.py] Nenhum documento hábil " + "especial encontrado" + ) + + fetch_and_store_documentos_habeis_especiais() + + +dag_instance = api_documentos_habeis_especiais_dag() diff --git a/airflow_lappis/dags/data_ingest/transferegov_emendas/empenhos_especiais_ingest_dag.py b/airflow_lappis/dags/data_ingest/transferegov_emendas/empenhos_especiais_ingest_dag.py new file mode 100644 index 00000000..69542a48 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transferegov_emendas/empenhos_especiais_ingest_dag.py @@ -0,0 +1,67 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_transferegov_emendas import ClienteTransfereGov +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("empenhos_especiais_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Leonardo e Tiago", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["transfere_gov_api", "empenhos_especiais", "MIR"], +) +def api_empenhos_especiais_dag() -> None: + """DAG para buscar e armazenar empenhos especiais do Transfere Gov.""" + + @task + def fetch_and_store_empenhos_especiais() -> None: + logging.info( + "[empenhos_especiais_ingest_dag.py] Iniciando extração de empenhos especiais" + ) + + api = ClienteTransfereGov() + postgres_conn_str = get_postgres_conn('postgres_mir') + db = ClientPostgresDB(postgres_conn_str) + + # Busca todos os documentos hábeis especiais com paginação automática + documentos_data = api.get_all_empenhos_especiais(page_size=1000) + + if documentos_data and len(documentos_data) > 0: + # Adicionar dt_ingest a cada documento + for documento in documentos_data: + documento["dt_ingest"] = datetime.now().isoformat() + + # Inserir/atualizar dados no banco + logging.info( + f"[empenhos_especiais_ingest_dag.py] Inserindo {len(documentos_data)} " + "empenhos especiais no schema transfere_gov" + ) + db.insert_data( + documentos_data, + "empenhos_especiais", + conflict_fields=["id_empenho"], + primary_key=["id_empenho"], + schema="transferegov_emendas", + ) + + logging.info( + f"[empenhos_especiais_ingest_dag.py] Concluído. Total de " + f"{len(documentos_data)} empenhos especiais inseridos/atualizados" + ) + else: + logging.warning( + "[empenhos_especiais_ingest_dag.py] Nenhum empenho especial encontrado" + ) + + fetch_and_store_empenhos_especiais() + + +dag_instance = api_empenhos_especiais_dag() diff --git a/airflow_lappis/dags/data_ingest/transferegov_emendas/executor_especial_ingest_dag.py b/airflow_lappis/dags/data_ingest/transferegov_emendas/executor_especial_ingest_dag.py new file mode 100644 index 00000000..a2c7cbc5 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transferegov_emendas/executor_especial_ingest_dag.py @@ -0,0 +1,74 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime +from postgres_helpers import get_postgres_conn +from cliente_transferegov_emendas import ClienteTransfereGov +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval="@daily", + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Mateus e Gabriel", + "retries": 0, + # "retry_delay": timedelta(minutes=5), + }, + tags=["transfere_gov_api", "planos_acao_especiais", "MIR"], +) +def api_executor_especial_dag() -> None: + """DAG para buscar e armazenar executores especiais do Transfere Gov de forma massiva.""" + + @task + def fetch_and_store_executores_especiais() -> None: + logging.info( + "[executores_especiais_ingest_dag.py] Iniciando extração massiva de executores especiais" + ) + + api = ClienteTransfereGov() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + executores_data = api.get_all_executores_especiais(limit=1000) + + if executores_data and len(executores_data) > 0: + timestamp_atual = datetime.now().isoformat() + + unique = {} + for row in executores_data: + key = (row["id_plano_acao"], row["id_executor"]) + unique[key] = row # se já existir, substitui e mantém apenas 1 + + executores_data = list(unique.values()) + + for executor in executores_data: + executor["dt_ingest"] = timestamp_atual + + logging.info( + f"[executores_especiais_ingest_dag.py] Inserindo {len(executores_data)} " + "executores no schema transferegov_emendas" + ) + + # Inserção/Update (Upsert) + db.insert_data( + executores_data, + "executor_especial", + conflict_fields=["id_plano_acao", "id_executor"], + primary_key=["id_plano_acao", "id_executor"], + schema="transferegov_emendas", + ) + + logging.info( + f"[executores_especiais_ingest_dag.py] Concluído. Total de " + f"{len(executores_data)} executores inseridos/atualizados" + ) + else: + logging.warning( + "[executores_especiais_ingest_dag.py] Nenhum executor especial encontrado na API" + ) + + fetch_and_store_executores_especiais() + + +api_executor_especial_dag() diff --git a/airflow_lappis/dags/data_ingest/transferegov_emendas/finalidade_especial_ingest_dag.py b/airflow_lappis/dags/data_ingest/transferegov_emendas/finalidade_especial_ingest_dag.py new file mode 100644 index 00000000..7e8222ca --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transferegov_emendas/finalidade_especial_ingest_dag.py @@ -0,0 +1,78 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_transferegov_emendas import ClienteTransfereGov +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("finalidade_especial_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Tiago", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["transfere_gov_api", "finalidade_especial", "MIR"], +) +def api_finalidade_especial_dag() -> None: + """DAG para buscar e armazenar finalidades especiais do Transfere Gov.""" + + @task + def fetch_and_store_finalidade_especial() -> None: + logging.info( + "[finalidade_especial_ingest_dag.py] Iniciando extração finalidades especiais" + ) + + api = ClienteTransfereGov() + postgres_conn_str = get_postgres_conn('postgres_mir') + db = ClientPostgresDB(postgres_conn_str) + + # Busca todas as finalidades especiais com paginação automática + finalidades_data = api.get_all_finalidades_especiais(page_size=1000) + + if finalidades_data and len(finalidades_data) > 0: + # Adicionar dt_ingest a cada documento + for documento in finalidades_data: + documento["dt_ingest"] = datetime.now().isoformat() + + # Inserir/atualizar dados no banco + logging.info( + f"[finalidade_especial_ingest_dag.py] Inserindo " + f"{len(finalidades_data)} finalidades especiais no " + f"schema transfere_gov" + ) + db.insert_data( + finalidades_data, + "finalidades_especiais", + conflict_fields=[ + "id_executor", + "cd_area_politica_publica_tipo_pt", + "area_politica_publica_pt", + ], + primary_key=[ + "id_executor", + "cd_area_politica_publica_tipo_pt", + "area_politica_publica_pt", + ], + schema="transferegov_emendas", + ) + + logging.info( + f"[finalidade_especial_ingest_dag.py] Concluído. " + f"Total de {len(finalidades_data)} finalidades especiais " + f"inseridas/atualizadas" + ) + else: + logging.warning( + "[finalidade_especial_ingest_dag.py] Nenhuma finalidade especial " + "encontrada" + ) + + fetch_and_store_finalidade_especial() + + +api_finalidade_especial_dag() diff --git a/airflow_lappis/dags/data_ingest/transferegov_emendas/historico_pagamentos_especiais_ingest_dag.py b/airflow_lappis/dags/data_ingest/transferegov_emendas/historico_pagamentos_especiais_ingest_dag.py new file mode 100644 index 00000000..a81d696e --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transferegov_emendas/historico_pagamentos_especiais_ingest_dag.py @@ -0,0 +1,72 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_transferegov_emendas import ClienteTransfereGov +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("historico_pagamentos_especiais_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Tiago", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["transfere_gov_api", "historico_pagamentos_especiais", "MIR"], +) +def api_historico_pagamentos_especiais_dag() -> None: + """DAG para buscar e armazenar histórico de pagamentos especiais do Transfere Gov.""" + + @task + def fetch_and_store_historico_pagamentos_especiais() -> None: + logging.info( + "[historico_pagamentos_especiais_ingest_dag.py] Iniciando extração histórico " + "de pagamentos especiais" + ) + + api = ClienteTransfereGov() + postgres_conn_str = get_postgres_conn('postgres_mir') + db = ClientPostgresDB(postgres_conn_str) + + # Busca todos os documentos hábeis especiais com paginação automática + historico_data = api.get_all_historico_pagamentos_especiais(page_size=1000) + + if historico_data and len(historico_data) > 0: + # Adicionar dt_ingest a cada documento + for documento in historico_data: + documento["dt_ingest"] = datetime.now().isoformat() + + # Inserir/atualizar dados no banco + logging.info( + f"[historico_pagamentos_especiais_ingest_dag.py] Inserindo " + f"{len(historico_data)} registros de histórico de pagamentos especiais no " + f"schema transfere_gov" + ) + db.insert_data( + historico_data, + "historico_pagamentos_especiais", + conflict_fields=["id_historico_op_ob"], + primary_key=["id_historico_op_ob"], + schema="transferegov_emendas", + ) + + logging.info( + f"[historico_pagamentos_especiais_ingest_dag.py] Concluído. " + f"Total de {len(historico_data)} registros de histórico de pagamentos" + " especiais " + f"inseridos/atualizados" + ) + else: + logging.warning( + "[historico_pagamentos_especiais_ingest_dag.py] Nenhum registro de " + "histórico de pagamento especial encontrado" + ) + + fetch_and_store_historico_pagamentos_especiais() + + +api_historico_pagamentos_especiais_dag() diff --git a/airflow_lappis/dags/data_ingest/transferegov_emendas/metas_especiais_ingest_dag.py b/airflow_lappis/dags/data_ingest/transferegov_emendas/metas_especiais_ingest_dag.py new file mode 100644 index 00000000..07e84b08 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transferegov_emendas/metas_especiais_ingest_dag.py @@ -0,0 +1,61 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_transferegov_emendas import ClienteTransfereGov +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("metas_especiais_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Tiago", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["transfere_gov_api", "metas_especiais", "MIR"], +) +def api_metas_especiais_dag() -> None: + """DAG para buscar e armazenar metas especiais do Transfere Gov.""" + + @task + def fetch_and_store_metas_especiais() -> None: + logging.info( + "[metas_especiais_ingest_dag.py] Iniciando extração de metas especiais" + ) + + api = ClienteTransfereGov() + postgres_conn_str = get_postgres_conn('postgres_mir') + db = ClientPostgresDB(postgres_conn_str) + + metas_data = api.get_all_metas_especiais() + + if not metas_data: + logging.warning( + "[metas_especiais_ingest_dag.py] Nenhuma meta especial encontrada" + ) + return + + for meta in metas_data: + meta["dt_ingest"] = datetime.now().isoformat() + + db.insert_data( + metas_data, + "metas_especiais", + conflict_fields=["id_meta"], + primary_key=["id_meta"], + schema="transferegov_emendas", + ) + + logging.info( + f"[metas_especiais_ingest_dag.py] Concluído. " + f"Total: {len(metas_data)} metas especiais inseridas/atualizadas" + ) + + fetch_and_store_metas_especiais() + + +api_metas_especiais_dag() diff --git a/airflow_lappis/dags/data_ingest/transferegov_emendas/ordem_bancaria_especial_ingest_dag.py b/airflow_lappis/dags/data_ingest/transferegov_emendas/ordem_bancaria_especial_ingest_dag.py new file mode 100644 index 00000000..7bf0f884 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transferegov_emendas/ordem_bancaria_especial_ingest_dag.py @@ -0,0 +1,70 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_transferegov_emendas import ClienteTransfereGov +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("ordem_bancaria_especial_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Tiago", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["transfere_gov_api", "ordem_bancaria_especial", "MIR"], +) +def api_ordem_bancaria_especial_dag() -> None: + """DAG para buscar e armazenar ordens bancárias especiais do Transfere Gov.""" + + @task + def fetch_and_store_ordem_bancaria_especial() -> None: + logging.info( + "[ordem_bancaria_especial_ingest_dag.py] Iniciando extração ordens bancárias especiais" + ) + + api = ClienteTransfereGov() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + # Busca todas as ordens bancárias especiais com paginação automática + ordem_data = api.get_all_ordens_bancarias_especiais(page_size=1000) + + if ordem_data and len(ordem_data) > 0: + # Adicionar dt_ingest a cada documento + for documento in ordem_data: + documento["dt_ingest"] = datetime.now().isoformat() + + # Inserir/atualizar dados no banco + logging.info( + f"[ordem_bancaria_especial_ingest_dag.py] Inserindo " + f"{len(ordem_data)} ordens bancárias especiais no " + f"schema transfere_gov" + ) + db.insert_data( + ordem_data, + "ordens_bancarias_especiais", + conflict_fields=["id_op_ob"], + primary_key=["id_op_ob"], + schema="transferegov_emendas", + ) + + logging.info( + f"[ordem_bancaria_especial_ingest_dag.py] Concluído. " + f"Total de {len(ordem_data)} ordens bancárias especiais " + f"inseridas/atualizadas" + ) + else: + logging.warning( + "[ordem_bancaria_especial_ingest_dag.py] Nenhuma ordem bancária especial " + "encontrada" + ) + + fetch_and_store_ordem_bancaria_especial() + + +api_ordem_bancaria_especial_dag() diff --git a/airflow_lappis/dags/data_ingest/transferegov_emendas/plano_trabalho_especial_ingest_dag.py b/airflow_lappis/dags/data_ingest/transferegov_emendas/plano_trabalho_especial_ingest_dag.py new file mode 100644 index 00000000..559d084d --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transferegov_emendas/plano_trabalho_especial_ingest_dag.py @@ -0,0 +1,67 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_transferegov_emendas import ClienteTransfereGov +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("plano_trabalho_especial_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Mateus", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["transfere_gov_api", "plano_trabalho", "MIR"], +) +def api_plano_trabalho_especial_dag() -> None: + """DAG para buscar e armazenar planos de trabalho especiais do Transfere Gov.""" + + @task + def fetch_and_store_plano_trabalho_especial() -> None: + logging.info( + "[plano_trabalho_especial_ingest_dag.py] Iniciando extração de " + "planos de trabalho especiais" + ) + + api = ClienteTransfereGov() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + plano_data = api.get_all_plano_trabalho_especial(page_size=1000) + + if plano_data and len(plano_data) > 0: + for item in plano_data: + item["dt_ingest"] = datetime.now().isoformat() + + logging.info( + f"[plano_trabalho_especial_ingest_dag.py] Inserindo " + f"{len(plano_data)} planos de trabalho no schema transferegov_emendas" + ) + + db.insert_data( + plano_data, + "plano_trabalho_especial", + conflict_fields=["id_plano_trabalho"], + primary_key=["id_plano_trabalho"], + schema="transferegov_emendas", + ) + + logging.info( + f"[plano_trabalho_especial_ingest_dag.py] Concluído. " + f"Total de {len(plano_data)} registros processados." + ) + else: + logging.warning( + "[plano_trabalho_especial_ingest_dag.py] Nenhum plano de trabalho " + "encontrado" + ) + + fetch_and_store_plano_trabalho_especial() + + +api_plano_trabalho_especial_dag() diff --git a/airflow_lappis/dags/data_ingest/transferegov_emendas/planos_acao_especiais_ingest_dag.py b/airflow_lappis/dags/data_ingest/transferegov_emendas/planos_acao_especiais_ingest_dag.py new file mode 100644 index 00000000..d6675ec9 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transferegov_emendas/planos_acao_especiais_ingest_dag.py @@ -0,0 +1,77 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_transferegov_emendas import ClienteTransfereGov +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("planos_acao_especiais_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Davi e Mateus", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["transfere_gov_api", "planos_acao_especiais", "MIR"], +) +def api_planos_acao_especiais_dag() -> None: + """DAG para buscar e armazenar planos de ação especiais do Transfere Gov.""" + + @task + def fetch_and_store_planos_acao_especiais() -> None: + logging.info( + "[planos_acao_especiais_ingest_dag.py] Iniciando extração de " + "planos de ação especiais" + ) + + api = ClienteTransfereGov() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + # Buscar IDs dos programas especiais + query = ( + "SELECT DISTINCT id_programa FROM transferegov_emendas.programas_especiais" + ) + programas_ids = db.execute_query(query) + + if not programas_ids: + logging.warning( + "[planos_acao_especiais_ingest_dag.py] Nenhum programa encontrado" + ) + return + + total_planos = 0 + for (id_programa,) in programas_ids: + logging.info( + f"[planos_acao_especiais_ingest_dag.py] Buscando planos de ação " + f"para programa {id_programa}" + ) + + planos_data = api.get_all_planos_acao_especiais_by_programa(id_programa) + + if planos_data: + for plano in planos_data: + plano["dt_ingest"] = datetime.now().isoformat() + + db.insert_data( + planos_data, + "planos_acao_especiais", + conflict_fields=["id_plano_acao"], + primary_key=["id_plano_acao"], + schema="transferegov_emendas", + ) + total_planos += len(planos_data) + + logging.info( + f"[planos_acao_especiais_ingest_dag.py] Concluído. " + f"Total: {total_planos} planos de ação inseridos/atualizados" + ) + + fetch_and_store_planos_acao_especiais() + + +dag_instance = api_planos_acao_especiais_dag() diff --git a/airflow_lappis/dags/data_ingest/transferegov_emendas/programas_especiais_ingest_dag.py b/airflow_lappis/dags/data_ingest/transferegov_emendas/programas_especiais_ingest_dag.py new file mode 100644 index 00000000..5aa38d37 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transferegov_emendas/programas_especiais_ingest_dag.py @@ -0,0 +1,67 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from schedule_loader import get_dynamic_schedule +from postgres_helpers import get_postgres_conn +from cliente_transferegov_emendas import ClienteTransfereGov +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("programas_especiais_ingest_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Davi e Mateus", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["transfere_gov_api", "programas_especiais", "MIR"], +) +def api_programas_especiais_dag() -> None: + """DAG para buscar e armazenar programas especiais do Transfere Gov.""" + + @task + def fetch_and_store_programas_especiais() -> None: + logging.info( + "[programas_especiais_ingest_dag.py] Iniciando extração programas especiais" + ) + + api = ClienteTransfereGov() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + # Busca todos os programas especiais com paginação automática + programas_data = api.get_all_programas_especiais(page_size=1000) + + if programas_data and len(programas_data) > 0: + # Adicionar dt_ingest a cada programa + for programa in programas_data: + programa["dt_ingest"] = datetime.now().isoformat() + + # Inserir/atualizar dados no banco + logging.info( + f"[programas_especiais_ingest_dag.py] Inserindo {len(programas_data)} " + "programas especiais no schema transfere_gov" + ) + db.insert_data( + programas_data, + "programas_especiais", + conflict_fields=["id_programa"], + primary_key=["id_programa"], + schema="transferegov_emendas", + ) + + logging.info( + f"[programas_especiais_ingest_dag.py] Concluído. Total de " + f"{len(programas_data)} programas especiais inseridos/atualizados" + ) + else: + logging.warning( + "[programas_especiais_ingest_dag.py] Nenhum programa especial encontrado" + ) + + fetch_and_store_programas_especiais() + + +dag_instance = api_programas_especiais_dag() diff --git a/airflow_lappis/dags/data_ingest/transferegov_emendas/relatorio_gestao_novo_especial_ingest_dag.py b/airflow_lappis/dags/data_ingest/transferegov_emendas/relatorio_gestao_novo_especial_ingest_dag.py new file mode 100644 index 00000000..d026c908 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transferegov_emendas/relatorio_gestao_novo_especial_ingest_dag.py @@ -0,0 +1,71 @@ +import logging +from schedule_loader import get_dynamic_schedule +from airflow.decorators import dag, task +from datetime import datetime, timedelta +from postgres_helpers import get_postgres_conn +from cliente_transferegov_emendas import ClienteTransfereGov +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval=get_dynamic_schedule("relatorio_gestao_novo_especial_dag"), + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Tiago", + "retries": 1, + "retry_delay": timedelta(minutes=5), + }, + tags=["transfere_gov_api", "relatorio_gestao_novo_especial", "MIR"], +) +def api_relatorio_gestao_novo_especial_dag() -> None: + """DAG para buscar e armazenar relatórios de gestão novo especial do Transfere Gov""" + + @task + def fetch_and_store_relatorios_gestao_novo_especial() -> None: + logging.info( + "[relatorio_gestao_novo_especial_dag.py] Iniciando extração relatórios de" + " gestão novo especial" + ) + + api = ClienteTransfereGov() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + # Busca todos os relatórios de gestão novo especial com paginação automática + relatorios_data = api.get_all_relatorios_gestao_novo_especial(page_size=1000) + + if relatorios_data and len(relatorios_data) > 0: + # Adicionar dt_ingest a cada relatório + for relatorio in relatorios_data: + relatorio["dt_ingest"] = datetime.now().isoformat() + + # Inserir/atualizar dados no banco + logging.info( + f"[relatorio_gestao_novo_especial_dag.py] Inserindo " + f"{len(relatorios_data)} relatórios de gestão no " + f"schema transfere_gov" + ) + db.insert_data( + relatorios_data, + "relatorios_gestao_novo_especial", + conflict_fields=["id_relatorio_gestao_novo"], + primary_key=["id_relatorio_gestao_novo"], + schema="transferegov_emendas", + ) + + logging.info( + f"[relatorio_gestao_novo_especial_dag.py] Concluído. " + f"Total de {len(relatorios_data)} relatórios de gestão novo especial " + f"inseridos/atualizados" + ) + else: + logging.info( + "[relatorio_gestao_novo_especial_dag.py] Nenhum relatório de gestão novo " + "especial encontrado" + ) + + fetch_and_store_relatorios_gestao_novo_especial() + + +api_relatorio_gestao_novo_especial_dag() diff --git a/airflow_lappis/dags/data_ingest/transferegov_emendas/reltorio_gestao_ingest.py b/airflow_lappis/dags/data_ingest/transferegov_emendas/reltorio_gestao_ingest.py new file mode 100644 index 00000000..011fb206 --- /dev/null +++ b/airflow_lappis/dags/data_ingest/transferegov_emendas/reltorio_gestao_ingest.py @@ -0,0 +1,57 @@ +import logging +from airflow.decorators import dag, task +from datetime import datetime +from postgres_helpers import get_postgres_conn +from cliente_transferegov_emendas import ClienteTransfereGov +from cliente_postgres import ClientPostgresDB + + +@dag( + schedule_interval="@daily", + start_date=datetime(2023, 1, 1), + catchup=False, + default_args={ + "owner": "Mateus", + "retries": 1, + }, + tags=["transfere_gov_api", "relatorio_gestao_especial", "MIR"], +) +def api_relatorio_gestao_especial_dag() -> None: + + @task + def fetch_and_store_relatorios() -> None: + logging.info("[relatorio_gestao] Iniciando extração massiva global...") + + api = ClienteTransfereGov() + postgres_conn_str = get_postgres_conn("postgres_mir") + db = ClientPostgresDB(postgres_conn_str) + + relatorios_data = api.get_all_relatorio_gestao_especial(page_size=1000) + + if not relatorios_data: + logging.warning("[relatorio_gestao] Nenhum relatório retornado da API.") + return + + timestamp_atual = datetime.now().isoformat() + + for row in relatorios_data: + row["dt_ingest"] = timestamp_atual + + logging.info( + f"[relatorio_gestao] Inserindo {len(relatorios_data)} registros no Postgres." + ) + + db.insert_data( + relatorios_data, + table_name="relatorio_gestao_especial", + conflict_fields=["id_relatorio_gestao"], + primary_key=["id_relatorio_gestao"], + schema="transferegov_emendas", + ) + + logging.info("[relatorio_gestao] Ingestão concluída com sucesso.") + + fetch_and_store_relatorios() + + +api_relatorio_gestao_especial_dag() diff --git a/airflow_lappis/dags/dbt/.user.yml b/airflow_lappis/dags/dbt/.user.yml new file mode 100755 index 00000000..5af1e34e --- /dev/null +++ b/airflow_lappis/dags/dbt/.user.yml @@ -0,0 +1 @@ +id: aecd48ae-3ec9-4354-b785-bd2c463a92a7 diff --git a/airflow_lappis/dags/dbt/ipea/.user.yml b/airflow_lappis/dags/dbt/ipea/.user.yml new file mode 100755 index 00000000..43198208 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/.user.yml @@ -0,0 +1 @@ +id: d025f823-c5b2-49c6-b826-226fb25f1ad8 diff --git a/airflow_lappis/dags/dbt/ipea/cosmos_dag.py b/airflow_lappis/dags/dbt/ipea/cosmos_dag.py new file mode 100755 index 00000000..2e30ded1 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/cosmos_dag.py @@ -0,0 +1,30 @@ +import os +from datetime import datetime +from cosmos import DbtDag, ProjectConfig, ProfileConfig, ExecutionConfig +from cosmos.constants import DBT_LOG_PATH_ENVVAR + +dbt_log_path = "/tmp/dbt_logs" +os.makedirs(dbt_log_path, exist_ok=True) +os.environ[DBT_LOG_PATH_ENVVAR] = dbt_log_path + +profile_config = ProfileConfig( + profiles_yml_filepath=f"{os.environ['AIRFLOW_REPO_BASE']}/dags/dbt/ipea/profiles.yml", + profile_name="ipea", + target_name="prod", +) + +my_cosmos_dag = DbtDag( + project_config=ProjectConfig(f"{os.environ['AIRFLOW_REPO_BASE']}/dags/dbt/ipea"), + profile_config=profile_config, + execution_config=ExecutionConfig( + dbt_executable_path=f"{os.environ['AIRFLOW_REPO_BASE']}/.local/bin/dbt", + ), + # Expressãp cron para agendar a execução do DAG diariamente às 01:00 + # Futuralmente isso pode ser substituído por um cronograma mais específico + # com dependências entre os DAGs + schedule_interval=" 0 1 * * *", + start_date=datetime(2025, 1, 1), + catchup=False, + dag_id="ipea_cosmos_dag", + default_args={"retries": 2}, +) diff --git a/airflow_lappis/dags/dbt/ipea/dbt_project.yml b/airflow_lappis/dags/dbt/ipea/dbt_project.yml new file mode 100755 index 00000000..e96aac73 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/dbt_project.yml @@ -0,0 +1,61 @@ +name: 'ipea' + +version: 1.0.0 +config-version: 2 + +profile: ipea + +model-paths: ["models"] +analysis-paths: ["analyses"] +test-paths: ["tests"] +seed-paths: ["seeds"] +macro-paths: ["macros"] +snapshot-paths: ["snapshots"] + +clean-targets: + - "target" + - "dbt_packages" + - "logs" + +models: + ipea: + +database: analytics + metadata: + +materialized: incremental + +schema: metadata + contratos_dbt: + +materialized: table + +schema: contratos + bronze: + +materialized: incremental + views: + +materialized: view + pessoas_dbt: + +materialized: table + +schema: pessoas + views: + +materialized: view + ted_dbt: + +materialized: table + +schema: ted + views: + +materialized: view + orcamento_dbt: + +materialized: table + +schema: orcamento + views: + +materialized: view + sistema_sisbolsas: + +materialized: table + +schema: sistema_sisbolsas + bronze: + +materialized: table + # siape_dbt: + # +materialized: table + # +schema: siape + # views: + # +materialized: view + + +on-run-start: + - '{{create_udfs()}}' \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/ipea/descriptions.yml b/airflow_lappis/dags/dbt/ipea/descriptions.yml new file mode 100644 index 00000000..b1054f6d --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/descriptions.yml @@ -0,0 +1,61 @@ +version: 2 + +models: + contratos_dbt: + bronze: + - name: contratos + description: > + Tabela com informações sobre contratos, incluindo detalhes como o valor do contrato, a data de início e término, e o status do contrato. + Esta tabela é fundamental para entender a execução e o cumprimento dos contratos firmados. + A tabela é atualizada diariamente e contém dados de contratos firmados pelo IPEA. + + - name: cronogramas + description: > + Essa tabela contém informações sobre as despesas mensais programadas cada contrato. + + - name: faturas + description: > + Essa tabela contém informações sobre as faturas emitidas mensalmente de cada contrato. + + - name: empenhos + description: > + Essa tabela contém informações sobre os empenhos de cada contrato. + + - name: empenhos_tesouro + description: > + Essa tabela contém informações sobre os empenhos extraídos do SIAFI. + Essa tabela é atualizada diariamente. + + - name: estagios + description: > + Essa tabela contém informações sobre os evento de cada empenho discriminados por mês. + + silver: + - name: contratos_empenhos + description: > + Essa tabela contém informações sobre os empenhos de cada contrato, incluindo detalhes como o valor do empenho, a data de emissão e o status do empenho. + Esta tabela é fundamental para entender a execução + + pessoas_dbt: + + + ted_dbt: + - name: pf_tesouro + description: > + Dados das programações financeiras extraídas do SIAFI, com informações sobre o tipo de programação, a data de execução e o valor. + Essa tabela é atualizada diariamente. + + - name: empenhos_plano_acao + description: > + Dados dos empenhos extraídos do SIAFI, incluído o identificador do plano de ação. + Essa tabela é atualizada diariamente. + + - name: nc_plano_acao + description: > + Dados das notas de crédito extraídas do SIAFI, incluso o identificador do plano de ação. + Essa tabela é atualizada diariamente. + + - name: pf_plano_acao + description: > + Dados das programações financeiras extraídas do SIAFI, includo o identificador do plano de ação. + Essa tabela é atualizada diariamente. \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/ipea/macros/create_udfs.sql b/airflow_lappis/dags/dbt/ipea/macros/create_udfs.sql new file mode 100644 index 00000000..dd230cf5 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/macros/create_udfs.sql @@ -0,0 +1,10 @@ +{% macro create_udfs() %} + +create schema if not exists {{ target.schema }}; + + {{ create_f_parse_dates() }} + ; + {{ create_f_format_nc() }} + ; + +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/ipea/macros/data_quality/row_count_match.sql b/airflow_lappis/dags/dbt/ipea/macros/data_quality/row_count_match.sql new file mode 100644 index 00000000..f248e30c --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/macros/data_quality/row_count_match.sql @@ -0,0 +1,14 @@ +{% macro test_row_count_match(model, source_table, target_table) %} + with + source_count as (select count(*) as row_count from {{ source_table }}), + target_count as (select count(*) as row_count from {{ target_table }}), + comparison as ( + select + source_count.row_count as source_row_count, + target_count.row_count as target_row_count + from source_count, target_count + ) + select * + from comparison + where source_row_count != target_row_count +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/ipea/macros/data_quality/verificacao_tipagem.sql b/airflow_lappis/dags/dbt/ipea/macros/data_quality/verificacao_tipagem.sql new file mode 100644 index 00000000..34c3d392 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/macros/data_quality/verificacao_tipagem.sql @@ -0,0 +1,25 @@ +{% macro test_verificacao_tipagem(model, nome_tabela, nome_coluna, tipo_esperado) %} + with + column_info as ( + select + table_schema, + table_name, -- Nome real da coluna no information_schema + column_name, -- Nome real da coluna no information_schema + data_type + from information_schema.columns + where + table_schema || '.' || table_name = '{{ nome_tabela }}' + and column_name = '{{ nome_coluna }}' + ), + comparison as ( + select + '{{ nome_tabela }}' as nome_tabela, + '{{ nome_coluna }}' as nome_coluna, + '{{ tipo_esperado }}' as tipo_esperado, + data_type as actual_type + from column_info + ) + select * + from comparison + where actual_type != tipo_esperado +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/ipea/macros/get_custom_schema.sql b/airflow_lappis/dags/dbt/ipea/macros/get_custom_schema.sql new file mode 100755 index 00000000..79444e9a --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/macros/get_custom_schema.sql @@ -0,0 +1,4 @@ +-- built-in schema generator +{% macro generate_schema_name(custom_schema_name, node) -%} + {{ generate_schema_name_for_env(custom_schema_name, node) }} +{%- endmacro %} diff --git a/airflow_lappis/dags/dbt/ipea/macros/metadata/generate_metadata.sql b/airflow_lappis/dags/dbt/ipea/macros/metadata/generate_metadata.sql new file mode 100644 index 00000000..8bfb115b --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/macros/metadata/generate_metadata.sql @@ -0,0 +1,46 @@ +{% macro get_model_metadata() %} +{# + Esta macro retorna os metadados do modelo atual. + Pode ser usada em post-hooks para registrar metadados automaticamente. +#} + SELECT + '{{ this.schema }}' AS schema_name, + '{{ this.name }}' AS table_name, + '{{ this.database }}' AS database_name, + ('{{ run_started_at }}'::TIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE 'America/Sao_Paulo') AS dt_transform, + '{{ invocation_id }}' AS run_id +{% endmacro %} + + +{% macro register_model_metadata() %} +{# + Esta macro registra os metadados do modelo em uma tabela central. + Deve ser usada como post-hook nos modelos que deseja rastrear. + + Uso no dbt_project.yml: + models: + ipea: + +post-hook: + - "{{ register_model_metadata() }}" +#} + + INSERT INTO {{ target.database }}.metadata.models_metadata ( + schema_name, + table_name, + database_name, + dt_transform, + run_id + ) + VALUES ( + '{{ this.schema }}', + '{{ this.name }}', + '{{ this.database }}', + ('{{ run_started_at }}'::TIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE 'America/Sao_Paulo'), + '{{ invocation_id }}' + ) + ON CONFLICT (schema_name, table_name) + DO UPDATE SET + dt_transform = EXCLUDED.dt_transform, + run_id = EXCLUDED.run_id; + +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/ipea/macros/parse_financial_value.sql b/airflow_lappis/dags/dbt/ipea/macros/parse_financial_value.sql new file mode 100644 index 00000000..437b673c --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/macros/parse_financial_value.sql @@ -0,0 +1,21 @@ +{% macro parse_financial_value(column_name) %} + + case + when {{ column_name }} is null or trim({{ column_name }}) = '' + then 0.00::numeric(15, 2) + when {{ column_name }} like '%NaN%' + then 0.00::numeric(15, 2) + when {{ column_name }} like '(%' + then + regexp_replace( + replace(coalesce({{ column_name }}, '0'), '.', ''), + '(\()?(\d+),(\d+)(\))?', + '-\2.\3' + )::numeric(15, 2) + else + replace( + replace(coalesce({{ column_name }}, '0'), '.', ''), ',', '.' + )::numeric(15, 2) + end + +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/ipea/macros/safe_casts.sql b/airflow_lappis/dags/dbt/ipea/macros/safe_casts.sql new file mode 100644 index 00000000..3c021e84 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/macros/safe_casts.sql @@ -0,0 +1,59 @@ +{% macro safe_text(column_name) -%} + case + when {{ column_name }} is null then null + when nullif(trim({{ column_name }}::text), '') is null then null + when upper(trim({{ column_name }}::text)) = 'NAN' then null + else trim({{ column_name }}::text) + end +{%- endmacro %} + +{% macro safe_bigint(column_name) -%} + case + when {{ column_name }} is null then null + when nullif(trim({{ column_name }}::text), '') is null then null + when upper(trim({{ column_name }}::text)) = 'NAN' then null + when trim({{ column_name }}::text) ~ '^[+-]?[0-9]+$' then trim({{ column_name }}::text)::bigint + else null + end +{%- endmacro %} + +{% macro safe_numeric(column_name, precision=18, scale=2) -%} + case + when {{ column_name }} is null then null + when nullif(trim({{ column_name }}::text), '') is null then null + when upper(trim({{ column_name }}::text)) = 'NAN' then null + when trim({{ column_name }}::text) ~ '^[+-]?([0-9]+([.][0-9]+)?|[.][0-9]+)$' + then trim({{ column_name }}::text)::numeric({{ precision }}, {{ scale }}) + else null + end +{%- endmacro %} + +{% macro safe_boolean(column_name) -%} + case + when {{ column_name }} is null then null + when nullif(trim({{ column_name }}::text), '') is null then null + when lower(trim({{ column_name }}::text)) in ('true', 't', '1', 'sim', 's') then true + when lower(trim({{ column_name }}::text)) in ('false', 'f', '0', 'nao', 'não', 'n') then false + else null + end +{%- endmacro %} + +{% macro safe_date(column_name) -%} + case + when {{ column_name }} is null then null + when trim({{ column_name }}::text) in ('', '0001-01-01', '1900-01-01') then null + when upper(trim({{ column_name }}::text)) = 'NAN' then null + when trim({{ column_name }}::text) ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' + then trim({{ column_name }}::text)::date + else null + end +{%- endmacro %} + +{% macro safe_timestamp(column_name) -%} + case + when {{ column_name }} is null then null + when nullif(trim({{ column_name }}::text), '') is null then null + when upper(trim({{ column_name }}::text)) = 'NAN' then null + else {{ column_name }}::timestamp + end +{%- endmacro %} diff --git a/airflow_lappis/dags/dbt/ipea/macros/schema.yml b/airflow_lappis/dags/dbt/ipea/macros/schema.yml new file mode 100644 index 00000000..694f3b23 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/macros/schema.yml @@ -0,0 +1,24 @@ + +version: 2 + +macros: + - name: create_udfs + description: > + Função que cria as UDFs necessárias para o funcionamento do projeto. + Essa função deve ser chamada no início de cada run para garantir que todas as UDFs estejam disponíveis. + + - name: generate_schema_name + description: > + Função que gera o nome do schema a ser utilizado no projeto. + A função dentro desta macro é built-in do dbt. + + ## UDFS + - name: create_f_parse_dates + description: > + Função que cria a UDF f_parse_dates, que é utilizada para converter texto no formato MÊS(texto)/ANO(numero) em datas. + arguments: + - name: in_text + type: text + description: > + Texto a ser convertido em data. + O texto deve estar no formato MÊS(texto)/ANO(numero). Ex.: FEV/2024 -> 2024-02-01 \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/ipea/macros/sharepoint.sql b/airflow_lappis/dags/dbt/ipea/macros/sharepoint.sql new file mode 100644 index 00000000..87e424d4 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/macros/sharepoint.sql @@ -0,0 +1,57 @@ +{% macro clean_sharepoint_html(column_name) -%} + nullif( + trim( + regexp_replace( + regexp_replace( + replace( + replace({{ safe_text(column_name) }}, ' ', ' '), + ' ', + ' ' + ), + '<[^>]*>', + ' ', + 'g' + ), + '\s+', + ' ', + 'g' + ) + ), + '' + ) +{%- endmacro %} + +{% macro extract_jsonb_key_values( + column_name, key_name, fallback_to_text=true, separator='; ' +) -%} + case + when {{ safe_text(column_name) }} like ('[' || '%') + then ( + select + string_agg( + element ->> '{{ key_name }}', + '{{ separator }}' order by ordinality + ) + from + jsonb_array_elements(({{ safe_text(column_name) }})::jsonb) + with ordinality as elements(element, ordinality) + where nullif(element ->> '{{ key_name }}', '') is not null + ) + when {{ safe_text(column_name) }} like ('{' || '%') + then ({{ safe_text(column_name) }})::jsonb ->> '{{ key_name }}' + {% if fallback_to_text %} + else {{ safe_text(column_name) }} + {% else %} + else null + {% endif %} + end +{%- endmacro %} + +{% macro sharepoint_jsonb(column_name) -%} + case + when {{ safe_text(column_name) }} like ('[' || '%') + or {{ safe_text(column_name) }} like ('{' || '%') + then ({{ safe_text(column_name) }})::jsonb + else null + end +{%- endmacro %} diff --git a/airflow_lappis/dags/dbt/ipea/macros/udfs/f_format_nc.sql b/airflow_lappis/dags/dbt/ipea/macros/udfs/f_format_nc.sql new file mode 100644 index 00000000..f7a06c86 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/macros/udfs/f_format_nc.sql @@ -0,0 +1,19 @@ +{% macro create_f_format_nc() %} + create or replace function {{ target.schema }}.format_nc(in_text text) + returns text + as $$ + + with + + pre_process as ( + select left(in_text, 7) as prefix, + right(in_text, 4)::numeric as posfix + ) + + select concat(prefix, to_char(posfix, 'FM00000')) as result + from pre_process + + $$ + language sql + ; +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/ipea/macros/udfs/f_parse_dates.sql b/airflow_lappis/dags/dbt/ipea/macros/udfs/f_parse_dates.sql new file mode 100644 index 00000000..3fd8693e --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/macros/udfs/f_parse_dates.sql @@ -0,0 +1,44 @@ +-- Essa fun +{% macro create_f_parse_dates() %} + + create or replace function {{ target.schema }}.parse_date(in_text text) + returns date + as + $$ + + with + + split_column as ( + select + split_part(in_text, '/', 1) as mes, + split_part(in_text, '/', 2) as ano + ), + + fixed_month as ( + select + ano, + case + when mes = 'JAN' then '01' + when mes = 'FEV' then '02' + when mes = 'MAR' then '03' + when mes = 'ABR' then '04' + when mes = 'MAI' then '05' + when mes = 'JUN' then '06' + when mes = 'JUL' then '07' + when mes = 'AGO' then '08' + when mes = 'SET' then '09' + when mes = 'OUT' then '10' + when mes = 'NOV' then '11' + when mes = 'DEZ' then '12' + else mes end as mes_num + from split_column + ) + + select + (to_date(ano::numeric - 1 || '-' || '12', 'YYYY-MM') + (mes_num || ' months')::interval)::date as result + from fixed_month + $$ + language sql + ; + +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/contratos.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/contratos.sql new file mode 100755 index 00000000..8d38922d --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/contratos.sql @@ -0,0 +1,123 @@ +{{ config(materialized="table") }} + +with + contratos_raw as ( + select + -- Conversão de tipos e formatação de colunas + cast(id as text) as id, + receita_despesa, + numero, + contratante__orgao_origem__codigo, + contratante__orgao_origem__nome, + contratante__orgao_origem__unidade_gestora_origem__codigo, + contratante__orgao_origem__unidade_gestora_origem__nome_resumido, + contratante__orgao_origem__unidade_gestora_origem__nome, + contratante__orgao_origem__unidade_gestora_origem__sisg, + contratante__orgao_origem__unidade_gestora_origem__utiliza_siafi, + contratante__orgao_origem__unidade_gestora_origem__utiliza_antecip, + contratante__orgao__codigo, + contratante__orgao__nome, + contratante__orgao__unidade_gestora__codigo, + contratante__orgao__unidade_gestora__nome_resumido, + contratante__orgao__unidade_gestora__nome, + contratante__orgao__unidade_gestora__sisg, + contratante__orgao__unidade_gestora__utiliza_siafi, + contratante__orgao__unidade_gestora__utiliza_antecipagov, + fornecedor__tipo as fornecedor_tipo, + fornecedor__nome as fornecedor_nome, + codigo_tipo as codigo_tipo, + tipo, + subtipo, + prorrogavel, + situacao, + justificativa_inativo, + categoria, + subcategoria, + unidades_requisitantes, + objeto, + amparo_legal, + informacao_complementar, + codigo_modalidade, + modalidade, + unidade_compra as unidade_compra, + licitacao_numero, + sistema_origem_licitacao, + cast(num_parcelas as int) as num_parcelas, + cast( + replace( + replace(cast(valor_inicial as text), '.', ''), ',', '.' + ) as numeric(15, 2) + ) as valor_inicial, + -- Tratar valores nulos ou inválidos nas colunas de data + cast( + replace( + replace(cast(valor_global as text), '.', ''), ',', '.' + ) as numeric(15, 2) + ) as valor_global, + cast( + replace( + replace(cast(valor_parcela as text), '.', ''), ',', '.' + ) as numeric(15, 2) + ) as valor_parcela, + cast( + replace( + replace(cast(valor_acumulado as text), '.', ''), ',', '.' + ) as numeric(15, 2) + ) as valor_acumulado, + regexp_replace( + fornecedor__cnpj_cpf_idgener, '[^0-9A-Za-z]', '', 'g' + ) as fornecedor_cnpj_cpf_idgener, + regexp_replace(processo, '[^0-9A-Za-z]', '', 'g') as processo, + -- Conversão de valores numéricos para FLOAT ou INT + case + when data_assinatura is null + then null + when + data_assinatura is not null + and cast(data_assinatura as text) ~ '^\d{4}-\d{2}-\d{2}$' + -- Retorna NULL se não for uma data válida + then to_date(cast(data_assinatura as text), 'YYYY-MM-DD') + end as data_assinatura, + case + when data_publicacao is null + then null + when + data_publicacao is not null + and cast(data_publicacao as text) ~ '^\d{4}-\d{2}-\d{2}$' + -- Retorna NULL se não for uma data válida + then to_date(cast(data_publicacao as text), 'YYYY-MM-DD') + end as data_publicacao, + case + when data_proposta_comercial is null + then null + when + data_proposta_comercial is not null + and cast(data_proposta_comercial as text) ~ '^\d{4}-\d{2}-\d{2}$' + then + -- Retorna NULL se não for uma data válida + to_date(cast(data_proposta_comercial as text), 'YYYY-MM-DD') + end as data_proposta_comercial, + case + when vigencia_inicio is null + then null + when + vigencia_inicio is not null + and cast(vigencia_inicio as text) ~ '^\d{4}-\d{2}-\d{2}$' + -- Retorna NULL se não for uma data válida + then to_date(cast(vigencia_inicio as text), 'YYYY-MM-DD') + end as vigencia_inicio, + case + when vigencia_fim is null + then null + when + vigencia_fim is not null + and cast(vigencia_fim as text) ~ '^\d{4}-\d{2}-\d{2}$' + -- Retorna NULL se não for uma data válida + then to_date(cast(vigencia_fim as text), 'YYYY-MM-DD') + end as vigencia_fim, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("compras_gov", "contratos") }} + ) -- + +select * +from contratos_raw diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/cronogramas.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/cronogramas.sql new file mode 100644 index 00000000..ba0da19f --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/cronogramas.sql @@ -0,0 +1,24 @@ +{{ config(materialized="table") }} + +with + cronogramas as ( + select + id::integer as id, + contrato_id::integer as contrato_id, + tipo::text as tipo, + numero::text as numero, + receita_despesa::text as receita_despesa, + observacao::text as observacao, + mesref::integer as mesref, + anoref::integer as anoref, + retroativo::text as retroativo, + replace(replace(valor::text, '.', ''), ',', '.')::numeric(15, 2) as valor, + vencimento::date as vencimento, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("compras_gov", "cronograma") }} + ), + + distinct_cronogramas as (select distinct * from cronogramas) + +select * +from distinct_cronogramas diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/empenhos.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/empenhos.sql new file mode 100755 index 00000000..1bb6ee31 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/empenhos.sql @@ -0,0 +1,58 @@ +{{ config(materialized="table") }} + +with + empenhos as ( + select + id::text as id, + contrato_id::text as contrato_id, + unidade_gestora, + gestao, + numero as nota_empenho, + credor, + fonte_recurso, + programa_trabalho, + planointerno, + naturezadespesa, + informacao_complementar, + sistema_origem, + links__documento_pagamento as links_documento_pagamento, + credor_obj__tipo as credor_obj_tipo, + credor_obj__cnpj_cpf_idgener as credor_obj_cnpj_cpf_idgener, + credor_obj__nome as credor_obj_nome, + + -- Tratar valores nulos ou inválidos nas colunas de data + replace(replace(empenhado::text, '.', ''), ',', '.')::numeric( + 15, 2 + ) as empenhado, + + -- Conversão de valores numéricos para FLOAT + replace(replace(aliquidar::text, '.', ''), ',', '.')::numeric( + 15, 2 + ) as aliquidar, + replace(replace(liquidado::text, '.', ''), ',', '.')::numeric( + 15, 2 + ) as liquidado, + replace(replace(pago::text, '.', ''), ',', '.')::numeric(15, 2) as pago, + replace(replace(rpinscrito::text, '.', ''), ',', '.')::numeric( + 15, 2 + ) as rpinscrito, + replace(replace(rpaliquidar::text, '.', ''), ',', '.')::numeric( + 15, 2 + ) as rpaliquidar, + replace(replace(rpliquidado::text, '.', ''), ',', '.')::numeric( + 15, 2 + ) as rpliquidado, + replace(replace(rppago::text, '.', ''), ',', '.')::numeric(15, 2) as rppago, + case + when + data_emissao is not null + and data_emissao::text ~ '^\d{4}-\d{2}-\d{2}$' + -- Retorna NULL se não for uma data válida + then to_date(data_emissao::text, 'YYYY-MM-DD') + end as data_emissao, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("compras_gov", "empenhos") }} + ) + +select * +from empenhos diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/empenhos_tesouro.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/empenhos_tesouro.sql new file mode 100755 index 00000000..f5efa88f --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/empenhos_tesouro.sql @@ -0,0 +1,49 @@ +{{ + config( + unique_key=[ + "ne_ccor", + "natureza_despesa", + "doc_observacao", + "ne_ccor_ano_emissao", + "emissao_dia", + "emissao_mes", + "despesas_empenhadas", + "despesas_liquidadas", + "despesas_pagas", + ], + incremental_strategy="merge", + ) +}} + +with + empenhos_raw as ( + select + emissao_mes::text as emissao_mes, + emissao_dia::text as emissao_dia, + ne_ccor::text as ne_ccor, + regexp_replace(ne_num_processo, '[./-]', '') as ne_num_processo, + ne_info_complementar::text as ne_info_complementar, + ne_ccor_descricao::text as ne_ccor_descricao, + doc_observacao::text as doc_observacao, + natureza_despesa::text as natureza_despesa, + natureza_despesa_descricao::text as natureza_despesa_descricao, + upper(ne_ccor_favorecido::text) as ne_ccor_favorecido, + ne_ccor_favorecido_descricao::text as ne_ccor_favorecido_descricao, + ne_ccor_ano_emissao::integer as ne_ccor_ano_emissao, + ptres::text as ptres, + fonte_recursos_detalhada::text as fonte_recursos_detalhada, + fonte_recursos_detalhada_descricao::text + as fonte_recursos_detalhada_descricao, + {{ parse_financial_value("despesas_empenhadas") }} as despesas_empenhadas, + {{ parse_financial_value("despesas_liquidadas") }} as despesas_liquidadas, + {{ parse_financial_value("despesas_pagas") }} as despesas_pagas, + {{ parse_financial_value("restos_a_pagar_inscritos") }} + as restos_a_pagar_inscritos, + {{ parse_financial_value("restos_a_pagar_pagos") }} as restos_a_pagar_pagos, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("siafi", "empenhos_tesouro") }} + where ne_ccor_ano_emissao like '20%' + ) + +select * +from empenhos_raw diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/faturas.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/faturas.sql new file mode 100755 index 00000000..0a8b5f88 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/faturas.sql @@ -0,0 +1,51 @@ +{{ config(materialized="table") }} + +with + faturas_raw as ( + select + id::integer as id, + contrato_id::integer as contrato_id, + tipolistafatura_id::text as tipolistafatura_id, + justificativafatura_id::text as justificativafatura_id, + sfadrao_id::text as sfadrao_id, + numero::text as numero, + emissao::date as emissao, + prazo::date as prazo, + vencimento::date as vencimento, + -- Limpar o formato numérico das colunas que têm problemas + replace(replace(valor::text, '.', ''), ',', '.')::numeric(15, 2) as valor, + replace(replace(juros::text, '.', ''), ',', '.')::numeric(15, 2) as juros, + replace(replace(multa::text, '.', ''), ',', '.')::numeric(15, 2) as multa, + replace(replace(glosa::text, '.', ''), ',', '.')::numeric(15, 2) as glosa, + replace(replace(valorliquido::text, '.', ''), ',', '.')::numeric( + 15, 2 + ) as valorliquido, + processo::text as processo, + protocolo::date as protocolo, + ateste::date as ateste, + repactuacao::text as repactuacao, + infcomplementar::text as infcomplementar, + mesref::integer as mesref, + anoref::integer as anoref, + situacao::text as situacao, + chave_nfe::text as chave_nfe, + dados_referencia::text as dados_referencia, + dados_item_faturado::text as dados_item_faturado, + jsonb_array_elements( + replace(dados_empenho, '''', '"')::jsonb + ) as dados_empenho, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("compras_gov", "faturas") }} + ), + -- Extrai os campos do JSON e transforma em colunas individuais + faturas_dados_empenho as ( + select + f.*, + f.dados_empenho ->> 'id_empenho' as id_empenho, + upper(f.dados_empenho ->> 'numero_empenho') as numero_empenho, + f.dados_empenho ->> 'valor_empenho' as valor_empenho, + f.dados_empenho ->> 'subelemento' as subelemento + from faturas_raw as f + ) -- +select * +from faturas_dados_empenho diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/schema.yml b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/schema.yml new file mode 100644 index 00000000..1ef6173e --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/bronze/schema.yml @@ -0,0 +1,480 @@ +version: 2 + +models: + + # Contratos DBT + + ## Bronze + - name: contratos + description: > + Tabela com informações sobre contratos, incluindo detalhes como o valor do contrato, a data de início e término, e o status do contrato. + Esta tabela é fundamental para entender a execução e o cumprimento dos contratos firmados. + A tabela é atualizada diariamente e contém dados de contratos firmados pelo IPEA. + A tabela realiza validações e limpezas dos dados extraídos do ComprasGov, incluindo formatação adequada de valores numéricos, + remoção de caracteres especiais em CPF/CNPJ e normalização de datas. + meta: + tags: + - bronze + columns: + - name: id + description: > + Identificador único do contrato, utilizado para referenciar o contrato em outras tabelas e análises. + - name: receita_despesa + description: > + Indica se o contrato é de receita ou despesa. + - name: numero + description: > + Número do contrato no sistema ComprasGov. + - name: fornecedor_tipo + description: > + Tipo do fornecedor (PF, PJ, IDGENERICO para empresas do exterior). + - name: fornecedor_nome + description: > + Nome do fornecedor contratado. + - name: tipo + description: > + Tipo de contrato ( Contrato, Empenho, Termo de Compromisso, etc.). + - name: situacao + description: > + Situação atual do contrato ( Ativo, Inativo). + - name: categoria + description: > + Categoria do contrato ( Informática, Serviços, Mão de Obra, Serviços de Engenharia, etc.). + - name: objeto + description: > + Descrição do objeto contratado. + - name: codigo_modalidade + description: > + Código que identifica a modalidade do contrato. + - name: modalidade + description: > + Modalidade do contrato ( Pregão, Dispensa, Inexigibilidade, etc.). + - name: num_parcelas + description: > + Número de parcelas para pagamento do contrato. + - name: valor_inicial + description: > + Valor inicial do contrato, formatado como numérico. + - name: valor_global + description: > + Valor total do contrato após eventuais aditivos, formatado como numérico. + - name: valor_parcela + description: > + Valor de cada parcela do contrato, formatado como numérico. + - name: valor_acumulado + description: > + Valor acumulado já realizado do contrato, formatado como numérico. + - name: fornecedor_cnpj_cpf_idgener + description: > + CNPJ, CPF ou identificador genérico do fornecedor, com formatação padronizada para remoção de caracteres especiais. + - name: processo + description: > + Número do processo administrativo relacionado ao contrato, formatado para remover caracteres especiais. + - name: data_assinatura + description: > + Data em que o contrato foi assinado, validada e convertida para formato de data padrão. + - name: data_publicacao + description: > + Data de publicação do contrato no Diário Oficial, validada e convertida para formato de data padrão. + - name: vigencia_inicio + description: > + Data de início da vigência do contrato, validada e convertida para formato de data padrão. + - name: vigencia_fim + description: > + Data de término da vigência do contrato, validada e convertida para formato de data padrão. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + data_tests: + - row_count_match: + source_table: compras_gov.contratos + target_table: contratos.contratos + - verificacao_tipagem: + nome_tabela: 'contratos.contratos' + nome_coluna: 'num_parcelas' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'contratos.contratos' + nome_coluna: 'valor_inicial' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.contratos' + nome_coluna: 'valor_global' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.contratos' + nome_coluna: 'valor_parcela' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.contratos' + nome_coluna: 'valor_acumulado' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.contratos' + nome_coluna: 'data_assinatura' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'contratos.contratos' + nome_coluna: 'data_publicacao' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'contratos.contratos' + nome_coluna: 'data_proposta_comercial' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'contratos.contratos' + nome_coluna: 'vigencia_inicio' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'contratos.contratos' + nome_coluna: 'vigencia_fim' + tipo_esperado: 'date' + + - name: cronogramas + description: > + Essa tabela contém informações sobre as despesas mensais programadas para cada contrato. + Os dados são extraídos da tabela cronograma do ComprasGov, com valores formatados adequadamente. + Apresenta o planejamento financeiro do contrato distribuído em parcelas mensais. + meta: + tags: + - bronze + columns: + - name: id + description: > + Identificador único do cronograma. + - name: contrato_id + description: > + Identificador do contrato relacionado ao cronograma. + - name: tipo + description: > + Tipo de cada item do cronograma. + - name: mesref + description: > + Mês de referência do cronograma. + - name: anoref + description: > + Ano de referência do cronograma. + - name: valor + description: > + Valor programado para o mês e ano de referência, formatado como numérico. + - name: vencimento + description: > + Data de vencimento da parcela do cronograma. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + data_tests: + - verificacao_tipagem: + nome_tabela: 'contratos.cronogramas' + nome_coluna: 'id' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'contratos.cronogramas' + nome_coluna: 'contrato_id' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'contratos.cronogramas' + nome_coluna: 'mesref' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'contratos.cronogramas' + nome_coluna: 'anoref' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'contratos.cronogramas' + nome_coluna: 'valor' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.cronogramas' + nome_coluna: 'vencimento' + tipo_esperado: 'date' + + - name: faturas + description: > + Essa tabela contém informações sobre as faturas emitidas mensalmente de cada contrato. + Os dados são extraídos da tabela faturas do ComprasGov com formatação adequada de valores numéricos e datas. + A tabela inclui um processamento adicional para extrair informações dos empenhos a partir do campo JSON dados_empenho. + meta: + tags: + - bronze + columns: + - name: id + description: > + Identificador único da fatura. + - name: contrato_id + description: > + Identificador do contrato relacionado à fatura. + - name: numero + description: > + Número da fatura emitida pelo fornecedor. + - name: emissao + description: > + Data de emissão da fatura pelo fornecedor. + - name: prazo + description: > + Data limite para pagamento da fatura. + - name: vencimento + description: > + Data de vencimento da fatura. + - name: valor + description: > + Valor bruto da fatura, formatado como numérico. + - name: juros + description: > + Valor de juros aplicados à fatura, formatado como numérico. + - name: multa + description: > + Valor de multa aplicada à fatura, formatado como numérico. + - name: glosa + description: > + Valor de glosa aplicada à fatura, formatado como numérico. + - name: valorliquido + description: > + Valor líquido da fatura após subtrações de glosas, multas ou juros, formatado como numérico. + - name: situacao + description: > + Status atual da fatura (Pago ou Pendente). + - name: mesref + description: > + Mês de referência da fatura. + - name: anoref + description: > + Ano de referência da fatura. + - name: id_empenho + description: > + Identificador do empenho extraído do campo JSON dados_empenho. + - name: numero_empenho + description: > + Número do empenho relacionado à fatura, extraído do campo JSON dados_empenho e convertido para maiúsculas. + - name: valor_empenho + description: > + Valor do empenho extraído do campo JSON dados_empenho. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + data_tests: + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'id' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'contrato_id' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'emissao' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'prazo' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'vencimento' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'valor' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'juros' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'multa' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'glosa' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'valorliquido' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'protocolo' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'ateste' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'mesref' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'contratos.faturas' + nome_coluna: 'anoref' + tipo_esperado: 'integer' + + - name: empenhos + description: > + Essa tabela contém informações sobre os empenhos de cada contrato extraídos do ComprasGov. + Os dados são formatados adequadamente para garantir consistência nos tipos numéricos e de data. + Registra os compromissos de pagamento assumidos pela administração para cada contrato. + meta: + tags: + - bronze + columns: + - name: id + description: > + Identificador único do empenho no ComprasGov. + - name: contrato_id + description: > + Identificador do contrato relacionado ao empenho. + - name: nota_empenho + description: > + Número da nota de empenho registrada no sistema. + - name: credor + description: > + Nome ou razão social do credor do empenho. + - name: credor_obj_cnpj_cpf_idgener + description: > + CNPJ, CPF ou identificador genérico do credor do empenho. + - name: empenhado + description: > + Valor total empenhado, formatado como numérico. + - name: aliquidar + description: > + Valor a liquidar do empenho, formatado como numérico. + - name: liquidado + description: > + Valor já liquidado do empenho, formatado como numérico. + - name: pago + description: > + Valor já pago do empenho, formatado como numérico. + - name: rpinscrito + description: > + Valor inscrito em restos a pagar, formatado como numérico. + - name: rpaliquidar + description: > + Valor inscrito em restos a pagar a liquidar, formatado como numérico. + - name: rpliquidado + description: > + Valor inscrito em restos a pagar já liquidado, formatado como numérico. + - name: rppago + description: > + Valor inscrito em restos a pagar já pago, formatado como numérico. + - name: data_emissao + description: > + Data de emissão do empenho, validada e convertida para formato de data padrão. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + data_tests: + - row_count_match: + source_table: compras_gov.empenhos + target_table: contratos.empenhos + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos' + nome_coluna: 'empenhado' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos' + nome_coluna: 'aliquidar' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos' + nome_coluna: 'liquidado' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos' + nome_coluna: 'pago' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos' + nome_coluna: 'rpinscrito' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos' + nome_coluna: 'rpaliquidar' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos' + nome_coluna: 'rpliquidado' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos' + nome_coluna: 'rppago' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos' + nome_coluna: 'data_emissao' + tipo_esperado: 'date' + + - name: empenhos_tesouro + description: > + Essa tabela contém informações sobre movimentação financeira dos empenhos extraídos do SIAFI. + Contém dados de empenhos do Tesouro Nacional com formatação de valores financeiros através da macro parse_financial_value. + Apresenta valores para as diferentes etapas da execução orçamentária (empenho, liquidação, pagamento). + meta: + tags: + - bronze + columns: + - name: emissao_mes + description: > + Mês de emissão do empenho. + - name: emissao_dia + description: > + Data específica de emissão do empenho. + - name: ne_ccor + description: > + Número completo da nota de empenho no SIAFI. + - name: ne_num_processo + description: > + Número do processo relacionado ao empenho, com formatação para remover caracteres especiais. + - name: ne_info_complementar + description: > + Informações complementares sobre o empenho. + - name: ne_ccor_favorecido + description: > + CNPJ ou CPF do favorecido, formatado em caixa alta. + - name: ne_ccor_ano_emissao + description: > + Ano de emissão do empenho, filtrado para incluir apenas empenhos a partir do ano 2000. + - name: despesas_empenhadas + description: > + Valor das despesas empenhadas, formatado como numérico. + - name: despesas_liquidadas + description: > + Valor das despesas liquidadas, formatado como numérico. + - name: despesas_pagas + description: > + Valor das despesas pagas, formatado como numérico. + - name: restos_a_pagar_inscritos + description: > + Valor dos restos a pagar inscritos, formatado como numérico. + - name: restos_a_pagar_pagos + description: > + Valor dos restos a pagar pagos, formatado como numérico. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + data_tests: + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos_tesouro' + nome_coluna: 'ne_ccor_ano_emissao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos_tesouro' + nome_coluna: 'despesas_empenhadas' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos_tesouro' + nome_coluna: 'despesas_liquidadas' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos_tesouro' + nome_coluna: 'despesas_pagas' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos_tesouro' + nome_coluna: 'restos_a_pagar_inscritos' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'contratos.empenhos_tesouro' + nome_coluna: 'restos_a_pagar_pagos' + tipo_esperado: 'numeric' diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/gold/contratos_comparativo_mensal.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/gold/contratos_comparativo_mensal.sql new file mode 100644 index 00000000..9b529c44 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/gold/contratos_comparativo_mensal.sql @@ -0,0 +1,71 @@ +with + + siafi_data as ( + select *, mes_lancamento as mes_ref from {{ ref("contratos_estagios") }} + ), + + compras_gov_data as (select * from {{ ref("cronogramas_faturas_mensal") }}), + + partial_result as ( + select + contrato_id, + mes_ref, + c.valor_cronograma as comprasgov_valor_cronograma, + ( + c.valor_faturas_pagas + c.valor_faturas_pendentes + ) as comprasgov_valor_faturas, + c.saldo_contratual_disponivel as comprasgov_saldo_contratual_disponivel, + s.valor_empenhado as siafi_valor_empenhado, + s.valor_liquidado as siafi_valor_liquidado, + s.valor_pago as siafi_valor_pago, + s.restos_a_pagar as siafi_restos_a_pagar, + s.restos_a_pagar_pago as siafi_restos_a_pagar_pago, + greatest(c.dt_ingest::timestamptz, s.dt_ingest::timestamptz) as dt_ingest + from compras_gov_data as c + full join siafi_data as s using (contrato_id, mes_ref) + + ), + + preenchimento as (select contrato_id, mes_ref from {{ ref("preenchimento_meses") }}), + + contratos as ( + select id, numero, fornecedor_cnpj_cpf_idgener, fornecedor_tipo, fornecedor_nome, dt_ingest as dt_ingest_contratos + from {{ ref("contratos") }} + ), + + comparativo_mensal as ( + select + contrato_id, + mes_ref, + comprasgov_valor_cronograma, + comprasgov_valor_faturas, + comprasgov_saldo_contratual_disponivel, + siafi_valor_empenhado, + siafi_valor_liquidado, + siafi_valor_pago, + siafi_restos_a_pagar, + siafi_restos_a_pagar_pago, + dt_ingest + from partial_result + full join preenchimento using (contrato_id, mes_ref) + ) + +-- +select + ccm.contrato_id, + ccm.mes_ref, + ccm.comprasgov_valor_cronograma, + ccm.comprasgov_valor_faturas, + ccm.comprasgov_saldo_contratual_disponivel, + ccm.siafi_valor_empenhado, + ccm.siafi_valor_liquidado, + ccm.siafi_valor_pago, + ccm.siafi_restos_a_pagar, + ccm.siafi_restos_a_pagar_pago, + c.numero, + c.fornecedor_cnpj_cpf_idgener, + c.fornecedor_tipo, + c.fornecedor_nome, + greatest(ccm.dt_ingest::timestamptz, c.dt_ingest_contratos::timestamptz) as dt_ingest +from comparativo_mensal as ccm +left join contratos as c on ccm.contrato_id = c.id diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/gold/contratos_resumo.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/gold/contratos_resumo.sql new file mode 100755 index 00000000..e0402cef --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/gold/contratos_resumo.sql @@ -0,0 +1,53 @@ +with + + valores_pagos_contratos as ( + select contrato_id as id, sum(despesas_pagas) as despesas_pagas, max(dt_ingest) as dt_ingest_vpc + from {{ ref("contratos_empenhos") }} + where contrato_id is not null + group by contrato_id + ), + + contratos_gold as ( + select + *, + case + when vp.despesas_pagas = c.valor_global then 'Sim' else 'Não' + end as pendente_baixa + from {{ ref("contratos") }} as c + left join valores_pagos_contratos as vp using (id) + ) + +-- +select + id as contrato_id, + fornecedor_cnpj_cpf_idgener as fornecedor_cnpj_cpf, + numero, + categoria, + modalidade, + tipo, + situacao, + pendente_baixa, + fornecedor_nome, + objeto, + valor_global, + despesas_pagas, + vigencia_inicio, + vigencia_fim, + num_parcelas, + case + when fornecedor_tipo = 'IDGENERICO' + then 'Empresa do Exterior' + else fornecedor_tipo + end as fornecedor_tipo, + concat( + contratante__orgao__unidade_gestora__codigo, + ' - ', + contratante__orgao__unidade_gestora__nome_resumido + ) as "Unidade", + case + when vigencia_fim - vigencia_inicio >= 730 and num_parcelas > 1 + then 'Sim' + else 'Não' + end as continuado, + greatest(dt_ingest, dt_ingest_vpc) as dt_ingest +from contratos_gold diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/gold/contratos_somatorio.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/gold/contratos_somatorio.sql new file mode 100644 index 00000000..c75887b5 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/gold/contratos_somatorio.sql @@ -0,0 +1,22 @@ +select + contrato_id, + numero, + fornecedor_cnpj_cpf_idgener, + fornecedor_tipo, + fornecedor_nome, + sum(comprasgov_valor_cronograma) as total_cronograma, + sum(comprasgov_valor_faturas) as total_faturas, + sum(comprasgov_saldo_contratual_disponivel) as total_saldo_disponivel, + + -- Indicador de Orçamento a Executar: + sum( + case when comprasgov_valor_faturas = 0 then comprasgov_valor_cronograma else 0 end + ) as orcamento_a_executar, + + sum(siafi_valor_empenhado) as total_empenhado, + sum(siafi_valor_liquidado) as total_liquidado, + sum(siafi_valor_pago) as total_pago, + max(dt_ingest) as dt_ingest + +from {{ ref("contratos_comparativo_mensal") }} +group by contrato_id, numero, fornecedor_cnpj_cpf_idgener, fornecedor_tipo, fornecedor_nome diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/gold/schema.yml b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/gold/schema.yml new file mode 100644 index 00000000..831fe06d --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/gold/schema.yml @@ -0,0 +1,172 @@ +version: 2 + +models: + + ## Golds + - name: contratos_resumo + description: > + Essa tabela contém um resumo dos contratos, informações contratuais como valor global e valor pago, + e situação atual, como em vigência ou pendente de baixa. + Facilita a análise gerencial dos contratos com indicadores chave como status de pagamento, tipo de fornecedor, + e identificação de contratos continuados (com vigência superior a dois anos e mais de uma parcela). + meta: + tags: + - gold + columns: + - name: contrato_id + description: > + Identificador único do contrato. + - name: fornecedor_cnpj_cpf + description: > + CNPJ ou CPF do fornecedor contratado, com formatação padronizada. + - name: numero + description: > + Número do contrato no sistema ComprasGov. + - name: categoria + description: > + Categoria do contrato ( Informática, Serviços, Mão de Obra). + - name: modalidade + description: > + Modalidade de licitação utilizada na contratação. + - name: tipo + description: > + Tipo de contrato ( Contrato, Empenho, Termo de Compromisso). + - name: situacao + description: > + Situação atual do contrato (Ativo ou Inativo). + - name: pendente_baixa + description: > + Indica se o contrato está pendente de baixa ('Sim' quando o valor pago for igual ao valor global, 'Não' caso contrário). + - name: fornecedor_nome + description: > + Nome ou razão social do fornecedor contratado. + - name: objeto + description: > + Descrição detalhada do objeto contratado. + - name: valor_global + description: > + Valor total do contrato após eventuais aditivos. + - name: despesas_pagas + description: > + Valor total já pago do contrato conforme registros do SIAFI. + - name: vigencia_inicio + description: > + Data de início da vigência do contrato. + - name: vigencia_fim + description: > + Data de término da vigência do contrato. + - name: num_parcelas + description: > + Número de parcelas para pagamento do contrato. + - name: fornecedor_tipo + description: > + Tipo do fornecedor, categorizado como 'Empresa do Exterior' para IDGENERICO. + - name: Unidade + description: > + Unidade gestora responsável pelo contrato, no formato "código - nome_resumido". + - name: continuado + description: > + Indica se o contrato é de prestação continuada ('Sim' quando a vigência for maior que 730 dias e tiver mais de uma parcela). + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: contratos_comparativo_mensal + description: > + Essa tabela contém um comparativo mensal dos contratos, discriminando + os valores empenhados, liquidados e pagos do SIAFI, + programados e faturados do ComprasGov. + Permite a identificação de inconsistências entre os sistemas e o acompanhamento detalhado + da execução financeira mensal de cada contrato. + meta: + tags: + - gold + columns: + - name: contrato_id + description: > + Identificador único do contrato. + - name: numero + description: > + Número do contrato no sistema ComprasGov. + - name: fornecedor_cnpj_cpf_idgener + description: > + CNPJ, CPF ou identificador genérico do fornecedor do contrato. + - name: fornecedor_tipo + description: > + Tipo do fornecedor do contrato. + - name: fornecedor_nome + description: > + Nome ou razão social do fornecedor associado ao contrato. + - name: mes_ref + description: > + Mês de referência da informação financeira, preenchido para todos os meses entre o início e fim do contrato. + - name: comprasgov_valor_cronograma + description: > + Valor programado para o mês conforme cronograma registrado no ComprasGov. + - name: comprasgov_valor_faturas + description: > + Soma dos valores das faturas (pagas e pendentes) no mês de referência conforme ComprasGov. + - name: comprasgov_saldo_contratual_disponivel + description: > + Diferença entre o valor programado e o valor faturado no mês, indicando o saldo disponível. + - name: siafi_valor_empenhado + description: > + Valor empenhado no mês de referência conforme registros do SIAFI. + - name: siafi_valor_liquidado + description: > + Valor liquidado no mês de referência conforme registros do SIAFI. + - name: siafi_valor_pago + description: > + Valor pago no mês de referência conforme registros do SIAFI. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: contratos_somatorio + description: > + Essa tabela contém somatórios dos valores de cronograma, fatura, empenho, liquidação e pagamento para cada contrato. + Facilita análises agregadas sobre a execução financeira total de cada contrato, com indicadores importantes + como o orçamento pendente de execução. + meta: + tags: + - gold + columns: + - name: contrato_id + description: > + Identificador único do contrato. + - name: numero + description: > + Número do contrato no sistema ComprasGov. + - name: fornecedor_cnpj_cpf_idgener + description: > + CNPJ, CPF ou identificador genérico do fornecedor do contrato. + - name: fornecedor_tipo + description: > + Tipo do fornecedor do contrato. + - name: fornecedor_nome + description: > + Nome ou razão social do fornecedor associado ao contrato. + - name: total_cronograma + description: > + Soma de todos os valores programados no cronograma do contrato. + - name: total_faturas + description: > + Soma de todos os valores faturados (pagos e pendentes) para o contrato. + - name: total_saldo_disponivel + description: > + Diferença total entre valores programados e faturados, indicando o saldo contratual disponível. + - name: orcamento_a_executar + description: > + Soma dos valores programados em meses onde ainda não houve faturamento, indicando o orçamento pendente de execução. + - name: total_empenhado + description: > + Soma de todos os valores empenhados para o contrato segundo o SIAFI. + - name: total_liquidado + description: > + Soma de todos os valores liquidados para o contrato segundo o SIAFI. + - name: total_pago + description: > + Soma de todos os valores pagos para o contrato segundo o SIAFI. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/contratos_empenhos.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/contratos_empenhos.sql new file mode 100755 index 00000000..50263f32 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/contratos_empenhos.sql @@ -0,0 +1,251 @@ +with + contratos_com_ne_cnpj_cpf as ( + select distinct contrato_id, ne, cnpj_cpf + from {{ ref("identificadores") }} + where ne is not null + ), + + empenhos_tesouro_transformed as ( + select + *, + case + when ne_ccor is not null then upper(right(ne_ccor, 12)) + end as ne_transformed + from {{ ref("empenhos_tesouro") }} + ), + + -- Primeiro merge: apenas os contratos que tem ne e cnpj_cpf + full_join as ( + select + *, + case + when c.cnpj_cpf is not null and e.ne_ccor_favorecido is not null + then 'both' + when c.cnpj_cpf is not null and e.ne_ccor_favorecido is null + then 'left only' -- nao existe no empenhos_tesouro + when c.cnpj_cpf is null and e.ne_ccor_favorecido is not null + then 'right only' -- existe no empenhos_tesouro mas nao no raw.contratos (ESSA QUE NÓS QUEREMOS) + end as origem + from contratos_com_ne_cnpj_cpf as c + full join + empenhos_tesouro_transformed as e + on c.ne = e.ne_transformed + and c.cnpj_cpf = e.ne_ccor_favorecido + ), + + resultado_1 as ( + select + contrato_id, + coalesce(ne_transformed, ne) as ne_transformed, + ne_ccor, + ne_info_complementar, + ne_num_processo, + ne_ccor_descricao, + doc_observacao, + natureza_despesa, + natureza_despesa_descricao, + ne_ccor_favorecido, + ne_ccor_ano_emissao, + despesas_empenhadas, + despesas_liquidadas, + despesas_pagas, + dt_ingest + from full_join + where origem = 'both' or origem = 'left only' + -- contrato_id nulo significa lacuna no lado esquerdo do RIGHT JOIN, + -- ou contratos em que o join usando ne e cnpj/cpf não foi possível + ), + + -- --------------------------------------------------------------------------------------------------- + empenhos_restantes_1 as ( + select * + from empenhos_tesouro_transformed + where ne_ccor in (select ne_ccor from full_join where origem = 'right only') + ), + + todos_contratos as ( + select distinct contrato_id, processo, cnpj_cpf, info_complementar + from {{ ref("identificadores") }} + ), + + -- Segundo merge: usando processos em que há um único contrato + processos_unicos as ( + select distinct contrato_id, processo as num_processo + from todos_contratos + where + processo in ( + select processo from todos_contratos group by processo having count(*) = 1 + ) + ), + + juncao_processo as ( + select * + from processos_unicos as cpu + right join empenhos_restantes_1 as er on cpu.num_processo = er.ne_num_processo + ), + + resultado_2 as ( + select + contrato_id, + ne_transformed, + ne_ccor, + ne_info_complementar, + ne_num_processo, + ne_ccor_descricao, + doc_observacao, + natureza_despesa, + natureza_despesa_descricao, + ne_ccor_favorecido, + ne_ccor_ano_emissao, + despesas_empenhadas, + despesas_liquidadas, + despesas_pagas, + dt_ingest + from juncao_processo + -- WHERE origem = 'both' + where contrato_id is not null + ), + -- --------------------------------------------------------------------------------------------------- + empenhos_restantes_2 as ( + select * + from empenhos_restantes_1 + where ne_ccor not in (select ne_ccor from resultado_2) + ), + + -- Terceiro merge: usando CNPJs que possuem um único contrato + cnpjs_unicos as ( + select distinct contrato_id, cnpj_cpf + from todos_contratos + where + cnpj_cpf in ( + select cnpj_cpf from todos_contratos group by cnpj_cpf having count(*) = 1 + ) + ), + + juncao_cnpjs as ( + select * + from cnpjs_unicos as cu + right join empenhos_restantes_2 as er on cu.cnpj_cpf = er.ne_ccor_favorecido + ), + + resultado_3 as ( + select + contrato_id, + ne_transformed, + ne_ccor, + ne_info_complementar, + ne_num_processo, + ne_ccor_descricao, + doc_observacao, + natureza_despesa, + natureza_despesa_descricao, + ne_ccor_favorecido, + ne_ccor_ano_emissao, + despesas_empenhadas, + despesas_liquidadas, + despesas_pagas, + dt_ingest + from juncao_cnpjs + where contrato_id is not null + ), + -- --------------------------------------------------------------------------------------------------- + empenhos_restantes_3 as ( + select + *, + -- garantir que ambos os lados estão no mesmo formato + substring(ne_info_complementar from '^([0-9]+) -') as info_complementar + from empenhos_restantes_2 + where ne_ccor not in (select ne_ccor from resultado_3) + ), + + contratos_info_complementar as ( + select distinct contrato_id, info_complementar from todos_contratos + ), + + -- Quarto merge: usando "informação complementar", que é um agregado da unidade + -- gestora + modalidade + numero do contrato ou numero da licitação + juncao_info_complementar as ( + select * + from contratos_info_complementar as c + right join empenhos_restantes_3 as e on c.info_complementar = e.info_complementar + ), + + resultado_4 as ( + select + contrato_id, + ne_transformed, + ne_ccor, + ne_info_complementar, + ne_num_processo, + ne_ccor_descricao, + doc_observacao, + natureza_despesa, + natureza_despesa_descricao, + ne_ccor_favorecido, + ne_ccor_ano_emissao, + despesas_empenhadas, + despesas_liquidadas, + despesas_pagas, + dt_ingest + from juncao_info_complementar + where contrato_id is not null + ), + + -- União de todos os resultados parciais + resultado_final as ( + select * + from resultado_1 + union + select * + from resultado_2 + union + select * + from resultado_3 + union + select * + from resultado_4 + ), + + contratos as ( + select + id, + fornecedor_tipo, + fornecedor_nome, + fornecedor_cnpj_cpf_idgener, + numero, + unidades_requisitantes, + objeto, + vigencia_inicio, + vigencia_fim, + situacao, + dt_ingest as dt_ingest_contratos + from {{ ref("contratos") }} + ) + +select + ce.contrato_id, + ce.ne_transformed, + ce.ne_ccor, + ce.ne_info_complementar, + ce.ne_num_processo, + ce.ne_ccor_descricao, + ce.doc_observacao, + ce.natureza_despesa, + ce.natureza_despesa_descricao, + ce.ne_ccor_favorecido, + ce.ne_ccor_ano_emissao, + ce.despesas_empenhadas, + ce.despesas_liquidadas, + ce.despesas_pagas, + cc.fornecedor_tipo, + cc.fornecedor_nome, + cc.fornecedor_cnpj_cpf_idgener, + cc.numero, + cc.unidades_requisitantes, + cc.objeto, + cc.vigencia_inicio, + cc.vigencia_fim, + greatest(ce.dt_ingest, cc.dt_ingest_contratos) as dt_ingest +from resultado_final as ce +full join contratos as cc on ce.contrato_id = cc.id +where cc.situacao = 'Ativo' diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/contratos_estagios.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/contratos_estagios.sql new file mode 100644 index 00000000..3be3a6b8 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/contratos_estagios.sql @@ -0,0 +1,126 @@ +with + + ids_filtrados as ( + select contrato_id, ne, cnpj_cpf, processo, info_complementar + from {{ ref("identificadores") }} + where categoria not in ('Cessão') + ), + + -- Join 1 + id_table_1 as (select distinct contrato_id, ne, cnpj_cpf from ids_filtrados), + + joined_table_1 as ( + select + contrato_id, + cnpj_cpf, + ne, + num_processo, + info_complementar, + mes_lancamento, + valor_empenhado, + valor_liquidado, + valor_pago, + restos_a_pagar, + restos_a_pagar_pago, + dt_ingest + from {{ ref("estagios_mensal") }} + left join id_table_1 using (ne, cnpj_cpf) + ), + + -- Part 2 + empenhos_restantes_1 as (select * from joined_table_1 where contrato_id is null), + + id_table_2 as ( + select distinct contrato_id, cnpj_cpf, processo as num_processo + from ids_filtrados l + where + not exists ( + select distinct contrato_id + from joined_table_1 r + where r.contrato_id = l.contrato_id + ) + ), + + joined_table_2 as ( + select + r.contrato_id, + cnpj_cpf, + ne, + num_processo, + info_complementar, + mes_lancamento, + valor_empenhado, + valor_liquidado, + valor_pago, + restos_a_pagar, + restos_a_pagar_pago, + dt_ingest + from empenhos_restantes_1 l + left join id_table_2 r using (cnpj_cpf, num_processo) + ), + + -- Part 3 + empenhos_restantes_2 as (select * from joined_table_2 where contrato_id is null), + + id_table_3 as ( + select distinct t0.contrato_id, t0.cnpj_cpf, t0.info_complementar + from ids_filtrados t0 + left join id_table_1 t1 on t0.contrato_id = t1.contrato_id + left join id_table_2 t2 on t0.contrato_id = t2.contrato_id + where t1.contrato_id is null or t2.contrato_id is null + + ), + + joined_table_3 as ( + select + r.contrato_id, + cnpj_cpf, + ne, + num_processo, + info_complementar, + mes_lancamento, + valor_empenhado, + valor_liquidado, + valor_pago, + restos_a_pagar, + restos_a_pagar_pago, + dt_ingest + from empenhos_restantes_2 l + left join id_table_3 r using (cnpj_cpf, info_complementar) + ), + + result_table as ( + select * + from joined_table_1 + union + select * + from joined_table_2 + union + select * + from joined_table_3 + ), + + contratos as ( + select id, numero, fornecedor_cnpj_cpf_idgener, fornecedor_tipo, fornecedor_nome, dt_ingest as dt_ingest_contratos + from {{ ref("contratos") }} + ) + +-- +select + r.contrato_id, + c.numero, + c.fornecedor_cnpj_cpf_idgener, + c.fornecedor_tipo, + c.fornecedor_nome, + r.mes_lancamento, + sum(r.valor_empenhado) as valor_empenhado, + sum(r.valor_liquidado) as valor_liquidado, + sum(r.valor_pago) as valor_pago, + sum(r.restos_a_pagar) as restos_a_pagar, + sum(r.restos_a_pagar_pago) as restos_a_pagar_pago, + greatest(max(r.dt_ingest), max(c.dt_ingest_contratos)) as dt_ingest +from result_table r +left join contratos c on r.contrato_id = c.id +where r.contrato_id is not null +group by 1, 2, 3, 4, 5, 6 +order by r.contrato_id, r.mes_lancamento diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/contratos_faturas.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/contratos_faturas.sql new file mode 100644 index 00000000..4d983e70 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/contratos_faturas.sql @@ -0,0 +1,61 @@ +{{ config(materialized="table") }} + +with + contratos as ( + select + id::int as contrato_id, + fornecedor_cnpj_cpf_idgener, + fornecedor_tipo, + fornecedor_nome, + processo as processo_contrato, + numero as numero_contrato, + objeto as objeto_contrato, + unidades_requisitantes, + dt_ingest as dt_ingest_contratos + from {{ ref("contratos") }} + ), + + faturas_base as (select * from {{ ref("faturas") }}) + +select + f.id, + f.contrato_id, + c.numero_contrato, + c.processo_contrato as contrato_processo, + c.fornecedor_cnpj_cpf_idgener, + c.fornecedor_tipo, + c.fornecedor_nome, + c.objeto_contrato, + c.unidades_requisitantes, + f.tipolistafatura_id, + f.justificativafatura_id, + f.sfadrao_id, + f.numero, + f.emissao, + f.prazo, + f.vencimento, + f.valor, + f.juros, + f.multa, + f.glosa, + f.valorliquido, + f.processo, + f.protocolo, + f.ateste, + f.repactuacao, + f.infcomplementar, + f.mesref, + f.anoref, + f.situacao, + f.chave_nfe, + f.dados_referencia, + f.dados_item_faturado, + f.dados_empenho, + f.id_empenho, + f.numero_empenho, + f.valor_empenho, + f.subelemento, + greatest(f.dt_ingest, c.dt_ingest_contratos) as dt_ingest +from faturas_base f +left join contratos c on f.contrato_id = c.contrato_id +where f.emissao < '2026-01-01' diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/cronogramas_faturas_mensal.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/cronogramas_faturas_mensal.sql new file mode 100644 index 00000000..a581d292 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/cronogramas_faturas_mensal.sql @@ -0,0 +1,97 @@ +with + + cronograma_agg as ( + select contrato_id, vencimento as mes_ref, sum(valor) as valor_cronograma + from {{ ref("cronogramas") }} + group by 1, 2 + order by contrato_id, vencimento + ), + + faturas_parsed as ( + select + contrato_id::integer as contrato_id, + emissao::date as emissao, + replace(replace(juros::text, '.', ''), ',', '.')::numeric(15, 2) as juros, + replace(replace(multa::text, '.', ''), ',', '.')::numeric(15, 2) as multa, + replace(replace(glosa::text, '.', ''), ',', '.')::numeric(15, 2) as glosa, + replace(replace(valorliquido::text, '.', ''), ',', '.')::numeric( + 15, 2 + ) as valorliquido, + situacao::text as situacao, + dt_ingest + from {{ source("compras_gov", "faturas") }} + ), + + faturas_pago as ( + select + contrato_id, + to_date( + split_part(emissao::text, '-', 1) -- verificar se o mês de emissão é o adequado para ser utilizada + || '-' + || split_part(emissao::text, '-', 2), + 'YYYY-MM' + ) as mes_ref, + sum(juros + multa + glosa + valorliquido) as valor_faturas_pagas, + max(dt_ingest) as dt_ingest_pago + from faturas_parsed + where situacao = 'Pago' + group by 1, 2 + ), + + faturas_pendente as ( + select + contrato_id, + to_date( + split_part(emissao::text, '-', 1) + || '-' + || split_part(emissao::text, '-', 2), + 'YYYY-MM' + ) as mes_ref, + sum(juros + multa + glosa + valorliquido) as valor_faturas_pendentes, + max(dt_ingest) as dt_ingest_pendente + from faturas_parsed + where situacao = 'Pendente' + group by 1, 2 + ), + + joined_table as ( + select * + from cronograma_agg + left join faturas_pago using (contrato_id, mes_ref) + left join faturas_pendente using (contrato_id, mes_ref) + ), + + joined_ajustado as ( + select + contrato_id::text, + mes_ref, + coalesce(valor_cronograma, 0) as valor_cronograma, + coalesce(valor_faturas_pagas, 0) as valor_faturas_pagas, + coalesce(valor_faturas_pendentes, 0) as valor_faturas_pendentes, + coalesce(valor_cronograma, 0) + - coalesce(valor_faturas_pagas, 0) + - coalesce(valor_faturas_pendentes, 0) as saldo_contratual_disponivel, + greatest(dt_ingest_pago, dt_ingest_pendente) AS dt_ingest + from joined_table + order by contrato_id, mes_ref + ), + + contratos as ( + select id::text as contrato_id, numero, fornecedor_cnpj_cpf_idgener, fornecedor_tipo, fornecedor_nome, dt_ingest + from {{ ref("contratos") }} + ) + +select + ja.contrato_id, + ja.mes_ref, + ja.valor_cronograma, + ja.valor_faturas_pagas, + ja.valor_faturas_pendentes, + ja.saldo_contratual_disponivel, + c.numero, + c.fornecedor_cnpj_cpf_idgener, + c.fornecedor_tipo, + c.fornecedor_nome, + greatest(ja.dt_ingest::timestamp with time zone, c.dt_ingest::timestamp with time zone) as dt_ingest +from joined_ajustado ja +left join contratos c using (contrato_id) diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/estagios_mensal.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/estagios_mensal.sql new file mode 100644 index 00000000..f990dcd9 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/estagios_mensal.sql @@ -0,0 +1,134 @@ +with + + parsed_estagios as ( + select + right(ne_ccor, 12) as ne, + case when emissao_dia like '000/%' then true else false end as eh_rap, + case + when emissao_dia like '000/%' + then '01' + else substring(emissao_dia, '\/(\d{2})\/') + end as mes_lancamento, + right(emissao_mes, 4) as ano_lancamento, + ne_ccor_favorecido as cnpj_cpf, + substring(ne_info_complementar, '(^[0-9]+)') as info_complementar, + ne_num_processo, + despesas_empenhadas as valor_empenhado, + despesas_liquidadas as valor_liquidado, + despesas_pagas as valor_pago, + restos_a_pagar_inscritos as restos_a_pagar, + restos_a_pagar_pagos as restos_a_pagar_pago, + dt_ingest + from {{ ref("empenhos_tesouro") }} + where true and ne_ccor != 'Total' + ), + + grouped_estagios as ( + select + ne, + eh_rap, + ano_lancamento::integer as ano_lancamento, + mes_lancamento, + cnpj_cpf, + max(info_complementar) as info_complementar, + max(ne_num_processo) as num_processo, + sum(valor_empenhado) as valor_empenhado, + sum(valor_liquidado) as valor_liquidado, + sum(valor_pago) as valor_pago, + sum(restos_a_pagar) as restos_a_pagar, + sum(restos_a_pagar_pago) as restos_a_pagar_pago, + max(dt_ingest) as dt_ingest + from parsed_estagios + group by 1, 2, 3, 4, 5 + order by 1, 2 + ), + + processo_fixed as ( + select + ne, + cnpj_cpf, + info_complementar, + eh_rap, + mes_lancamento, + ano_lancamento, + case + when eh_rap + then array[ano_lancamento - 1, ano_lancamento] + else array[ano_lancamento] + end as ano_efetivo, + min(num_processo) over (partition by ne) as num_processo, + valor_empenhado, + valor_liquidado, + valor_pago, + restos_a_pagar, + restos_a_pagar_pago, + dt_ingest + from grouped_estagios + ), + + unnest_rap as ( + select + ne, + cnpj_cpf, + info_complementar, + eh_rap, + mes_lancamento, + ano_lancamento, + unnest(ano_efetivo) as ano_efetivo, + num_processo, + valor_empenhado, + valor_liquidado, + valor_pago, + restos_a_pagar, + restos_a_pagar_pago, + dt_ingest + from processo_fixed + ), + + fix_data as ( + select + ne, + cnpj_cpf, + info_complementar, + eh_rap, + case + when ano_efetivo = ano_lancamento then mes_lancamento else '12' + end as mes_lancamento, + ano_lancamento, + ano_efetivo, + num_processo, + valor_empenhado, + valor_liquidado, + valor_pago, + case + when ano_efetivo = ano_lancamento + then restos_a_pagar + else - restos_a_pagar + end as restos_a_pagar, + restos_a_pagar_pago, + dt_ingest + from unnest_rap + ), + + results as ( + select + ne, + cnpj_cpf, + info_complementar, + num_processo, + to_date( + ano_efetivo || '-' || mes_lancamento || '-01', 'YYYY-MM-DD' + ) as mes_lancamento, + valor_empenhado, + valor_liquidado, + valor_pago, + restos_a_pagar, + restos_a_pagar_pago, + dt_ingest + from fix_data + ) + +-- +select * +from results +order by ne, cnpj_cpf, mes_lancamento diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/schema.yml b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/schema.yml new file mode 100644 index 00000000..28f174c0 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/silver/schema.yml @@ -0,0 +1,326 @@ +version: 2 + +models: + + ## Silver + - name: contratos_empenhos + description: > + Essa tabela combina as informações de movimentação financeira dos empenhos com os ids de contrato do IPEA. + Realiza uma complexa estratégia de join entre os contratos e empenhos do tesouro, tentando várias abordagens + para relacionar os dados do SIAFI com os contratos do ComprasGov usando: + 1) número de empenho e CNPJ/CPF, 2) número de processo, 3) CNPJ/CPF único, e 4) informação complementar. + meta: + tags: + - silver + columns: + - name: contrato_id + description: > + Identificador único do contrato relacionado ao empenho. + - name: ne_transformed + description: > + Número da nota de empenho padronizado para facilitar joins entre diferentes fontes. + - name: ne_ccor + description: > + Número completo da nota de empenho no SIAFI. + - name: ne_info_complementar + description: > + Informações complementares sobre o empenho no SIAFI. + - name: ne_num_processo + description: > + Número do processo administrativo relacionado ao empenho, formatado para remover caracteres especiais. + - name: ne_ccor_descricao + description: > + Descrição da nota de empenho conforme registrado no SIAFI. + - name: doc_observacao + description: > + Observações registradas no documento do empenho. + - name: natureza_despesa + description: > + Código da natureza da despesa do empenho. + - name: natureza_despesa_descricao + description: > + Descrição da natureza da despesa do empenho. + - name: ne_ccor_favorecido + description: > + CNPJ ou CPF do favorecido do empenho. + - name: ne_ccor_ano_emissao + description: > + Ano de emissão do empenho. + - name: despesas_empenhadas + description: > + Valor total das despesas empenhadas para o contrato. + - name: despesas_liquidadas + description: > + Valor total das despesas liquidadas para o contrato. + - name: despesas_pagas + description: > + Valor total das despesas pagas para o contrato. + - name: fornecedor_nome + description: > + Nome do fornecedor associado ao contrato. + - name: fornecedor_cnpj_cpf_idgener + description: > + CNPJ, CPF ou identificador genérico do fornecedor do contrato. + - name: numero + description: > + Número do contrato no sistema ComprasGov. + - name: unidades_requisitantes + description: > + Unidades requisitantes associadas ao contrato. + - name: objeto + description: > + Descrição do objeto contratado. + - name: vigencia_inicio + description: > + Data de início da vigência do contrato. + - name: vigencia_fim + description: > + Data de término da vigência do contrato. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: contratos_estagios + description: > + Essa tabela combina as informações de movimentação financeira dos empenhos com os ids de contrato do IPEA mensalmente. + Utiliza uma estratégia de join em cascata, tentando correlacionar os estágios das despesas do SIAFI com os + contratos do ComprasGov através de várias combinações de chaves, incluindo número de empenho, CNPJ/CPF, número + de processo e informações complementares. + meta: + tags: + - silver + columns: + - name: contrato_id + description: > + Identificador único do contrato relacionado aos estágios. + - name: numero + description: > + Número do contrato no sistema ComprasGov. + - name: fornecedor_cnpj_cpf_idgener + description: > + CNPJ, CPF ou identificador genérico do fornecedor do contrato. + - name: fornecedor_tipo + description: > + Tipo do fornecedor do contrato. + - name: fornecedor_nome + description: > + Nome ou razão social do fornecedor associado ao contrato. + - name: mes_lancamento + description: > + Mês em que o estágio da despesa foi registrado no SIAFI. + - name: valor_empenhado + description: > + Valor empenhado no mês de referência para o contrato. + - name: valor_liquidado + description: > + Valor liquidado no mês de referência para o contrato. + - name: valor_pago + description: > + Valor pago no mês de referência para o contrato. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: estagios_mensal + description: > + Essa tabela discrimina os valores empenhados, liquidados e pagos mensalmente extraídos do SIAFI para cada contrato. + Transforma e agrega os dados da tabela empenhos_tesouro do SIAFI, padronizando formatos e consolidando valores + por número de empenho, mês e CNPJ/CPF. + meta: + tags: + - silver + columns: + - name: ne + description: > + Número da nota de empenho no formato padronizado, extraído das 12 últimas posições do ne_ccor. + - name: mes_lancamento + description: > + Mês de referência do lançamento, convertido para formato de data padrão. + - name: cnpj_cpf + description: > + CNPJ ou CPF do favorecido do empenho. + - name: info_complementar + description: > + Informação complementar extraída do ne_info_complementar. + - name: num_processo + description: > + Número do processo administrativo relacionado ao empenho. + - name: valor_empenhado + description: > + Valor total empenhado no mês e para o empenho específico. + - name: valor_liquidado + description: > + Valor total liquidado no mês e para o empenho específico. + - name: valor_pago + description: > + Valor total pago no mês e para o empenho específico. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: contratos_faturas + description: > + Essa tabela integra informações detalhadas de faturas com dados dos contratos relacionados. + Permite uma visão completa de cada fatura no contexto do seu contrato, incluindo dados como número de contrato, + processo, objeto e fornecedor. Facilita a análise financeira das faturas em conjunto com seus respectivos contratos. + meta: + tags: + - silver + columns: + - name: id + description: > + Identificador único da fatura. + - name: contrato_id + description: > + Identificador do contrato relacionado à fatura. + - name: numero_contrato + description: > + Número do contrato no sistema ComprasGov. + - name: contrato_processo + description: > + Número do processo administrativo relacionado ao contrato. + - name: fornecedor_cnpj_cpf_idgener + description: > + CNPJ, CPF ou identificador genérico do fornecedor do contrato. + - name: fornecedor_tipo + description: > + Tipo do fornecedor do contrato. + - name: fornecedor_nome + description: > + Nome ou razão social do fornecedor associado ao contrato. + - name: objeto_contrato + description: > + Descrição do objeto contratado. + - name: unidades_requisitantes + description: > + Unidades requisitantes associadas ao contrato relacionado à fatura. + - name: tipolistafatura_id + description: > + Identificador do tipo de lista de fatura. + - name: justificativafatura_id + description: > + Identificador da justificativa da fatura, quando aplicável. + - name: sfadrao_id + description: > + Identificador do padrão de sistema financeiro relacionado. + - name: numero + description: > + Número da fatura emitida pelo fornecedor. + - name: emissao + description: > + Data de emissão da fatura. + - name: prazo + description: > + Data limite para pagamento da fatura. + - name: vencimento + description: > + Data de vencimento da fatura. + - name: valor + description: > + Valor bruto da fatura. + - name: juros + description: > + Valor de juros aplicados à fatura. + - name: multa + description: > + Valor de multa aplicada à fatura. + - name: glosa + description: > + Valor de glosa aplicada à fatura. + - name: valorliquido + description: > + Valor líquido da fatura após subtrações de glosas, multas ou juros. + - name: processo + description: > + Número do processo administrativo específico da fatura, quando diferente do processo do contrato. + - name: protocolo + description: > + Data de protocolo da fatura no sistema. + - name: ateste + description: > + Data em que a fatura foi atestada, confirmando a entrega do produto ou serviço. + - name: repactuacao + description: > + Indica se a fatura está relacionada a uma repactuação contratual. + - name: infcomplementar + description: > + Informações complementares sobre a fatura. + - name: mesref + description: > + Mês de referência da fatura. + - name: anoref + description: > + Ano de referência da fatura. + - name: situacao + description: > + Status atual da fatura (Pago ou Pendente). + - name: chave_nfe + description: > + Chave da Nota Fiscal Eletrônica associada à fatura, quando disponível. + - name: dados_referencia + description: > + Dados de referência adicionais da fatura, geralmente em formato JSON. + - name: dados_item_faturado + description: > + Detalhamento dos itens faturados, geralmente em formato JSON. + - name: dados_empenho + description: > + Informações sobre o empenho relacionado à fatura, em formato JSON. + - name: id_empenho + description: > + Identificador do empenho extraído do campo JSON dados_empenho. + - name: numero_empenho + description: > + Número do empenho relacionado à fatura. + - name: valor_empenho + description: > + Valor do empenho relacionado à fatura. + - name: subelemento + description: > + Código do subelemento de despesa relacionado à fatura. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: cronogramas_faturas_mensal + description: > + Essa tabela discrimina os valores programados e faturados mensalmente pelo ComprasGov para cada contrato. + Combina dados dos cronogramas com as faturas pagas e pendentes, agrupando por contrato e mês de referência, + e calculando o saldo contratual disponível (diferença entre o valor programado e os valores faturados). + meta: + tags: + - silver + columns: + - name: contrato_id + description: > + Identificador único do contrato. + - name: numero + description: > + Número do contrato no sistema ComprasGov. + - name: fornecedor_cnpj_cpf_idgener + description: > + CNPJ, CPF ou identificador genérico do fornecedor do contrato. + - name: fornecedor_tipo + description: > + Tipo do fornecedor do contrato. + - name: fornecedor_nome + description: > + Nome ou razão social do fornecedor associado ao contrato. + - name: mes_ref + description: > + Mês de referência dos valores programados e faturados. + - name: valor_cronograma + description: > + Valor total programado para o mês de referência conforme cronograma do contrato. + - name: valor_faturas_pagas + description: > + Valor total de faturas com status "Pago" no mês de referência. + - name: valor_faturas_pendentes + description: > + Valor total de faturas com status "Pendente" no mês de referência. + - name: saldo_contratual_disponivel + description: > + Diferença entre o valor programado no cronograma e a soma dos valores faturados (pagos e pendentes). + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/views/identificadores.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/views/identificadores.sql new file mode 100644 index 00000000..f0301d2f --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/views/identificadores.sql @@ -0,0 +1,59 @@ +with + + ids_from_empenhos as ( + select distinct contrato_id::text as contrato_id, upper(nota_empenho) as ne + from {{ ref("empenhos") }} + ), + + ids_from_faturas as ( + select distinct contrato_id::text as contrato_id, upper(numero_empenho) as ne + from {{ ref("faturas") }} + ), + + ids_table as ( + select * + from ids_from_empenhos e + full join ids_from_faturas f using (contrato_id, ne) + ), + + contratos as ( + select + id::text as contrato_id, + categoria, + case when length(numero) = 12 then numero end as ne, + regexp_replace(processo, '[^0-9]', '', 'g') as processo, + regexp_replace(fornecedor_cnpj_cpf_idgener, '[/.-]', '', 'g') as cnpj_cpf, + case + when codigo_modalidade in ('05', '06') + then + concat( + contratante__orgao__unidade_gestora__codigo, + codigo_modalidade, + replace(numero, '/', '') + ) + when codigo_modalidade = '07' + then + concat( + contratante__orgao__unidade_gestora__codigo, + codigo_modalidade, + replace(licitacao_numero, '/', '') + ) + end as info_complementar + from {{ ref("contratos") }} + ), + + identificadores as ( + select + c.contrato_id, + c.categoria, + c.processo, + c.cnpj_cpf, + c.info_complementar, + coalesce(i.ne, c.ne) as ne + from contratos as c + full join ids_table as i on c.contrato_id = i.contrato_id + ) + +-- +select * +from identificadores diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/views/preenchimento_meses.sql b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/views/preenchimento_meses.sql new file mode 100644 index 00000000..f1f13297 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/views/preenchimento_meses.sql @@ -0,0 +1,17 @@ +-- Essa view será usada para preencher todos os gaps temporais +-- em tabelas gold com o propósito de eliminar decontinuidades +-- nas visualizações em linha +with + + contractos_lista as ( + select contrato_id, min(mes_ref) as min_mes, max(mes_ref) as max_mes + from {{ ref("cronogramas_faturas_mensal") }} + group by contrato_id + ), + + meses_lista as (select distinct mes_ref from {{ ref("cronogramas_faturas_mensal") }}) + +-- +select c.contrato_id, m.mes_ref +from contractos_lista c +left join meses_lista m on (c.min_mes <= m.mes_ref) and (c.max_mes >= m.mes_ref) diff --git a/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/views/schema.yml b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/views/schema.yml new file mode 100644 index 00000000..157ad343 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/contratos_dbt/views/schema.yml @@ -0,0 +1,30 @@ +version: 2 + +models: + + ## View + - name: identificadores + description: > + Essa tabela contém os identificadores dos contratos, empenhos e faturas. + Consolida os números de empenho encontrados em diferentes fontes (contratos, faturas, empenhos) + e padroniza informações como processo e CNPJ/CPF para facilitar joins entre as tabelas. + Também cria um campo info_complementar que combina unidade gestora, modalidade e número de contrato/licitação. + columns: + - name: contrato_id + description: > + Identificador único do contrato. + - name: categoria + description: > + Categoria do contrato ( Informática, Serviços, Mão de Obra). + - name: processo + description: > + Número do processo administrativo do contrato, formatado para remover caracteres não numéricos. + - name: cnpj_cpf + description: > + CNPJ ou CPF do fornecedor, formatado para remover caracteres especiais como barras, pontos e hífens. + - name: info_complementar + description: > + Informação complementar construída a partir da unidade gestora, código de modalidade e número do contrato ou licitação. + - name: ne + description: > + Número da nota de empenho relacionada ao contrato, combinando informações de contratos, empenhos e faturas. diff --git a/airflow_lappis/dags/dbt/ipea/models/metadata/models_metadata.sql b/airflow_lappis/dags/dbt/ipea/models/metadata/models_metadata.sql new file mode 100644 index 00000000..f1981994 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/metadata/models_metadata.sql @@ -0,0 +1,67 @@ +{{ + config( + materialized='incremental', + unique_key=['schema_name', 'table_name'], + on_schema_change='sync_all_columns' + ) +}} + +{# + Tabela de Metadados dos Modelos dbt + =================================== + + Esta tabela armazena metadados de todos os modelos executados no dbt. + + Campos principais: + - schema_name: Schema do modelo + - table_name: Nome da tabela/modelo + - dt_transform: Data da última transformação (quando o modelo foi executado) + - run_id: ID único da execução do dbt + + A tabela é atualizada de forma incremental, mantendo apenas o registro + mais recente para cada combinação de schema + table_name. +#} + +WITH dbt_models AS ( + {# + Usando a função graph do dbt para iterar sobre todos os modelos do projeto. + Isso garante que capturamos metadados de todos os modelos definidos. + #} + {% set models_data = [] %} + + {% for node in graph.nodes.values() %} + {% if node.resource_type == 'model' %} + {% do models_data.append({ + 'schema_name': node.schema, + 'table_name': node.name, + 'database_name': node.database, + 'materialization': node.config.materialized, + 'description': node.description | default('') | replace("'", "''") + }) %} + {% endif %} + {% endfor %} + + {% for model in models_data %} + SELECT + '{{ model.schema_name }}' AS schema_name, + '{{ model.table_name }}' AS table_name, + '{{ model.database_name }}' AS database_name, + '{{ model.materialization }}' AS materialization, + '{{ model.description[:500] }}' AS description, + ('{{ run_started_at }}'::TIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE 'America/Sao_Paulo') AS dt_transform, + '{{ invocation_id }}' AS run_id + {% if not loop.last %} + UNION ALL + {% endif %} + {% endfor %} +) + +SELECT + schema_name, + table_name, + database_name, + materialization, + description, + dt_transform, + run_id +FROM dbt_models diff --git a/airflow_lappis/dags/dbt/ipea/models/metadata/schema.yml b/airflow_lappis/dags/dbt/ipea/models/metadata/schema.yml new file mode 100644 index 00000000..e4fce0ab --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/metadata/schema.yml @@ -0,0 +1,46 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dbt-labs/dbt-jsonschema/main/schemas/latest/dbt_yml_files-latest.json + +version: 2 + +models: + - name: models_metadata + description: > + Tabela central de metadados que armazena informações sobre todos os modelos dbt executados. + Cada linha representa um modelo único, identificado pela combinação de schema e table_name. + A tabela é atualizada de forma incremental, mantendo histórico das execuções. + meta: + tags: + - metadata + - governance + columns: + - name: schema_name + description: Nome do schema onde o modelo está localizado. + tests: + - not_null + + - name: table_name + description: Nome da tabela/modelo. + tests: + - not_null + + - name: database_name + description: Nome do banco de dados onde o modelo está materializado. + + - name: materialization + description: Tipo de materialização do modelo (table, view, incremental, etc). + + - name: description + description: Descrição do modelo extraída do schema.yml. + + - name: dt_transform + description: > + Data e hora em que o modelo foi transformado/executado pela última vez. + Corresponde ao momento em que a execução do dbt foi iniciada (run_started_at). + Timezone: America/Sao_Paulo (UTC-3). + tests: + - not_null + + - name: run_id + description: > + Identificador único da execução do dbt (invocation_id). + Permite rastrear qual execução gerou a transformação. diff --git a/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/bronze/schema.yml b/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/bronze/schema.yml new file mode 100644 index 00000000..c498db75 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/bronze/schema.yml @@ -0,0 +1,14 @@ +version: 2 + +models: + + # Orçamento DBT + + ## Bronze + - name: visao_orcamentaria_total + description: > + Esta tabela contém a visão consolidada da execução orçamentária total, + apresentando informações agregadas sobre os recursos disponíveis, empenhados, liquidados e pagos. + meta: + tags: + - bronze diff --git a/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/bronze/visao_orcamentaria_total.sql b/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/bronze/visao_orcamentaria_total.sql new file mode 100644 index 00000000..cb60d080 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/bronze/visao_orcamentaria_total.sql @@ -0,0 +1,47 @@ +with + source_data as (select * from {{ source("siafi", "visao_orcamentaria_total") }}), + + typed_data as ( + select + ano_exercicio, + unidade_orcamentaria, + unidade_orcamentaria_desc, + acao_governo, + acao_governo_desc, + programa_governo, + programa_governo_desc, + unidade_plano_orcamentario, + plano_orcamentario_1, + plano_orcamentario_2, + programa_plano_orcamentario, + acao_plano_orcamentario, + plano_orcamentario_6, + plano_orcamentario_desc, + elemento_despesa, + elemento_despesa_desc, + orgao_uge, + orgao_uge_desc, + uge_matriz_filial, + ug_executora, + ug_executora_desc, + + -- Campos financeiros/monetários + {{ parse_financial_value("projeto_inicial_loa") }} as projeto_inicial_loa, + {{ parse_financial_value("dotacao_inicial") }} as dotacao_inicial, + {{ parse_financial_value("dotacao_atualizada") }} as dotacao_atualizada, + {{ parse_financial_value("credito_disponivel") }} as credito_disponivel, + {{ parse_financial_value("despesas_empenhadas") }} as despesas_empenhadas, + {{ parse_financial_value("despesas_a_liquidar") }} as despesas_a_liquidar, + {{ parse_financial_value("despesar_a_pagar") }} as despesar_a_pagar, + {{ parse_financial_value("despesas_pagas") }} as despesas_pagas, + {{ parse_financial_value("restos_a_pagar_inscritos") }} + as restos_a_pagar_inscritos, + {{ parse_financial_value("restos_a_pagar_pagos") }} as restos_a_pagar_pagos, + + (dt_ingest || '-03:00')::timestamptz as dt_ingest + + from source_data + ) + +select * +from typed_data diff --git a/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/silver/categoria_gastos_orcamento_total_.sql b/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/silver/categoria_gastos_orcamento_total_.sql new file mode 100644 index 00000000..46069f5c --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/silver/categoria_gastos_orcamento_total_.sql @@ -0,0 +1,54 @@ +WITH categorias_gastos AS ( + select + ano_exercicio, + acao_governo, + acao_governo_desc, + elemento_despesa, + elemento_despesa_desc, + COALESCE(dotacao_atualizada, 0) as valor, + 'Dotação' as categoria, + dt_ingest + from {{ ref('visao_orcamentaria_total') }} + + union all + + select + ano_exercicio, + acao_governo, + acao_governo_desc, + elemento_despesa, + elemento_despesa_desc, + COALESCE(despesas_empenhadas, 0) as valor, + 'Orçamento alocado (empenhado)' as categoria, + dt_ingest + from {{ ref('visao_orcamentaria_total') }} + + union all + + select + ano_exercicio, + acao_governo, + acao_governo_desc, + elemento_despesa, + elemento_despesa_desc, + COALESCE(despesar_a_pagar, 0) as valor, + 'Despesas programadas' as categoria, + dt_ingest + from {{ ref('visao_orcamentaria_total') }} + + union all + + select + ano_exercicio, + acao_governo, + acao_governo_desc, + elemento_despesa, + elemento_despesa_desc, + COALESCE(despesas_pagas, 0) as valor, + 'Despesas pagas' as categoria, + dt_ingest + from {{ ref('visao_orcamentaria_total') }} +) + +select * from categorias_gastos +order by valor desc diff --git a/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/silver/orcamento_total_.sql b/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/silver/orcamento_total_.sql new file mode 100644 index 00000000..3d5744df --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/silver/orcamento_total_.sql @@ -0,0 +1,31 @@ +WITH orcamento_teds AS ( + select + SUM(credito_disponivel) + SUM(despesas_empenhadas) as orcamento, + ano_exercicio, + max(dt_ingest) as dt_ingest + from {{ ref('visao_orcamentaria_total') }} + where unidade_orcamentaria not in ('25300', '47204') + group by ano_exercicio +), + +orcamento AS ( + select + SUM(dotacao_atualizada) as orcamento, + ano_exercicio, + max(dt_ingest) as dt_ingest + from {{ ref('visao_orcamentaria_total') }} + group by ano_exercicio +), + +orcamento_total AS ( + select * from orcamento_teds + union + select * from orcamento +) + +select + ano_exercicio, + sum(orcamento) as orcamento, + max(dt_ingest) as dt_ingest +from orcamento_total +group by ano_exercicio diff --git a/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/silver/schema.yml b/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/silver/schema.yml new file mode 100644 index 00000000..fd9bf7dd --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/orcamento_dbt/silver/schema.yml @@ -0,0 +1,50 @@ +version: 2 + +models: + + - name: categoria_gasto_orcamento_total + description: > + Tabela que transforma a visão orçamentária em categorias de gastos para utilização em gráficos de barras empilhadas. + As categorias são: Dotação, Orçamento alocado (empenhado), Despesas programadas e Despesas pagas. + Os dados são derivados do modelo `visao_orcamentaria_total`. + meta: + tags: + - silver + columns: + - name: ano_exercicio + description: Ano do exercício orçamentário. + - name: acao_governo + description: Código da ação de governo. + - name: acao_governo_desc + description: Descrição da ação de governo. + - name: elemento_despesa + description: Código do elemento de despesa. + - name: elemento_despesa_desc + description: Descrição do elemento de despesa. + - name: valor + description: Valor numérico associado à categoria (com COALESCE 0 para nulos). + - name: categoria + description: > + Categoria do gasto para agrupamento no gráfico. + Valores possíveis: 'Dotação', 'Orçamento alocado (empenhado)', 'Despesas programadas', 'Despesas pagas'. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: orcamento_total + description: > + Tabela que consolida o orçamento total por ano de exercício, combinando duas visões: + (1) orçamento das TEDs, calculado como crédito disponível + despesas empenhadas excluindo unidades específicas (25300, 47204); + (2) orçamento geral, baseado na dotação atualizada de todas as unidades. + O resultado final agrega os valores de ambas as visões por ano. + meta: + tags: + - silver + columns: + - name: ano_exercicio + description: Ano do exercício orçamentário. + - name: orcamento + description: Soma do orçamento total (TEDs + geral) para o ano de exercício. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/afastamento_historico.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/afastamento_historico.sql new file mode 100644 index 00000000..0bf04e39 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/afastamento_historico.sql @@ -0,0 +1,133 @@ +with + afastamento_historico_raw as ( + select + adiantamentosalarioferias, + anoexercicio, + datafim, + datafimaquisicao, + dataini, + datainicioaquisicao, + datainicioferiasinterrompidas, + diasrestantes, + gratificacaonatalina, + numerodaparcela, + parcelacontinuacaointerrupcao, + parcelainterrompida, + qtdedias, + cpf, + coddiplomaafastamento, + codocorrencia, + datapublicacaoafastamento, + descdiplomaafastamento, + descocorrencia, + numerodiplomaafastamento, + dt_ingest + -- grmatricula não está presente + from {{ source("siape", "afastamento_historico") }} + ), + + afastamento_historico_cleaned as ( + -- tive que adicionar um nullif adicional pois nas colunas tinha escrito "NaN" + -- como string, diferentemente das outras tabelas + select + nullif( + nullif(nullif(trim(adiantamentosalarioferias), ''), 'NaN'), '[null]' + ) as adiantamentosalarioferias_clean, + nullif( + nullif(nullif(trim(anoexercicio), ''), 'NaN'), '[null]' + ) as anoexercicio_clean, + nullif(nullif(nullif(trim(datafim), ''), 'NaN'), '[null]') as datafim_clean, + nullif( + nullif(nullif(trim(datafimaquisicao), ''), 'NaN'), '[null]' + ) as datafimaquisicao_clean, + nullif(nullif(nullif(trim(dataini), ''), 'NaN'), '[null]') as dataini_clean, + nullif( + nullif(nullif(trim(datainicioaquisicao), ''), 'NaN'), '[null]' + ) as datainicioaquisicao_clean, + nullif( + nullif(nullif(trim(datainicioferiasinterrompidas), ''), 'NaN'), '[null]' + ) as datainicioferiasinterrompidas_clean, + nullif( + nullif(nullif(trim(diasrestantes), ''), 'NaN'), '[null]' + ) as diasrestantes_clean, + nullif( + nullif(nullif(trim(gratificacaonatalina), ''), 'NaN'), '[null]' + ) as gratificacaonatalina_clean, + nullif( + nullif(nullif(trim(numerodaparcela), ''), 'NaN'), '[null]' + ) as numerodaparcela_clean, + nullif( + nullif(nullif(trim(parcelacontinuacaointerrupcao), ''), 'NaN'), '[null]' + ) as parcelacontinuacaointerrupcao_clean, + nullif( + nullif(nullif(trim(parcelainterrompida), ''), 'NaN'), '[null]' + ) as parcelainterrompida_clean, + nullif(nullif(nullif(trim(qtdedias), ''), 'NaN'), '[null]') as qtdedias_clean, + nullif(nullif(nullif(trim(cpf), ''), 'NaN'), '[null]') as cpf_clean, + nullif( + nullif(nullif(trim(coddiplomaafastamento), ''), 'NaN'), '[null]' + ) as coddiplomaafastamento_clean, + nullif( + nullif(nullif(trim(codocorrencia), ''), 'NaN'), '[null]' + ) as codocorrencia_clean, + nullif( + nullif(nullif(trim(datapublicacaoafastamento), ''), 'NaN'), '[null]' + ) as datapublicacaoafastamento_clean, + nullif( + nullif(nullif(trim(descdiplomaafastamento), ''), 'NaN'), '[null]' + ) as descdiplomaafastamento_clean, + nullif( + nullif(nullif(trim(descocorrencia), ''), 'NaN'), '[null]' + ) as descocorrencia_clean, + nullif( + nullif(nullif(trim(numerodiplomaafastamento), ''), 'NaN'), '[null]' + ) as numerodiplomaafastamento_clean, + dt_ingest + from afastamento_historico_raw + ) + +select + null as gr_matricula, -- placeholder para matrícula, pois aqui na tabela histórica não tem ... + adiantamentosalarioferias_clean as adiantamento_salario_ferias, + anoexercicio_clean as ano_exercicio, + -- mesma logica dos dados_afastamento, garantindo os comprimentos corretos + case + when length(datafim_clean) = 8 then to_date(datafim_clean, 'DDMMYYYY') else null + end as dt_fim, + case + when length(datafimaquisicao_clean) = 8 + then to_date(datafimaquisicao_clean, 'DDMMYYYY') + else null + end as dt_fim_aquisicao, + case + when length(dataini_clean) = 8 then to_date(dataini_clean, 'DDMMYYYY') else null + end as dt_ini, + case + when length(datainicioaquisicao_clean) = 8 + then to_date(datainicioaquisicao_clean, 'DDMMYYYY') + else null + end as dt_inicio_aquisicao, + case + when length(datainicioferiasinterrompidas_clean) = 8 + then to_date(datainicioferiasinterrompidas_clean, 'DDMMYYYY') + else null + end as dt_inicio_ferias_interrompidas, + cast(diasrestantes_clean as int) as dias_restantes, + gratificacaonatalina_clean as gratificacao_natalina, + cast(numerodaparcela_clean as int) as numero_parcela, + parcelacontinuacaointerrupcao_clean as parcela_continuacao_interrupcao, + parcelainterrompida_clean as parcela_interrompida, + cast(qtdedias_clean as int) as qtde_dias, + regexp_replace(cpf_clean, '[^0-9]', '', 'g') as cpf, + coddiplomaafastamento_clean as cod_diploma_afastamento, + codocorrencia_clean as cod_ocorrencia, + case + when length(datapublicacaoafastamento_clean) = 8 + then to_date(datapublicacaoafastamento_clean, 'YYYYMMDD') -- Formato YYYYMMDD + else null + end as dt_publicacao_afastamento, + descdiplomaafastamento_clean as desc_diploma_afastamento, + descocorrencia_clean as desc_ocorrencia, + cast(numerodiplomaafastamento_clean as int) as numero_diploma_afastamento, + (dt_ingest || '-03:00')::timestamptz as dt_ingest +from afastamento_historico_cleaned diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/cargos_funcoes.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/cargos_funcoes.sql new file mode 100644 index 00000000..21463d1b --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/cargos_funcoes.sql @@ -0,0 +1,61 @@ +with + fonte as ( + select + codigotipo, + nome, + sigla, + codigocargofuncao, + categoria, + nivel, + regraautoridade, + atonormativo__tipoato, + atonormativo__codigounidade, + atonormativo__numero, + atonormativo__dataassinatura, + atonormativo__datapublicacao, + atonormativo__datavigencia, + atonormativo__ementa, + atonormativo__url, + atonormativo__codigotipo, + atonormativo__siglatipo, + denominacoes__denominacao, + dt_ingest + from {{ source("siorg", "cargos_funcao") }} + ), + + denominacoes_expandidas as ( + select + f.*, + denominacao_elem ->> 'codigo' as denominacao_codigo, + denominacao_elem ->> 'descricao' as denominacao_descricao + from + fonte f, + lateral jsonb_array_elements( + replace( + replace(f.denominacoes__denominacao, '''', '"'), 'None', 'null' + )::jsonb + ) as denominacao_elem + ) + +select + codigotipo, + nome, + sigla, + codigocargofuncao, + categoria, + nivel, + regraautoridade, + atonormativo__tipoato, + atonormativo__codigounidade, + atonormativo__numero, + atonormativo__dataassinatura, + atonormativo__datapublicacao, + atonormativo__datavigencia, + atonormativo__ementa, + atonormativo__url, + atonormativo__codigotipo, + atonormativo__siglatipo, + denominacao_codigo, + denominacao_descricao, + (dt_ingest || '-03:00')::timestamptz as dt_ingest +from denominacoes_expandidas diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_afastamento.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_afastamento.sql new file mode 100644 index 00000000..66bf946a --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_afastamento.sql @@ -0,0 +1,109 @@ +with + dados_afastamento_raw as ( + select + adiantamentosalarioferias, + anoexercicio, + datafim, + datafimaquisicao, + dataini, + datainicioaquisicao, + gratificacaonatalina, + numerodaparcela, + parcelacontinuacaointerrupcao, + parcelainterrompida, + qtdedias, + grmatricula, + cpf, + coddiplomaafastamento, + codocorrencia, + datapublicacaoafastamento, + descdiplomaafastamento, + descocorrencia, + numerodiplomaafastamento, + datainicioferiasinterrompidas, + diasrestantes, + dt_ingest + from {{ source("siape", "dados_afastamento") }} + ), + + dados_afastamento_cleaned as ( + select + nullif( + trim(adiantamentosalarioferias), '' + ) as adiantamentosalarioferias_clean, + nullif(trim(anoexercicio), '') as anoexercicio_clean, + nullif(trim(datafim), '') as datafim_clean, + nullif(trim(datafimaquisicao), '') as datafimaquisicao_clean, + nullif(trim(dataini), '') as dataini_clean, + nullif(trim(datainicioaquisicao), '') as datainicioaquisicao_clean, + nullif(trim(gratificacaonatalina), '') as gratificacaonatalina_clean, + nullif(trim(numerodaparcela), '') as numerodaparcela_clean, + nullif( + trim(parcelacontinuacaointerrupcao), '' + ) as parcelacontinuacaointerrupcao_clean, + nullif(trim(parcelainterrompida), '') as parcelainterrompida_clean, + nullif(trim(qtdedias), '') as qtdedias_clean, + nullif(trim(grmatricula), '') as grmatricula_clean, + nullif(trim(cpf), '') as cpf_clean, + nullif(trim(coddiplomaafastamento), '') as coddiplomaafastamento_clean, + nullif(trim(codocorrencia), '') as codocorrencia_clean, + nullif( + trim(datapublicacaoafastamento), '' + ) as datapublicacaoafastamento_clean, + nullif(trim(descdiplomaafastamento), '') as descdiplomaafastamento_clean, + nullif(trim(descocorrencia), '') as descocorrencia_clean, + nullif(trim(numerodiplomaafastamento), '') as numerodiplomaafastamento_clean, + nullif( + trim(datainicioferiasinterrompidas), '' + ) as datainicioferiasinterrompidas_clean, + nullif(trim(diasrestantes), '') as diasrestantes_clean, + dt_ingest + from dados_afastamento_raw + ) + +select + adiantamentosalarioferias_clean as adiantamento_salario_ferias, + anoexercicio_clean as ano_exercicio, + -- adicionei esses checks pois tinham algumas strings de datas retornando "0" e + -- quebrando o to_date ... + case + when length(datafim_clean) = 8 then to_date(datafim_clean, 'DDMMYYYY') else null + end as dt_fim, + case + when length(datafimaquisicao_clean) = 8 + then to_date(datafimaquisicao_clean, 'DDMMYYYY') + else null + end as dt_fim_aquisicao, + case + when length(dataini_clean) = 8 then to_date(dataini_clean, 'DDMMYYYY') else null + end as dt_ini, + case + when length(datainicioaquisicao_clean) = 8 + then to_date(datainicioaquisicao_clean, 'DDMMYYYY') + else null + end as dt_inicio_aquisicao, + gratificacaonatalina_clean as gratificacao_natalina, + cast(numerodaparcela_clean as int) as numero_parcela, + parcelacontinuacaointerrupcao_clean as parcela_continuacao_interrupcao, + parcelainterrompida_clean as parcela_interrompida, + cast(qtdedias_clean as int) as qtde_dias, + grmatricula_clean as gr_matricula, + regexp_replace(cpf_clean, '[^0-9]', '', 'g') as cpf, + coddiplomaafastamento_clean as cod_diploma_afastamento, + codocorrencia_clean as cod_ocorrencia, + case + when length(datapublicacaoafastamento_clean) = 8 + then to_date(datapublicacaoafastamento_clean, 'YYYYMMDD') -- Essa veio diferente, n sei o pq + else null + end as dt_publicacao_afastamento, + descdiplomaafastamento_clean as desc_diploma_afastamento, + descocorrencia_clean as desc_ocorrencia, + cast(numerodiplomaafastamento_clean as int) as numero_diploma_afastamento, + case + when length(datainicioferiasinterrompidas_clean) = 8 + then to_date(datainicioferiasinterrompidas_clean, 'DDMMYYYY') + else null + end as dt_inicio_ferias_interrompidas, + cast(diasrestantes_clean as int) as dias_restantes, + (dt_ingest || '-03:00')::timestamptz as dt_ingest +from dados_afastamento_cleaned diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_curriculo.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_curriculo.sql new file mode 100644 index 00000000..c613711f --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_curriculo.sql @@ -0,0 +1,45 @@ +with + dados_curriculo_raw as ( + select + cpf, + identificunica, + codigo, + codcurso, + nomecurso, + dataconclusao, + instituicao, + nome, + cargahoraria, + cargo, + datainicio, + nomeorgaoempresa, + datafim, + projeto, + infoadicionais, + tipodesc, + dt_ingest + from {{ source("siape", "dados_curriculo") }} + ) + +select + regexp_replace(nullif(trim(cpf), ''), '[^0-9]', '', 'g') as cpf, + nullif(trim(identificunica), '') as ident_unica, + nullif(trim(codigo), '') as codigo_experiencia, + nullif(trim(codcurso), '') as cod_curso, + nullif(trim(nomecurso), '') as nome_curso, + -- Converte YYYYMM para DATE (1º dia do mês) + to_date(nullif(trim(dataconclusao), '') || '01', 'YYYYMMDD') as dt_mes_conclusao, + nullif(trim(instituicao), '') as nome_instituicao, + nullif(trim(nome), '') as nome_area_experiencia, + nullif(trim(cargahoraria), '') as carga_horaria, -- Mantido como varchar para segurança + nullif(trim(cargo), '') as nome_cargo, + -- Converte YYYYMM para DATE (1º dia do mês) + to_date(nullif(trim(datainicio), '') || '01', 'YYYYMMDD') as dt_mes_inicio, + nullif(trim(nomeorgaoempresa), '') as nome_orgao_empresa, + -- Converte YYYYMM para DATE (1º dia do mês) + to_date(nullif(trim(datafim), '') || '01', 'YYYYMMDD') as dt_mes_fim, + nullif(trim(projeto), '') as descricao_projeto, + nullif(trim(infoadicionais), '') as informacoes_adicionais, + nullif(trim(tipodesc), '') as tipo_descricao, + (dt_ingest || '-03:00')::timestamptz as dt_ingest +from dados_curriculo_raw diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_dependentes.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_dependentes.sql new file mode 100644 index 00000000..d34e7443 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_dependentes.sql @@ -0,0 +1,53 @@ +with + dados_dependentes_raw as ( + select + codcondicao, + codgrauparentesco, + codorgao, + cpf, + matricula, + nome, + nomecondicao, + nomegrauparentesco, + codbeneficio, + datafim, + datainicio, + nomebeneficio, + dt_ingest + from {{ source("siape", "dados_dependentes") }} + ), + + dados_dependentes_cleaned as ( + select + nullif(trim(codcondicao), 'NaN') as codcondicao, + nullif(trim(codgrauparentesco), 'NaN') as codgrauparentesco, + nullif(trim(codorgao), 'NaN') as codorgao, + nullif(trim(cpf), 'NaN') as cpf, + nullif(trim(matricula), 'NaN') as matricula, + nullif(trim(nome), 'NaN') as nome, + nullif(trim(nomecondicao), 'NaN') as nomecondicao, + nullif(trim(nomegrauparentesco), 'NaN') as nomegrauparentesco, + nullif(trim(codbeneficio), 'NaN') as codbeneficio, + nullif(trim(datafim), 'NaN') as datafim, + nullif(trim(datainicio), 'NaN') as datainicio, + nullif(trim(nomebeneficio), 'NaN') as nomebeneficio, + dt_ingest + from dados_dependentes_raw + ) + +select + nullif(codcondicao, '') as cod_condicao, + nullif(codgrauparentesco, '') as cod_grau_parentesco, + nullif(codorgao, '') as cod_orgao, + regexp_replace(nullif(cpf, ''), '[^0-9]', '', 'g') as cpf, + nullif(matricula, '') as matricula, + nullif(nome, '') as nome_dependente, + nullif(nomecondicao, '') as nome_condicao, + nullif(nomegrauparentesco, '') as nome_grau_parentesco, + nullif(codbeneficio, '') as cod_beneficio, + -- Converte para DATE, tratando '', 'NaN' e '00000000' como NULL + to_date(nullif(nullif(datafim, ''), '00000000'), 'DDMMYYYY') as dt_fim, + to_date(nullif(nullif(datainicio, ''), '00000000'), 'DDMMYYYY') as dt_inicio, + nullif(nomebeneficio, '') as nome_beneficio, + (dt_ingest || '-03:00')::timestamptz as dt_ingest +from dados_dependentes_cleaned diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_escolares.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_escolares.sql new file mode 100644 index 00000000..eb282a35 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_escolares.sql @@ -0,0 +1,28 @@ +with + dados_escolares_raw as ( + select + codcurso, + nomecurso, + codmatricula, + codorgao, + codtitulacao, + nometitulacao, + codescolaridade, + nomeescolaridade, + cpf, + dt_ingest + from {{ source("siape", "dados_escolares") }} + ) + +select + nullif(trim(codcurso), '') as cod_curso, + nullif(trim(nomecurso), '') as nome_curso, + nullif(trim(codmatricula), '') as cod_matricula, + nullif(trim(codorgao), '') as cod_orgao, + nullif(trim(codtitulacao), '') as cod_titulacao, + nullif(trim(nometitulacao), '') as nome_titulacao, + nullif(trim(codescolaridade), '') as cod_escolaridade, + nullif(trim(nomeescolaridade), '') as nome_escolaridade, + regexp_replace(nullif(trim(cpf), ''), '[^0-9]', '', 'g') as cpf, + (dt_ingest || '-03:00')::timestamptz as dt_ingest +from dados_escolares_raw diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_financeiros.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_financeiros.sql new file mode 100644 index 00000000..b1b6d980 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_financeiros.sql @@ -0,0 +1,127 @@ +with + dados_financeiros_raw as ( + select + codrubrica, + indicadorrd, + nomerubrica, + numeroseq, + valorrubrica, + dataanomesrubrica, + pzrubrica, + mesanopagamento, + cpf, + indicadormovsupl, + perubrica, + dt_ingest + from {{ source("siape", "dados_financeiros") }} + ), + + dados_financeiros_cleaned as ( + select + nullif(trim(codrubrica), '') as cod_rubrica, + nullif(trim(indicadorrd), '') as indicador_rd, + nullif(trim(nomerubrica), '') as nome_rubrica, + nullif(trim(numeroseq), '') as numero_sequencia, + nullif(trim(valorrubrica), '') as valor_rubrica_str, -- Mantém como string + nullif(trim(dataanomesrubrica), '') as data_anomes_rubrica_str, + nullif(trim(pzrubrica), '') as prazo_rubrica, + nullif(trim(mesanopagamento), '') as mes_ano_pagamento_str, + nullif(trim(cpf), '') as cpf_str, + nullif(trim(indicadormovsupl), '') as indicador_mov_supl, + nullif(trim(perubrica), '') as periodo_rubrica, + dt_ingest + from dados_financeiros_raw + ), + + conversao_mes as ( + select + *, + case + upper(substring(data_anomes_rubrica_str, 1, 3)) + when 'JAN' + then '01' + when 'FEV' + then '02' + when 'MAR' + then '03' + when 'ABR' + then '04' + when 'MAI' + then '05' + when 'JUN' + then '06' + when 'JUL' + then '07' + when 'AGO' + then '08' + when 'SET' + then '09' + when 'OUT' + then '10' + when 'NOV' + then '11' + when 'DEZ' + then '12' + else null + end as mes_num_rubrica, + substring(data_anomes_rubrica_str, 4, 4) as ano_rubrica, + case + upper(substring(mes_ano_pagamento_str, 1, 3)) + when 'JAN' + then '01' + when 'FEV' + then '02' + when 'MAR' + then '03' + when 'ABR' + then '04' + when 'MAI' + then '05' + when 'JUN' + then '06' + when 'JUL' + then '07' + when 'AGO' + then '08' + when 'SET' + then '09' + when 'OUT' + then '10' + when 'NOV' + then '11' + when 'DEZ' + then '12' + else null + end as mes_num_pagamento, + substring(mes_ano_pagamento_str, 4, 4) as ano_pagamento, + max(dt_ingest) over () dt_ingest_max + from dados_financeiros_cleaned + ) + +select + cod_rubrica, + indicador_rd, + nome_rubrica, + numero_sequencia, + -- Limpa (remove '.') e converte (',' para '.') para NUMERIC + cast( + replace( + replace(valor_rubrica_str, '.', ''), -- Remove o separador de milhares '.' + ',', + '.' -- Troca a vírgula decimal ',' por '.' + ) as numeric + ) as valor_rubrica, + -- Converte MONYYYY para DATE (primeiro dia do mês) + to_date( + ano_rubrica || '-' || mes_num_rubrica || '-01', 'YYYY-MM-DD' + ) as data_anomes_rubrica, + prazo_rubrica, + -- Converte MONYYYY para DATE (primeiro dia do mês) + to_date( + ano_pagamento || '-' || mes_num_pagamento || '-01', 'YYYY-MM-DD' + ) as mes_ano_pagamento, + regexp_replace(cpf_str, '[^0-9]', '', 'g') as cpf, + indicador_mov_supl, + periodo_rubrica, + (dt_ingest_max || '-03:00')::timestamptz as dt_ingest +from conversao_mes diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_funcionais.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_funcionais.sql new file mode 100644 index 00000000..9608fd58 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_funcionais.sql @@ -0,0 +1,87 @@ +with dados_funcionais_raw as (select * from {{ source("siape", "dados_funcionais") }}) + +select + nullif(trim(codativfun), '') as cod_atividade_funcao, + nullif(trim(codfuncao), '') as cod_funcao, + nullif(trim(codjornada), '') as cod_jornada, + nullif(trim(codocorringressoorgao), '') as cod_ocorr_ingresso_orgao, + nullif(trim(codocorringressoservpublico), '') as cod_ocorr_ingresso_serv_publico, + nullif(trim(codorgao), '') as cod_orgao, + nullif(trim(codpadrao), '') as cod_padrao, + nullif(trim(codsitfuncional), '') as cod_situacao_funcional, + nullif(trim(coduorgexercicio), '') as cod_uorg_exercicio, + nullif(trim(codupag), '') as cod_upag, + nullif(trim(codigoorgaoorigem), '') as cod_orgao_origem, + regexp_replace( + nullif(trim(cpfchefiaimediata), ''), '[^0-9]', '', 'g' + ) as cpf_chefia_imediata, + to_date(nullif(trim(dataexercicionoorgao), ''), 'DDMMYYYY') as dt_exercicio_no_orgao, + to_date(nullif(trim(datafimvalear), ''), 'DDMMYYYY') as dt_fim_vale_ar, + to_date(nullif(trim(dataingressofuncao), ''), 'DDMMYYYY') as dt_ingresso_funcao, + to_date( + nullif(trim(dataocorringressoorgao), ''), 'DDMMYYYY' + ) as dt_ocorr_ingresso_orgao, + to_date( + nullif(trim(dataocorringressoservpublico), ''), 'DDMMYYYY' + ) as dt_ocorr_ingresso_serv_publico, + lower(nullif(trim(emailchefiaimediata), '')) as email_chefia_imediata, + lower(nullif(trim(emailinstitucional), '')) as email_institucional, + lower(nullif(trim(emailservidor), '')) as email_servidor, + nullif(trim(identunica), '') as ident_unica, + nullif(trim(matriculasiape), '') as matricula_siape, + nullif(trim(modalidadepgd), '') as modalidade_pgd, + nullif(trim(nomeativfun), '') as nome_atividade_funcao, + nullif(trim(nomechefeuorg), '') as nome_chefe_uorg, + nullif(trim(nomefuncao), '') as nome_funcao, + nullif(trim(nomejornada), '') as nome_jornada, + nullif(trim(nomeocorringressoorgao), '') as nome_ocorr_ingresso_orgao, + nullif(trim(nomeocorringressoservpublico), '') as nome_ocorr_ingresso_serv_publico, + nullif(trim(nomeorgao), '') as nome_orgao, + nullif(trim(nomeregimejuridico), '') as nome_regime_juridico, + nullif(trim(nomesitfuncional), '') as nome_situacao_funcional, + nullif(trim(nomeuorgexercicio), '') as nome_uorg_exercicio, + nullif(trim(nomeupag), '') as nome_upag, + nullif(trim(participapgd), '') as participa_pgd, + cast(replace(nullif(trim(percentualts), ''), ',', '.') as numeric) + / 100 as percentual_ts, + nullif(trim(siglaorgao), '') as sigla_orgao, + nullif(trim(siglaorgaoorigem), '') as sigla_orgao_origem, + nullif(trim(siglaregimejuridico), '') as sigla_regime_juridico, + nullif(trim(siglauorgexercicio), '') as sigla_uorg_exercicio, + nullif(trim(siglaupag), '') as sigla_upag, + regexp_replace(nullif(trim(cpf), ''), '[^0-9]', '', 'g') as cpf, + nullif(trim(codcargo), '') as cod_cargo, + nullif(trim(codclasse), '') as cod_classe, + nullif(trim(codocorraposentadoria), '') as cod_ocorr_aposentadoria, + to_date(nullif(trim(datainivalear), ''), 'DDMMYYYY') as dt_ini_vale_ar, + to_date( + nullif(trim(dataocorraposentadoria), ''), 'DDMMYYYY' + ) as dt_ocorr_aposentadoria, + nullif(trim(nomecargo), '') as nome_cargo, + nullif(trim(nomeclasse), '') as nome_classe, + nullif(trim(nomeocorraposentadoria), '') as nome_ocorr_aposentadoria, + nullif(trim(siglanivelcargo), '') as sigla_nivel_cargo, + nullif(trim(tipovalear), '') as tipo_vale_ar, + nullif(trim(codocorrisencaoir), '') as cod_ocorr_isencao_ir, + to_date( + nullif(trim(datainiocorrisencaoir), ''), 'DDMMYYYY' + ) as dt_ini_ocorr_isencao_ir, + nullif(trim(nomeocorrisencaoir), '') as nome_ocorr_isencao_ir, + nullif(trim(coduorglotacao), '') as cod_uorg_lotacao, + nullif(trim(nomeuorglotacao), '') as nome_uorg_lotacao, + nullif(trim(siglauorglotacao), '') as sigla_uorg_lotacao, + to_date( + nullif(trim(datafimocorrisencaoir), ''), 'DDMMYYYY' + ) as dt_fim_ocorr_isencao_ir, + nullif(trim(codocorrexclusao), '') as cod_ocorr_exclusao, + to_date(nullif(trim(dataocorrexclusao), ''), 'DDMMYYYY') as dt_ocorr_exclusao, + nullif(trim(nomeocorrexclusao), '') as nome_ocorr_exclusao, + to_date(nullif(trim(datauorglotacao), ''), 'DDMMYYYY') as dt_uorg_lotacao, + nullif(trim(codvaletransporte), '') as cod_vale_transporte, + cast( + replace(nullif(trim(valorvaletransporte), ''), ',', '.') as numeric + ) as valor_vale_transporte, + to_date(nullif(trim(datauorgexercicio), ''), 'DDMMYYYY') as dt_uorg_exercicio, + nullif(trim(pontuacaodesempenho), '') as pontuacao_desempenho, -- Mantido como varchar (Não da pra saber se é "A","B" ou é um número > tudo null) + (dt_ingest || '-03:00')::timestamptz as dt_ingest +from dados_funcionais_raw diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_pa.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_pa.sql new file mode 100644 index 00000000..c0c9e907 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_pa.sql @@ -0,0 +1,40 @@ +with + dados_pa as ( + select + agenciabeneficiario, + bancobeneficiario, + codorgao, + contabeneficiario, + cpfbeneficiario, + matricula, + nomebeneficiario, + valorultimapensao, + cpf_servidor, + codvinculoservidor, + nomealimentado, + nomevinculoservidor, + dt_ingest + from {{ source("siape", "dados_pa") }} + ) + +select + regexp_replace( + nullif(trim(agenciabeneficiario), ''), '[^0-9]', '', 'g' + ) as agencia_beneficiario, + nullif(trim(bancobeneficiario), '') as banco_beneficiario, + nullif(trim(codorgao), '') as cod_orgao, + upper( + regexp_replace(nullif(trim(contabeneficiario), ''), '[^0-9A-Za-z]', '', 'g') + ) as conta_beneficiario, + regexp_replace( + nullif(trim(cpfbeneficiario), ''), '[^0-9]', '', 'g' + ) as cpf_beneficiario, + nullif(trim(matricula), '') as matricula_servidor, + nullif(trim(nomebeneficiario), '') as nome_beneficiario, + nullif(trim(valorultimapensao), '') as valor_ultima_pensao, + regexp_replace(nullif(trim(cpf_servidor), ''), '[^0-9]', '', 'g') as cpf_servidor, + nullif(trim(codvinculoservidor), '') as cod_vinculo_servidor, + nullif(trim(nomealimentado), '') as nome_alimentado, + nullif(trim(nomevinculoservidor), '') as nome_vinculo_servidor, + (dt_ingest || '-03:00')::timestamptz as dt_ingest +from dados_pa diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_pessoais.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_pessoais.sql new file mode 100644 index 00000000..3df21476 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_pessoais.sql @@ -0,0 +1,52 @@ +with + dados_pessoais as ( + select + codcor, + codestadocivil, + codnacionalidade, + codsexo, + datanascimento, + gruposanguineo, + nome, + nomecor, + nomeestadocivil, + nomemae, + nomemunicipnasc, + nomenacionalidade, + nomepai, + nomesexo, + numpispasep, + ufnascimento, + cpf, + coddeffisica, + nomedeffisica, + datachegbrasil, + nomepais, + dt_ingest + from {{ source("siape", "dados_pessoais") }} + ) + +select + nullif(trim(codcor), '') as cod_cor, + nullif(trim(codestadocivil), '') as cod_estado_civil, + nullif(trim(codnacionalidade), '') as cod_nacionalidade, + nullif(trim(codsexo), '') as cod_sexo, + to_date(nullif(trim(datanascimento), ''), 'DDMMYYYY') as dt_nascimento, + nullif(trim(gruposanguineo), '') as grupo_sanguineo, + nullif(trim(nome), '') as nome_pessoa, + nullif(trim(nomecor), '') as nome_cor, + nullif(trim(nomeestadocivil), '') as nome_estado_civil, + nullif(trim(nomemae), '') as nome_mae, + nullif(trim(nomemunicipnasc), '') as nome_municipio_nascimento, + nullif(trim(nomenacionalidade), '') as nome_nacionalidade, + nullif(nullif(trim(nomepai), ''), 'NAO DECLARADO') as nome_pai, + nullif(trim(nomesexo), '') as nome_sexo, + regexp_replace(nullif(trim(numpispasep), ''), '[^0-9]', '', 'g') as num_pispasep, + upper(nullif(trim(ufnascimento), '')) as uf_nascimento, + regexp_replace(nullif(trim(cpf), ''), '[^0-9]', '', 'g') as cpf, + nullif(trim(coddeffisica), '') as cod_deficiencia_fisica, + nullif(trim(nomedeffisica), '') as nome_deficiencia_fisica, + to_date(nullif(trim(datachegbrasil), ''), 'DDMMYYYY') as dt_chegada_brasil, + nullif(trim(nomepais), '') as nome_pais_origem, + (dt_ingest || '-03:00')::timestamptz as dt_ingest +from dados_pessoais diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_uorg.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_uorg.sql new file mode 100644 index 00000000..49020e90 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/dados_uorg.sql @@ -0,0 +1,46 @@ +with + dados_uorg as ( + select + bairrouorg, + cepuorg, + codmatricula, + codmunicipiouorg, + codorgao, + codorgaouorg, + emailuorg, + enduorg, + logradourouorg, + nomemunicipiouorg, + nomeuorg, + numtelefoneuorg, + numerouorg, + siglauorg, + ufuorg, + cpf, + complementouorg, + numfaxuorg, + dt_ingest + from {{ source("siape", "dados_uorg") }} + ) + +select + nullif(trim(bairrouorg), '') as bairro_uorg, + regexp_replace(nullif(trim(cepuorg), ''), '[^0-9]', '', 'g') as cep_uorg, + nullif(trim(codmatricula), '') as codigo_matricula, + nullif(trim(codmunicipiouorg), '') as codigo_municipio_uorg, + nullif(trim(codorgao), '') as codigo_orgao, + nullif(trim(codorgaouorg), '') as codigo_orgao_uorg, + lower(nullif(trim(emailuorg), '')) as email_uorg, + nullif(trim(enduorg), '') as tipo_endereco_uorg, + nullif(trim(logradourouorg), '') as logradouro_uorg, + nullif(trim(nomemunicipiouorg), '') as nome_municipio_uorg, + nullif(trim(nomeuorg), '') as nome_uorg, + regexp_replace(nullif(trim(numtelefoneuorg), ''), '[^0-9]', '', 'g') as telefone_uorg, + nullif(trim(numerouorg), '') as numero_endereco_uorg, + nullif(trim(siglauorg), '') as sigla_uorg, + upper(nullif(trim(ufuorg), '')) as uf_uorg, + regexp_replace(nullif(trim(cpf), ''), '[^0-9]', '', 'g') as cpf, + nullif(nullif(trim(complementouorg), ''), '---') as complemento_endereco_uorg, + regexp_replace(nullif(trim(numfaxuorg), ''), '[^0-9]', '', 'g') as fax_uorg, + (dt_ingest || '-03:00')::timestamptz as dt_ingest +from dados_uorg diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/estrutura_organizacional_cargos.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/estrutura_organizacional_cargos.sql new file mode 100644 index 00000000..d44b51dd --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/estrutura_organizacional_cargos.sql @@ -0,0 +1,78 @@ +with + fonte as ( + select + codigounidade, + nomeunidade, + siglaunidade, + municipio, + uf, + cargos, + ordem_grandeza, + dt_ingest + from {{ source("siorg", "estrutura_organizacional_cargos") }} + ), + + cargos_expandidos as ( + select + f.codigounidade, + f.nomeunidade, + f.siglaunidade, + f.municipio, + f.uf, + f.ordem_grandeza, + f.dt_ingest, + cargo_elem + from + fonte f, + lateral jsonb_array_elements( + replace(replace(f.cargos, '''', '"'), 'None', 'null')::jsonb + ) as cargo_elem + ), + + instancias_expandidas as ( + select + ce.codigounidade, + ce.nomeunidade, + ce.siglaunidade, + ce.municipio, + ce.uf, + ce.ordem_grandeza, + ce.dt_ingest, + cargo_elem ->> 'denominacao' as denominacao, + cargo_elem ->> 'funcao' as funcao, + instancia_elem ->> 'codigoInstancia' as codigo_instancia, + instancia_elem ->> 'nomeTitular' as nome_titular, + instancia_elem ->> 'cpfTitular' as cpf_titular + from + cargos_expandidos ce, + lateral jsonb_array_elements(cargo_elem -> 'instancias') as instancia_elem + ), + + instancias_filtradas as ( + select * + from + ( + select + *, + row_number() over ( + partition by codigo_instancia order by ordem_grandeza desc + ) as rn + from instancias_expandidas + ) t + where rn = 1 + ) + +select + codigounidade, + nomeunidade, + siglaunidade, + municipio, + uf, + denominacao, + funcao, + codigo_instancia, + nome_titular, + cpf_titular, + ordem_grandeza, + (dt_ingest || '-03:00')::timestamptz as dt_ingest +from instancias_filtradas diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/lista_servidores.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/lista_servidores.sql new file mode 100644 index 00000000..9ca75c25 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/lista_servidores.sql @@ -0,0 +1,8 @@ +with + lista_servidores as ( + select cpf, dataultimatransacao as dt_ultima_transacao, coduorg as cod_uorg, (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("siape", "lista_servidores") }} + ) + +select * +from lista_servidores diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/lista_uorgs.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/lista_uorgs.sql new file mode 100644 index 00000000..94954e79 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/lista_uorgs.sql @@ -0,0 +1,8 @@ +with + lista_uorgs as ( + select cast(codigo as int) as codigo, dt_ultima_transacao, nome, (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("siape", "lista_uorgs") }} + ) + +select * +from lista_uorgs diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/schema.yml b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/schema.yml new file mode 100644 index 00000000..c3b66dfc --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/schema.yml @@ -0,0 +1,906 @@ +version: 2 + +models: + + # Pessoas DBT + + ## Bronze + - name: afastamento_historico + description: > + Tabela bronze que armazena o histórico de afastamentos dos servidores. + Contém informações detalhadas sobre cada período de afastamento, incluindo datas, tipo, e amparo legal. + Os dados são limpos e padronizados a partir da fonte original. + meta: + tags: + - bronze + columns: + - name: adiantamento_salario_ferias + description: "Indica se houve adiantamento de salário durante as férias." + - name: ano_exercicio + description: "Ano de exercício a que o afastamento se refere." + - name: dt_fim + description: "Data de término do afastamento." + - name: dt_fim_aquisicao + description: "Data final do período aquisitivo de férias." + - name: dt_ini + description: "Data de início do afastamento." + - name: dt_inicio_aquisicao + description: "Data inicial do período aquisitivo de férias." + - name: dt_inicio_ferias_interrompidas + description: "Data de início de férias que foram interrompidas." + - name: dias_restantes + description: "Dias restantes de afastamento." + - name: gratificacao_natalina + description: "Indica se o afastamento está relacionado à gratificação natalina." + - name: numero_parcela + description: "Número da parcela do afastamento." + - name: parcela_continuacao_interrupcao + description: "Indica se a parcela é uma continuação ou interrupção." + - name: parcelainterrompida + description: "Indica se a parcela foi interrompida." + - name: qtde_dias + description: "Quantidade de dias do afastamento." + - name: cpf + description: "CPF do servidor." + - name: cod_diploma_afastamento + description: "Código do diploma legal do afastamento." + - name: cod_ocorrencia + description: "Código da ocorrência do afastamento." + - name: dt_publicacao_afastamento + description: "Data de publicação do afastamento." + - name: desc_diploma_afastamento + description: "Descrição do diploma legal do afastamento." + - name: desc_ocorrencia + description: "Descrição da ocorrência do afastamento." + - name: numero_diploma_afastamento + description: "Número do diploma legal do afastamento." + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + + - name: cargos_funcoes + description: > + Tabela bronze que representa as funções e cargos extraídos do sistema SIORG. + Ela contém informações sobre o código e nome do cargo/função, nível hierárquico, + categoria funcional, regras de autoridade, além de dados normativos (ato normativo) + e as diversas denominações associadas a cada cargo/função. + Os dados são expandidos a partir de arrays JSON para facilitar a análise. + meta: + tags: + - bronze + columns: + - name: codigotipo + description: "Código que representa o tipo do cargo ou função." + + - name: nome + description: "Nome completo do cargo ou função." + + - name: sigla + description: "Sigla identificadora do cargo ou função." + + - name: codigocargofuncao + description: "Código único que identifica o cargo ou função." + + - name: categoria + description: "Categoria funcional do cargo, como direção, assessoramento, etc." + + - name: nivel + description: "Nível hierárquico do cargo ou função." + + - name: atonormativo__tipoato + description: "Tipo do ato normativo que criou ou regulamenta o cargo/função." + + - name: atonormativo__codigounidade + description: "Código da unidade responsável pelo ato normativo." + + - name: atonormativo__numero + description: "Número do ato normativo relacionado ao cargo ou função." + + - name: atonormativo__dataassinatura + description: "Data em que o ato normativo foi assinado." + + - name: atonormativo__datapublicacao + description: "Data de publicação do ato normativo no diário oficial." + + - name: atonormativo__datavigencia + description: "Data em que o ato normativo passou a vigorar." + + - name: atonormativo__ementa + description: "Ementa ou resumo descritivo do ato normativo." + + - name: atonormativo__url + description: "URL de acesso ao conteúdo completo do ato normativo." + + - name: atonormativo__codigotipo + description: "Código que representa o tipo de ato normativo." + + - name: atonormativo__siglatipo + description: "Sigla que representa o tipo de ato normativo." + + - name: denominacao_codigo + description: "Código individual da denominação expandida a partir do array JSON." + + - name: denominacao_descricao + description: "Descrição da denominação expandida para o cargo/função." + + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + + - name: dados_afastamento + description: > + Tabela bronze com dados atuais de afastamentos dos servidores. + meta: + tags: + - bronze + columns: + - name: adiantamento_salario_ferias + description: "Indica se houve adiantamento de salário durante as férias." + - name: ano_exercicio + description: "Ano de exercício a que o afastamento se refere." + - name: cod_diploma_afastamento + description: "Código do diploma legal do afastamento." + - name: cod_ocorrencia + description: "Código da ocorrência do afastamento." + - name: desc_diploma_afastamento + description: "Descrição do diploma legal do afastamento." + - name: desc_ocorrencia + description: "Descrição da ocorrência do afastamento." + - name: dt_fim + description: "Data de término do afastamento." + - name: dt_ini + description: "Data de início do afastamento." + - name: dt_inicio_aquisicao + description: "Data inicial do período aquisitivo de férias." + - name: dt_publicacao_afastamento + description: "Data de publicação do afastamento." + - name: dias_restantes + description: "Dias restantes de afastamento." + - name: gratificacao_natalina + description: "Indica se o afastamento está relacionado à gratificação natalina." + - name: gr_matricula + description: "Matrícula GR." + - name: numero_diploma_afastamento + description: "Número do diploma legal do afastamento." + - name: numero_parcela + description: "Número da parcela do afastamento." + - name: parcela_continuacao_interrupcao + description: "Indica se a parcela é uma continuação ou interrupção." + - name: qtde_dias + description: "Quantidade de dias do afastamento." + - name: cpf + description: "CPF do servidor." + - name: dt_fim_aquisicao + description: "Data final do período aquisitivo de férias." + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + + + - name: dados_curriculo + description: > + Tabela bronze que representa dados curriculares e experiências profissionais dos servidores públicos. + Inclui informações sobre cursos realizados, instituições de ensino, tempo de experiência, projetos desenvolvidos e vínculos com órgãos ou empresas. + As datas foram padronizadas para o primeiro dia do mês (`dt_mes_`) e os dados textuais foram limpos de caracteres inválidos. + meta: + tags: + - bronze + columns: + - name: cpf + description: "CPF do servidor, formatado apenas com números." + + - name: ident_unica + description: "Identificador único do servidor no sistema, usado para correlacionar registros." + + - name: codigo_experiencia + description: "Código associado ao tipo de experiência do servidor ( 1 para formação, 2 para experiência profissional)." + + - name: cod_curso + description: "Código do curso realizado, se disponível." + + - name: nome_curso + description: "Nome do curso de formação acadêmica ou técnica. Exemplo: 'Ciências Contábeis', 'Administração'." + + - name: dt_mes_conclusao + description: "Data de conclusão do curso, normalizada para o primeiro dia do mês. Exemplo: '1991-12-01'." + + - name: nome_instituicao + description: "Nome da instituição onde o curso foi realizado. Exemplo: 'UDF', 'Universidade Católica de Brasília'." + + - name: nome_area_experiencia + description: > + Grau ou situação da experiência/capacitação. Pode indicar o nível ou status como: + 'Concluído', 'Intermediário', 'Básico', 'Curso', 'Avançado'." + + - name: carga_horaria + description: "Carga horária do curso, se disponível. Armazenado como texto." + + - name: nome_cargo + description: "Nome do cargo ou função exercida durante a experiência profissional." + + - name: dt_mes_inicio + description: "Data de início da experiência profissional, no formato 'YYYY-MM-01'." + + - name: nome_orgao_empresa + description: "Nome da organização, empresa ou órgão público em que a experiência foi realizada." + + - name: dt_mes_fim + description: "Data de término da experiência profissional, normalizada para o primeiro dia do mês." + + - name: descricao_projeto + description: "Descrição de projetos relevantes associados à experiência." + + - name: informacoes_adicionais + description: "Informações complementares que detalham a experiência ou formação." + + - name: tipo_descricao + description: "Tipo da descrição curricular. Pode indicar se o registro refere-se a curso, experiência, projeto etc." + + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + + - name: dados_dependentes + description: > + Tabela bronze que contém informações sobre os dependentes dos servidores públicos. + Inclui grau de parentesco, condições de dependência, benefícios associados e o período em que a dependência esteve ativa. + Os dados foram tratados para remover valores inválidos como 'NaN' e '00000000', e datas foram convertidas para o formato `DATE`. + meta: + tags: + - bronze + columns: + - name: cod_condicao + description: > + Código que representa a condição da dependência ( dependente econômico, legal, etc.). + Usado para classificar o tipo de vínculo do dependente com o servidor. + + - name: cod_grau_parentesco + description: > + Código que indica o grau de parentesco do dependente com o servidor ( filho, cônjuge, pai, etc.). + + - name: cod_orgao + description: "Código do órgão ao qual o servidor está vinculado." + + - name: cpf + description: "CPF do servidor titular da matrícula, contendo apenas números." + + - name: matricula + description: "Número da matrícula funcional do servidor ao qual o dependente está associado." + + - name: nome_dependente + description: "Nome completo do dependente." + + - name: nome_condicao + description: > + Descrição da condição do dependente ( 'Dependente para IR', 'Dependente para Plano de Saúde'). + + - name: nome_grau_parentesco + description: > + Descrição textual do grau de parentesco entre o servidor e o dependente ( 'Filho', 'Cônjuge'). + + - name: cod_beneficio + description: "Código que representa o tipo de benefício relacionado ao dependente, se houver." + + - name: dt_fim + description: > + Data de término da condição de dependência, no formato 'DDMMYYYY', convertida para DATE. + Pode ser nula se a dependência ainda estiver ativa. + + - name: dt_inicio + description: > + Data de início da condição de dependência, no formato 'DDMMYYYY', convertida para DATE. + + - name: nome_beneficio + description: > + Nome do benefício associado ao dependente ( auxílio-saúde, plano de assistência etc.). + + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + + - name: dados_escolares + description: > + Tabela bronze que armazena dados sobre a formação educacional dos servidores públicos. + Inclui informações sobre escolaridade, titulação e cursos associados às matrículas funcionais. + Os dados foram limpos para remover valores vazios e o CPF foi padronizado contendo apenas dígitos. + meta: + tags: + - bronze + columns: + - name: cod_curso + description: "Código identificador do curso realizado pelo servidor." + + - name: nome_curso + description: "Nome do curso relacionado à formação do servidor." + + - name: cod_matricula + description: "Código da matrícula funcional do servidor, vinculado ao curso." + + - name: cod_orgao + description: "Código do órgão público ao qual o servidor está vinculado." + + - name: cod_titulacao + description: "Código que representa a titulação do servidor ( especialização, mestrado, doutorado)." + + - name: nome_titulacao + description: "Descrição textual da titulação ( 'Mestrado', 'Doutorado')." + + - name: cod_escolaridade + description: "Código do nível de escolaridade ( ensino médio, superior, técnico)." + + - name: nome_escolaridade + description: "Descrição do nível de escolaridade do servidor." + + - name: cpf + description: "CPF do servidor, contendo apenas números, utilizado para identificação única." + + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + + + - name: dados_financeiros + description: > + Tabela bronze que armazena dados financeiros associados a servidores públicos. + Contém informações detalhadas sobre rubricas (proventos e descontos), valores pagos, + períodos de referência e data de pagamento. Os dados passam por limpeza e transformação + de formatos monetários e temporais para garantir consistência e padronização. + meta: + tags: + - bronze + columns: + - name: cod_rubrica + description: "Código da rubrica financeira (provento ou desconto) referente ao pagamento do servidor." + + - name: indicador_rd + description: "Indicador que diferencia se a rubrica é de receita (R) ou despesa (D)." + + - name: nome_rubrica + description: "Nome descritivo da rubrica, como 'Salário Base', 'Auxílio Alimentação', etc." + + - name: numero_sequencia + description: "Número sequencial que identifica a ordem da rubrica na folha de pagamento." + + - name: valor_rubrica + description: > + Valor monetário da rubrica, convertido de string para tipo numérico. + Foi realizada limpeza dos pontos e substituição da vírgula decimal por ponto. + + - name: data_anomes_rubrica + description: > + Data correspondente ao mês e ano de referência da rubrica, convertida do formato textual ( 'JAN2020') para uma data no formato YYYY-MM-DD + com o dia fixado em 01. + + - name: prazo_rubrica + description: "Informação de prazo da rubrica (campo opcional e com dados variados, pode indicar vencimento ou parcelamento)." + + - name: mes_ano_pagamento + description: > + Data correspondente ao mês e ano de efetivação do pagamento, convertida do formato textual ( 'JAN2020') para data com o dia fixado em 01. + + - name: cpf + description: > + CPF do servidor, padronizado contendo apenas dígitos. Utilizado para vinculação com outras informações do servidor. + + - name: indicador_mov_supl + description: > + Indicador que mostra se a movimentação financeira se refere a um pagamento suplementar ou retroativo (campo técnico da folha). + + - name: periodo_rubrica + description: > + Período a que se refere a rubrica, podendo representar um agrupamento ou classificação contábil adicional." + + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + + - name: dados_funcionais + description: > + Tabela bronze que armazena os dados funcionais dos servidores, contendo informações + sobre cargos, funções, regimes jurídicos, ocorrências de ingresso e aposentadoria, e dados + de unidades organizacionais (lotação e exercício). As datas são padronizadas, os CPFs higienizados + e os valores tratados para garantir integridade e legibilidade dos dados. + meta: + tags: + - bronze + columns: + - name: cod_atividade_funcao + description: "Código da atividade ou função exercida pelo servidor." + + - name: cod_funcao + description: "Código identificador da função do servidor." + + - name: cod_jornada + description: "Código da jornada de trabalho do servidor." + + - name: cod_ocorr_ingresso_orgao + description: "Código da ocorrência referente ao ingresso no órgão." + + - name: cod_ocorr_ingresso_serv_publico + description: "Código da ocorrência de ingresso no serviço público federal." + + - name: cod_orgao + description: "Código do órgão atual de lotação ou exercício do servidor." + + - name: cod_padrao + description: "Código do padrão do cargo ocupado." + + - name: cod_situacao_funcional + description: "Código representando a situação funcional atual do servidor (ativo, afastado, etc.)." + + - name: cod_uorg_exercicio + description: "Código da unidade organizacional onde o servidor exerce suas atividades." + + - name: cod_upag + description: "Código da Unidade Pagadora responsável pelo pagamento ao servidor." + + - name: cod_orgao_origem + description: "Código do órgão de origem, em caso de movimentação funcional." + + - name: cpf_chefia_imediata + description: "CPF da chefia imediata, higienizado para conter apenas dígitos." + + - name: dt_exercicio_no_orgao + description: "Data em que o servidor iniciou o exercício no órgão atual." + + - name: dt_fim_vale_ar + description: "Data final da vigência do vale de auxílio-refeição." + + - name: dt_ingresso_funcao + description: "Data de ingresso na função atual." + + - name: dt_ocorr_ingresso_orgao + description: "Data da ocorrência de ingresso no órgão atual." + + - name: dt_ocorr_ingresso_serv_publico + description: "Data da ocorrência de ingresso no serviço público." + + - name: email_chefia_imediata + description: "E-mail institucional da chefia imediata, padronizado em minúsculo." + + - name: email_institucional + description: "E-mail institucional do servidor, padronizado em minúsculo." + + - name: email_servidor + description: "E-mail pessoal do servidor, padronizado em minúsculo." + + - name: ident_unica + description: "Identificação única do servidor no sistema." + + - name: matricula_siape + description: "Matrícula do servidor no sistema SIAPE." + + - name: modalidade_pgd + description: "Modalidade de participação no Programa de Gestão e Desempenho (PGD)." + + - name: nome_atividade_funcao + description: "Descrição textual da atividade ou função exercida." + + - name: nome_chefe_uorg + description: "Nome da chefia imediata da unidade organizacional." + + - name: nome_funcao + description: "Nome da função ocupada." + + - name: nome_jornada + description: "Descrição da jornada de trabalho." + + - name: nome_ocorr_ingresso_orgao + description: "Descrição da ocorrência de ingresso no órgão." + + - name: nome_ocorr_ingresso_serv_publico + description: "Descrição da ocorrência de ingresso no serviço público." + + - name: nome_orgao + description: "Nome do órgão onde o servidor está vinculado." + + - name: nome_regime_juridico + description: "Nome do regime jurídico do servidor ( Estatutário, CLT)." + + - name: nome_situacao_funcional + description: "Descrição da situação funcional do servidor." + + - name: nome_uorg_exercicio + description: "Nome da unidade organizacional de exercício." + + - name: nome_upag + description: "Nome da unidade pagadora responsável pelo servidor." + + - name: participa_pgd + description: "Indica se o servidor participa do Programa de Gestão e Desempenho." + + - name: percentual_ts + description: "Percentual de tempo de trabalho remoto (teletrabalho), convertido para valor numérico ( 0.75 representa 75%)." + + - name: sigla_orgao + description: "Sigla do órgão atual." + + - name: sigla_orgao_origem + description: "Sigla do órgão de origem." + + - name: sigla_regime_juridico + description: "Sigla do regime jurídico do servidor." + + - name: sigla_uorg_exercicio + description: "Sigla da unidade de exercício." + + - name: sigla_upag + description: "Sigla da unidade pagadora." + + - name: cpf + description: "CPF do servidor, com apenas dígitos." + + - name: cod_cargo + description: "Código do cargo efetivo ocupado pelo servidor." + + - name: cod_classe + description: "Código da classe funcional do cargo." + + - name: cod_ocorr_aposentadoria + description: "Código da ocorrência de aposentadoria." + + - name: dt_ini_vale_ar + description: "Data de início do benefício de auxílio-refeição." + + - name: dt_ocorr_aposentadoria + description: "Data da ocorrência de aposentadoria." + + - name: nome_cargo + description: "Nome do cargo efetivo." + + - name: nome_classe + description: "Nome da classe funcional do cargo." + + - name: nome_ocorr_aposentadoria + description: "Descrição da ocorrência de aposentadoria." + + - name: sigla_nivel_cargo + description: "Sigla do nível do cargo ocupado." + + - name: tipo_vale_ar + description: "Tipo de auxílio-refeição concedido." + + - name: cod_ocorr_isencao_ir + description: "Código da ocorrência de isenção de imposto de renda." + + - name: dt_ini_ocorr_isencao_ir + description: "Data de início da ocorrência de isenção de IR." + + - name: nome_ocorr_isencao_ir + description: "Descrição da ocorrência de isenção de imposto de renda." + + - name: cod_uorg_lotacao + description: "Código da unidade de lotação do servidor." + + - name: nome_uorg_lotacao + description: "Nome da unidade de lotação do servidor." + + - name: sigla_uorg_lotacao + description: "Sigla da unidade de lotação do servidor." + + - name: dt_fim_ocorr_isencao_ir + description: "Data de término da isenção de IR." + + - name: cod_ocorr_exclusao + description: "Código da ocorrência de exclusão do servidor do sistema." + + - name: dt_ocorr_exclusao + description: "Data da ocorrência de exclusão do servidor." + + - name: nome_ocorr_exclusao + description: "Descrição da ocorrência de exclusão do servidor." + + - name: dt_uorg_lotacao + description: "Data da lotação na unidade organizacional atual." + + - name: cod_vale_transporte + description: "Código do benefício de vale transporte." + + - name: valor_vale_transporte + description: "Valor monetário do vale transporte." + + - name: dt_uorg_exercicio + description: "Data de início do exercício na unidade organizacional atual." + + - name: pontuacao_desempenho + description: > + Pontuação atribuída ao servidor conforme avaliação de desempenho. + Pode conter letras (A, B, C) ou valores numéricos, depende da origem. Mantido como string. + + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + + - name: dados_pa + description: > + Tabela bronze com dados de pensionistas e alimentados. + meta: + tags: + - bronze + columns: + - name: agencia_beneficiario + description: "Agência bancária do beneficiário." + - name: banco_beneficiario + description: "Banco do beneficiário." + - name: cod_orgao + description: "Código do órgão." + - name: conta_beneficiario + description: "Conta bancária do beneficiário." + - name: cpf_beneficiario + description: "CPF do beneficiário." + - name: matricula_servidor + description: "Matrícula do servidor instituidor da pensão." + - name: nome_beneficiario + description: "Nome do beneficiário." + - name: valor_ultima_pensao + description: "Valor da última pensão." + - name: cpf_servidor + description: "CPF do servidor instituidor da pensão." + - name: cod_vinculo_servidor + description: "Código do vínculo com o servidor." + - name: nome_alimentado + description: "Nome do alimentado." + - name: nome_vinculo_servidor + description: "Nome do vínculo com o servidor." + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + - name: dados_pessoais + description: > + Tabela bronze com dados pessoais dos servidores. + meta: + tags: + - bronze + columns: + - name: cod_cor + description: "Código da cor/raça." + - name: cod_estado_civil + description: "Código do estado civil." + - name: cod_nacionalidade + description: "Código da nacionalidade." + - name: cod_sexo + description: "Código do sexo." + - name: dt_nascimento + description: "Data de nascimento." + - name: grupo_sanguineo + description: "Grupo sanguíneo." + - name: nome_pessoa + description: "Nome da pessoa." + - name: nome_cor + description: "Nome da cor/raça." + - name: nome_estado_civil + description: "Nome do estado civil." + - name: nome_mae + description: "Nome da mãe." + - name: nome_municipio_nascimento + description: "Nome do município de nascimento." + - name: nome_nacionalidade + description: "Nome da nacionalidade." + - name: nome_pai + description: "Nome do pai." + - name: nome_sexo + description: "Nome do sexo." + - name: num_pispasep + description: "Número do PIS/PASEP." + - name: uf_nascimento + description: "UF de nascimento." + - name: cpf + description: "CPF do servidor." + - name: cod_deficiencia_fisica + description: "Código de deficiência física." + - name: nome_deficiencia_fisica + description: "Nome da deficiência física." + - name: dt_chegada_brasil + description: "Data de chegada ao Brasil." + - name: nome_pais_origem + description: "Nome do país de origem." + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + - name: dados_uorg + description: > + Tabela bronze com dados das Unidades Organizacionais (UORGs). + meta: + tags: + - bronze + columns: + - name: bairro_uorg + description: "Bairro da UORG." + - name: cep_uorg + description: "CEP da UORG." + - name: codigo_matricula + description: "Código de matrícula." + - name: codigo_municipio_uorg + description: "Código do município da UORG." + - name: codigo_orgao + description: "Código do órgão." + - name: codigo_orgao_uorg + description: "Código da UORG no órgão." + - name: email_uorg + description: "Email da UORG." + - name: tipo_endereco_uorg + description: "Tipo de endereço da UORG." + - name: logradouro_uorg + description: "Logradouro da UORG." + - name: nome_municipio_uorg + description: "Nome do município da UORG." + - name: nome_uorg + description: "Nome da UORG." + - name: telefone_uorg + description: "Telefone da UORG." + - name: numero_endereco_uorg + description: "Número do endereço da UORG." + - name: sigla_uorg + description: "Sigla da UORG." + - name: uf_uorg + description: "UF da UORG." + - name: cpf + description: "CPF associado à UORG." + - name: complemento_endereco_uorg + description: "Complemento do endereço da UORG." + - name: fax_uorg + description: "Fax da UORG." + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + + - name: estrutura_organizacional_cargos + description: > + Tabela gold contendo a estrutura organizacional de unidades com informações expandidas de cargos e instâncias. + Os dados foram transformados para extrair e normalizar os campos aninhados no JSON armazenado na coluna `cargos`. + A tabela inclui apenas uma instância por código, priorizando a de maior ordem de grandeza. + meta: + tags: + - bronze + columns: + - name: codigounidade + description: Código da unidade organizacional. + - name: nomeunidade + description: Nome completo da unidade organizacional. + - name: siglaunidade + description: Sigla da unidade organizacional. + - name: municipio + description: Município onde a unidade está localizada. + - name: uf + description: Unidade federativa (UF) correspondente ao município. + - name: denominacao + description: Denominação do cargo. + - name: funcao + description: Função associada à denominação do cargo. + - name: codigo_instancia + description: Código da instância do cargo na estrutura. + - name: nome_titular + description: Nome do servidor titular do cargo. + - name: cpf_titular + description: CPF do servidor titular do cargo (com apenas dígitos numéricos). + - name: ordem_grandeza + description: Nível hierárquico da unidade na estrutura, utilizado para escolher a instância mais relevante. + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + + - name: lista_servidores + description: > + Tabela bronze com a lista de servidores e suas UORGs. + meta: + tags: + - bronze + columns: + - name: cod_uorg + description: "Código da UORG." + - name: dt_ultima_transacao + description: "Data da última transação." + - name: cpf + description: "CPF do servidor." + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + - name: lista_uorgs + description: > + Tabela bronze com a lista de UORGs. + meta: + tags: + - bronze + columns: + - name: codigo + description: "Código da UORG." + - name: dt_ultima_transacao + description: "Data da última transação." + - name: nome + description: "Nome da UORG." + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + - name: terceirizados + description: > + Tabela gold contendo informações de trabalhadores terceirizados oriundos do sistema ComprasGov. + Inclui dados relacionados ao contrato, função, jornada, remuneração e benefícios, com tratamento de campos textuais e numéricos para padronização. + meta: + tags: + - bronze + columns: + - name: id + description: Identificador único do registro do trabalhador terceirizado. + - name: contrato_id + description: Identificador do contrato ao qual o trabalhador está vinculado. + - name: cpf + description: CPF do trabalhador, extraído da string `usuario`. + - name: nome + description: Nome do trabalhador, extraído da string `usuario`. + - name: funcao_id + description: Identificador da função exercida pelo trabalhador. + - name: descricao_complementar + description: Descrição complementar da função ou atividade do trabalhador. + - name: jornada + description: Quantidade de horas de jornada de trabalho semanal do trabalhador (convertido para numérico). + - name: unidade + description: Unidade organizacional onde o trabalhador presta serviço. + - name: salario + description: Valor do salário mensal do trabalhador (convertido para formato numérico padrão). + - name: custo + description: Custo total do trabalhador para a instituição (convertido para formato numérico padrão). + - name: escolaridade_id + description: Identificador do grau de escolaridade do trabalhador. + - name: data_inicio + description: Data de início do contrato do trabalhador, convertida para formato de data. + - name: data_fim + description: Data de término do contrato do trabalhador, convertida para formato de data. + - name: situacao + description: Situação atual do vínculo contratual do trabalhador (ativo, encerrado). + - name: aux_transporte + description: Valor mensal do auxílio transporte recebido pelo trabalhador (convertido para numérico). + - name: vale_alimentacao + description: Valor mensal do vale alimentação recebido pelo trabalhador (convertido para numérico). + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + + - name: unidade_organizacional + description: > + Tabela gold que representa a hierarquia das unidades organizacionais extraídas do sistema Siorg. + A hierarquia é construída de forma recursiva, partindo de uma unidade raiz definida manualmente. + A tabela traz dados estruturados sobre as unidades, seus vínculos parentais e o nível de profundidade hierárquica. + meta: + tags: + - bronze + columns: + - name: codigounidade + description: Código da unidade organizacional (sem prefixos de URI). + - name: codigounidadepai + description: Código da unidade organizacional pai, representando a hierarquia entre unidades. + - name: codigoorgaoentidade + description: Código do órgão ou entidade ao qual a unidade pertence. + - name: codigotipounidade + description: Código do tipo da unidade organizacional (departamento, secretaria). + - name: nome + description: Nome completo da unidade organizacional. + - name: sigla + description: Sigla da unidade organizacional. + - name: codigoesfera + description: Código da esfera governamental (federal, estadual, municipal). + - name: codigopoder + description: Código do poder ao qual a unidade está vinculada (Executivo, Judiciário). + - name: codigonaturezajuridica + description: Código da natureza jurídica da unidade. + - name: codigosubnaturezajuridica + description: Código da subnatureza jurídica da unidade. + - name: nivelnormatizacao + description: Nível de normatização da unidade organizacional. + - name: versaoconsulta + description: Número da versão da consulta no momento da extração dos dados. + - name: datafinalversaoconsulta + description: Data de finalização da versão da consulta. + - name: operacao + description: Tipo de operação registrada (inclusão, alteração, exclusão). + - name: codigounidadepaianterior + description: Código da unidade pai anterior (em caso de alteração estrutural). + - name: codigoorgaoentidadeanterior + description: Código do órgão/entidade anterior da unidade (em caso de mudança). + - name: ordem_grandeza + description: Nível hierárquico da unidade dentro da estrutura organizacional (quanto maior, mais profunda). + - name: caminho_unidade + description: Caminho hierárquico concatenado das siglas das unidades até o nível atual. + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw." + + diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/terceirizados.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/terceirizados.sql new file mode 100644 index 00000000..fd5d5367 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/terceirizados.sql @@ -0,0 +1,21 @@ +select + id, + contrato_id, + substring(usuario, '(.+) - ') as cpf, + substring(usuario, '- (.+)') as nome, + funcao_id, + descricao_complementar, + jornada::numeric as jornada, + unidade, + replace(replace(salario, '.', ''), ',', '.')::numeric(15, 2) as salario, + replace(replace(custo, '.', ''), ',', '.')::numeric(15, 2) as custo, + escolaridade_id, + to_date(data_inicio, 'YYYY-mm-dd') as data_inicio, + to_date(data_fim, 'YYYY-mm-dd') as data_fim, + situacao, + replace(replace(aux_transporte, '.', ''), ',', '.')::numeric(15, 2) as aux_transporte, + replace(replace(vale_alimentacao, '.', ''), ',', '.')::numeric( + 15, 2 + ) as vale_alimentacao, + (dt_ingest || '-03:00')::timestamptz as dt_ingest +from {{ source("compras_gov", "terceirizados") }} diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/unidade_organizacional.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/unidade_organizacional.sql new file mode 100644 index 00000000..ef0d04f1 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/bronze/unidade_organizacional.sql @@ -0,0 +1,42 @@ +with recursive + fonte as ( + select + regexp_replace(codigounidade, '^.*/', '') as codigounidade, + regexp_replace(codigounidadepai, '^.*/', '') as codigounidadepai, + regexp_replace(codigoorgaoentidade, '^.*/', '') as codigoorgaoentidade, + regexp_replace(codigotipounidade, '^.*/', '') as codigotipounidade, + nome, + sigla, + regexp_replace(codigoesfera, '^.*/', '') as codigoesfera, + regexp_replace(codigopoder, '^.*/', '') as codigopoder, + regexp_replace(codigonaturezajuridica, '^.*/', '') as codigonaturezajuridica, + codigosubnaturezajuridica, + nivelnormatizacao, + versaoconsulta, + datafinalversaoconsulta, + operacao, + codigounidadepaianterior, + codigoorgaoentidadeanterior, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("siorg", "unidade_organizacional") }} + ), + + unidades_raiz as (select '7' as codigounidade_raiz), + + hierarquia as ( + select f.*, 1 as ordem_grandeza, sigla as caminho_unidade + from fonte f + join unidades_raiz r on f.codigounidade = r.codigounidade_raiz + + union all + + select + f.*, + h.ordem_grandeza + 1 as ordem_grandeza, + h.caminho_unidade || '-' || lpad(f.sigla::text, 5, '0') as caminho_unidade + from fonte f + join hierarquia h on f.codigounidadepai = h.codigounidade + ) + +select * +from hierarquia diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/aposentadorias_resumo.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/aposentadorias_resumo.sql new file mode 100644 index 00000000..5e0cf404 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/aposentadorias_resumo.sql @@ -0,0 +1,34 @@ +-- Tabela contendo os aposentados e tempo de serviço +-- Granularidade: cpf +with + + aposentados_extract as ( + select distinct + cpf, + nome_pessoa, + dt_ocorr_ingresso_serv_publico, + dt_ocorr_aposentadoria, + date_trunc('month', dt_ocorr_aposentadoria) as mes_aposentadoria, + nome_situacao_funcional, + nome_ocorr_aposentadoria, + nome_cargo, + sigla_nivel_cargo, + cod_classe || '-' || cod_padrao as classe_padrao, + dt_ingest + from {{ ref("servidores_detalhados") }} sd + where dt_ocorr_aposentadoria is not null + ) + +select + *, + age(dt_ocorr_aposentadoria, dt_ocorr_ingresso_serv_publico) as age, + extract( + year from age(dt_ocorr_aposentadoria, dt_ocorr_ingresso_serv_publico) + ) as diff_anos, + extract( + month from age(dt_ocorr_aposentadoria, dt_ocorr_ingresso_serv_publico) + ) as diff_meses, + extract( + days from age(dt_ocorr_aposentadoria, dt_ocorr_ingresso_serv_publico) + ) as diff_dias +from aposentados_extract diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/cargos_consolidado.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/cargos_consolidado.sql new file mode 100644 index 00000000..96e000ee --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/cargos_consolidado.sql @@ -0,0 +1,33 @@ +-- cargos siape + siorg +-- ver logica para achar os cargos vagos e mudar essa tabela ! +-- como o siorg lista todos os cargos, os que vierem null no siape provavelmente estão +-- vagos +select + siorg.codigounidade as siorg_cod_unidade, + siorg.nomeunidade as siorg_nome_unidade, + siorg.siglaunidade as siorg_sigla_unidade, + siorg.municipio as siorg_municipio_unidade, + siorg.uf as siorg_uf_unidade, + siorg.denominacao as siorg_denominacao_cargo, + siorg.funcao as siorg_funcao, + siorg.codigo_instancia as siorg_cod_instancia_cargo, + siorg.cpf_titular as siorg_cpf_titular, + siorg.nome_titular as siorg_nome_titular, + + siape.cpf as siape_cpf, + siape.nome_pessoa as siape_nome_pessoa, + siape.nome_cargo as siape_nome_cargo_efetivo, + siape.nome_funcao as siape_nome_funcao_comissionada, + siape.cod_uorg_exercicio as siape_cod_uorg, + siape.nome_uorg_exercicio as siape_nome_uorg, + siape.sigla_uorg_exercicio as siape_sigla_uorg, + siape.uf_uorg as siape_uf_uorg, + siape.nome_situacao_funcional as siape_situacao_funcional, + greatest( + siorg.dt_ingest, + siape.dt_ingest + ) as dt_ingest + +from {{ ref("servidores_detalhados") }} siape +left join + {{ ref("estrutura_organizacional_cargos") }} siorg on siape.cpf = siorg.cpf_titular -- tabela siorg diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_genero.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_genero.sql new file mode 100644 index 00000000..56b692f8 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_genero.sql @@ -0,0 +1,9 @@ +-- Distribuição de servidores por gênero +select + nome_sexo as genero, + count(*) as quantidade_servidores, + count(*) * 1.0 / sum(count(*)) over () as percentual_distribuicao, + max(dt_ingest) as dt_ingest +from {{ ref("servidores_completos") }} +group by nome_sexo +order by percentual_distribuicao desc diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_mapa_uf.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_mapa_uf.sql new file mode 100644 index 00000000..3736e2e9 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_mapa_uf.sql @@ -0,0 +1,38 @@ +-- Modelo para gerar a distribuição geográfica de servidores por UF +-- Retorna todos os estados brasileiros com suas respectivas contagens e percentuais +with + -- Obter todos os servidores com localização + servidores_localizacao as ( + select distinct + df.cpf, du.uf_uorg, du.nome_municipio_uorg, df.nome_situacao_funcional, + greatest(df.dt_ingest, du.dt_ingest) as dt_ingest + from {{ ref("dados_funcionais") }} df + inner join {{ ref("dados_uorg") }} du on df.sigla_uorg_exercicio = du.sigla_uorg + where du.uf_uorg is not null + ), + + -- Contar servidores por UF + contagem_por_uf as ( + select uf_uorg, count(distinct cpf) as valor, max(dt_ingest) as dt_ingest_uf + from servidores_localizacao + group by uf_uorg + ), + + -- Calcular totais para percentual + total_servidores as (select sum(valor) as total from contagem_por_uf) + +-- Juntar todos os estados com suas contagens (0 para estados sem servidores) +select + eb.sigla_uf, + eb.nome_uf, + coalesce(cpu.valor, 0) as valor, + case + when coalesce(cpu.valor, 0) = 0 + then '0%' + else concat(round((coalesce(cpu.valor, 0) * 100.0 / ts.total), 0), '%') + end as percentual, + cpu.dt_ingest_uf as dt_ingest +from {{ ref("estados_brasil") }} eb +cross join total_servidores ts +left join contagem_por_uf cpu on eb.sigla_uf = cpu.uf_uorg +order by eb.sigla_uf diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_raca_cor.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_raca_cor.sql new file mode 100644 index 00000000..5c5a884b --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_raca_cor.sql @@ -0,0 +1,5 @@ +-- Distribuição de servidores por raça/cor +select nome_cor as cor_raca, count(nome_cor) as quantidade_servidores, max(dt_ingest) as dt_ingest +from {{ ref("servidores_completos") }} +group by nome_cor +order by quantidade_servidores desc diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_raca_cor_sexo_servidores_.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_raca_cor_sexo_servidores_.sql new file mode 100644 index 00000000..85e8de2e --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_raca_cor_sexo_servidores_.sql @@ -0,0 +1,10 @@ +-- Distribuição de servidores por raça/cor e sexo do servidor +SELECT + nome_cor, + SUM(CASE WHEN nome_sexo = 'FEMININO' THEN 1 ELSE 0 END) * -1 AS feminino, + SUM(CASE WHEN nome_sexo = 'MASCULINO' THEN 1 ELSE 0 END) AS masculino, + nome_situacao_funcional, + max(dt_ingest) as dt_ingest +FROM {{ ref("hierarquia") }} +GROUP BY nome_cor, nome_situacao_funcional +ORDER BY nome_cor diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_situacao_funcional.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_situacao_funcional.sql new file mode 100644 index 00000000..4ff775b9 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/distribuicao_situacao_funcional.sql @@ -0,0 +1,34 @@ +with + dados_funcionais_enriquecidos as ( + select distinct + df.*, + case + when df.modalidade_pgd is null + then 'Não participa' + when df.modalidade_pgd = 'parcial' + then 'Parcial' + when df.modalidade_pgd = 'integral' + then 'Integral' + when df.modalidade_pgd = 'presencial' + then 'Presencial' + when df.modalidade_pgd = 'no exterior' + then 'No exterior' + end as pdg, + case + when df.nome_situacao_funcional = 'ATIVO EM OUTRO ORGAO' + then 'Ativo em outro órgão' + else df.sigla_uorg_exercicio + end as unidade_exercicio, + du.nome_municipio_uorg, + greatest(df.dt_ingest, du.dt_ingest) as dt_ingest_max + from {{ ref("dados_funcionais") }} df + inner join {{ ref("dados_uorg") }} du on df.sigla_uorg_exercicio = du.sigla_uorg + ) + +select + nome_situacao_funcional as situacao_funcional_original, + count(nome_situacao_funcional) as quantidade_servidores, + max(dt_ingest_max) as dt_ingest +from dados_funcionais_enriquecidos +group by nome_situacao_funcional +order by quantidade_servidores desc diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/hierarquia.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/hierarquia.sql new file mode 100644 index 00000000..293cb6e4 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/hierarquia.sql @@ -0,0 +1,241 @@ +with + + correcao_funcao as ( + select *, replace(funcao, ' ', '') as funcao_sigla + from {{ ref("estrutura_organizacional_cargos") }} + ), + + codigos_siorg as ( + select distinct + funcao_sigla, + eorg.nomeunidade, + eorg.codigounidade, + eorg.ordem_grandeza, + eorg.denominacao, + uo.codigounidadepai, + uo.caminho_unidade, + case + when eorg.siglaunidade = 'GABIN-IPEA' then 'GABIN' else siglaunidade + end as siglaunidade, + substring(funcao_sigla, length(funcao_sigla) - 2, 1) as categoria_cargo, + -- hierarquia do cargo está sendo definida a partir da fórmula: + -- (categoria do cargo * 1000) - nível do cargo + -- quanto menor a hierarquia, maior o cargo + right(funcao_sigla, 2) as nivel_cargo, + cast(substring(funcao_sigla, length(funcao_sigla) - 2, 1) as int) * 1000 + - cast(right(funcao, 2) as int) as hierarquia_cargo, + greatest(eorg.dt_ingest, uo.dt_ingest) as dt_ingest_siorg + from correcao_funcao as eorg + inner join + {{ ref("unidade_organizacional") }} as uo + on eorg.codigounidade = uo.codigounidade + ), + + codigos_siape as ( + select distinct + df.cod_funcao, + df.nome_uorg_exercicio, + df.sigla_uorg_exercicio, + df.nome_cargo, + df.matricula_siape, + df.cpf, + df.cpf_chefia_imediata, + df.cod_situacao_funcional, + df.nome_situacao_funcional, + dp.nome_pessoa, + dp.dt_nascimento, + dp.nome_sexo, + dp.nome_estado_civil, + dp.nome_nacionalidade, + dp.nome_cor, + dp.uf_nascimento, + dp.nome_municipio_nascimento, + uo.codigounidade as codigounidade_alternativa, + uo.caminho_unidade as caminho_unidade_alternativa, + uo.codigounidadepai as codigounidadepai_alternativa, + uo.ordem_grandeza as ordem_grandeza_alternativa, + substring(df.cod_funcao, 1, 1) || substring( + df.cod_funcao, length(df.cod_funcao) - 2, 3 + ) as codigo_combinacao_siape, + greatest(df.dt_ingest, uo.dt_ingest, uo.dt_ingest) as dt_ingest_siape + from {{ ref("dados_funcionais") }} as df + left join {{ ref("dados_pessoais") }} as dp on df.cpf = dp.cpf + left join + {{ ref("unidade_organizacional") }} as uo + on df.sigla_uorg_exercicio = uo.sigla + where dt_ocorr_aposentadoria is null and dt_ocorr_exclusao is null + ), + + -- select count(*) from codigos_siape; + codigo_siorg_combinado as ( + select + *, + substring(funcao_sigla, 1, 1) || substring( + funcao_sigla, length(funcao_sigla) - 2, 3 + ) as codigo_combinacao_siorg + from codigos_siorg + ), + + primeira_correlacao as ( + select + *, + case + when + siorg.codigo_combinacao_siorg is not null + and siape.codigo_combinacao_siape is not null + then 'inner' + when + siorg.codigo_combinacao_siorg is not null + and siape.codigo_combinacao_siape is null + then 'left' + when + siorg.codigo_combinacao_siorg is null + and siape.codigo_combinacao_siape is not null + then 'right' + end as tipo_correlacao + from codigo_siorg_combinado as siorg + full join + codigos_siape as siape + on siorg.codigo_combinacao_siorg = siape.codigo_combinacao_siape + and siorg.siglaunidade = siape.sigla_uorg_exercicio + ), + + -- select count(*) from primeira_correlacao + tabela_correlacao_cargos as ( + select distinct + pr.cod_funcao as codigo_siape, + pr.funcao_sigla as codigo_siorg, + pr.codigo_combinacao_siape, + pr.codigo_combinacao_siorg, + pr.matricula_siape as matricula_siape, + pr.cpf as cpf, + pr.cpf_chefia_imediata as cpf_chefia_imediata, + pr.cod_situacao_funcional as cod_situacao_funcional, + pr.nome_situacao_funcional as nome_situacao_funcional, + pr.hierarquia_cargo as hierarquia_cargo, + pr.nome_pessoa as servidor, + pr.dt_nascimento as dt_nascimento, + pr.nome_sexo as nome_sexo, + pr.nome_estado_civil as nome_estado_civil, + pr.nome_nacionalidade as nome_nacionalidade, + pr.nome_cor as nome_cor, + pr.uf_nascimento as uf_nascimento, + pr.nome_municipio_nascimento as nome_municipio_nascimento, + dp.nome_pessoa as nome_chefia, + coalesce( + cast(pr.codigounidade as text), cast(pr.codigounidade_alternativa as text) + ) as codigounidade, + coalesce( + cast(pr.codigounidadepai as text), + cast(pr.codigounidadepai_alternativa as text) + ) as codigounidadepai, + coalesce( + cast(pr.caminho_unidade as text), + cast(pr.caminho_unidade_alternativa as text) + ) as caminho_unidade, + coalesce( + cast(pr.ordem_grandeza as text), + cast(pr.ordem_grandeza_alternativa as text) + ) as ordem_grandeza, + coalesce(nomeunidade, nome_uorg_exercicio) as nomeunidade, + coalesce(siglaunidade, sigla_uorg_exercicio) as siglaunidade, + coalesce(denominacao, nome_cargo) as nome_cargo, + case + when cod_situacao_funcional = '04' then 'Nomeação livre' else 'Carreira' + end as servidores_carreira, + greatest(pr.dt_ingest_siorg, pr.dt_ingest_siape, dp.dt_ingest) as dt_ingest + from primeira_correlacao as pr + left join {{ ref("dados_pessoais") }} as dp on pr.cpf_chefia_imediata = dp.cpf + order by caminho_unidade, hierarquia_cargo + ), + + hierarquia_filtrada as ( + select * + from tabela_correlacao_cargos + where nome_situacao_funcional != 'ATIVO EM OUTRO ORGAO' + ), + + hierarquia_enriquecida as ( + select + ph.*, + df.dt_ingest as dt_ingest_dados_funcionais, + case + when df.modalidade_pgd is null + then 'Não participa' + when df.modalidade_pgd = 'parcial' + then 'Parcial' + when df.modalidade_pgd = 'integral' + then 'Integral' + when df.modalidade_pgd = 'presencial' + then 'Presencial' + when df.modalidade_pgd = 'no exterior' + then 'No exterior' + end as pdg, + case + when ph.nome_situacao_funcional = 'ATIVO EM OUTRO ORGAO' + then 'Ativo em outro órgão' + else ph.siglaunidade + end as unidade_exercicio + from hierarquia_filtrada as ph + inner join {{ ref("dados_funcionais") }} as df on ph.cpf = df.cpf + ), + + servidores_enriquecidos as ( + select + distinct + ph.*, + du.nome_municipio_uorg, + du.dt_ingest as dt_ingest_dados_uorg + from hierarquia_enriquecida as ph + inner join {{ ref("dados_uorg") }} as du on ph.siglaunidade = du.sigla_uorg + order by caminho_unidade, hierarquia_cargo + ), + + hierarquia_completa as ( + select distinct + se.codigo_siape, + se.codigo_siorg, + se.codigo_combinacao_siape, + se.codigo_combinacao_siorg, + se.matricula_siape, + se.cpf, + se.cpf_chefia_imediata, + se.cod_situacao_funcional, + se.nome_situacao_funcional, + se.hierarquia_cargo, + se.servidor, + se.dt_nascimento, + se.nome_sexo, + se.nome_estado_civil, + se.nome_nacionalidade, + se.nome_cor, + se.uf_nascimento, + se.nome_municipio_nascimento, + se.nome_chefia, + se.codigounidade, + se.codigounidadepai, + se.caminho_unidade, + se.ordem_grandeza, + se.nomeunidade, + se.siglaunidade, + se.nome_cargo, + se.servidores_carreira, + se.pdg, + se.unidade_exercicio, + se.nome_municipio_uorg, + sd.cod_escolaridade_principal, + sd.nome_escolaridade_principal, + sd.nome_deficiencia_fisica, + sd.nome_cargo as nome_cargo_emprego, + greatest( + se.dt_ingest, + se.dt_ingest_dados_funcionais, + se.dt_ingest_dados_uorg, + sd.dt_ingest + ) as dt_ingest + from servidores_enriquecidos as se + inner join {{ ref("servidores_detalhados") }} as sd on se.cpf = sd.cpf + ) + +select * +from hierarquia_completa diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/kpis_servidores.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/kpis_servidores.sql new file mode 100644 index 00000000..f4da198c --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/kpis_servidores.sql @@ -0,0 +1,48 @@ +with + total_servidores as ( + select count(distinct cpf) as total, max(dt_ingest) as dt_ingest + from {{ ref("dados_funcionais") }} + ), + + servidores_ativos as ( + select count(distinct cpf) as total, max(dt_ingest) as dt_ingest + from {{ ref("dados_funcionais") }} + where nome_situacao_funcional in ('ATIVO PERMANENTE') + ), + + aposentados as ( + select count(distinct cpf) as total, max(dt_ingest) as dt_ingest + from {{ ref("dados_funcionais") }} + where nome_situacao_funcional in ('APOSENTADO') + ), + + estagiarios as ( + select count(distinct cpf) as total, max(dt_ingest) as dt_ingest + from {{ ref("dados_funcionais") }} + where nome_situacao_funcional in ('ESTAGIARIO SIGEPE') + ), + + terceirizados as (select count(distinct id) as total, max(dt_ingest) as dt_ingest from {{ ref("terceirizados") }}) + +select 'total_servidores' as kpi, total as valor, dt_ingest +from total_servidores + +union all + +select 'servidores_ativos_permanentes' as kpi, total as valor, dt_ingest +from servidores_ativos + +union all + +select 'aposentados' as kpi, total as valor, dt_ingest +from aposentados + +union all + +select 'estagiarios' as kpi, total as valor, dt_ingest +from estagiarios + +union all + +select 'terceirizados' as kpi, total as valor, dt_ingest +from terceirizados diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/resumo_quadro_pessoal.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/resumo_quadro_pessoal.sql new file mode 100644 index 00000000..368fb2b6 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/resumo_quadro_pessoal.sql @@ -0,0 +1,9 @@ +select + coalesce(nome_cargo, 'N/A') as cargo_efetivo, + coalesce(nome_sexo, 'N/A') as genero, + coalesce(nome_situacao_funcional, 'N/A') as situacao_funcional, + coalesce(uf_uorg, 'N/A') as localidade_uf, + count(distinct cpf) as quantidade_servidores, + max(dt_ingest) as dt_ingest +from {{ ref("servidores_detalhados") }} +group by 1, 2, 3, 4 diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/schema.yml b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/schema.yml new file mode 100644 index 00000000..a444a693 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/schema.yml @@ -0,0 +1,399 @@ +version: 2 + +models: + + # Gold + - name: aposentadorias_resumo + description: > + Tabela de resumo dos servidores aposentados, contendo informações detalhadas + sobre a data de ingresso no serviço público, data de aposentadoria e tempo + de serviço público calculado em anos, meses e dias. A granularidade da tabela + é por CPF, ou seja, cada linha representa um servidor aposentado único. + + meta: + tags: + - gold + columns: + - name: cpf + description: Número do CPF do servidor aposentado. + - name: nome_pessoa + description: Nome completo do servidor aposentado. + - name: dt_ocorr_ingresso_serv_publico + description: Data de ingresso do servidor no serviço público. + - name: dt_ocorr_aposentadoria + description: Data de aposentadoria do servidor. + - name: mes_aposentadoria + description: Mês da aposentadoria (arredondado para o primeiro dia do mês). + - name: nome_situacao_funcional + description: Situação funcional do servidor no momento da aposentadoria. + - name: nome_ocorr_aposentadoria + description: Tipo de ocorrência que levou à aposentadoria do servidor. + - name: nome_cargo + description: Cargo ocupado pelo servidor no momento da aposentadoria. + - name: sigla_nivel_cargo + description: Sigla do nível do cargo ocupado. + - name: classe_padrao + description: Classe e padrão do cargo, no formato "classe-padrão". + - name: age + description: Diferença completa entre as datas de ingresso e aposentadoria, em formato de intervalo. + - name: diff_anos + description: Quantidade de anos entre o ingresso no serviço público e a aposentadoria. + - name: diff_meses + description: Quantidade de meses entre o ingresso e a aposentadoria (ignora anos completos). + - name: diff_dias + description: Quantidade de dias entre o ingresso e a aposentadoria (ignora anos e meses completos). + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + + - name: cargos_consolidado + description: > + Tabela que consolida informações de cargos ocupados e vagos no serviço público, + a partir da junção entre os dados do SIAPE (servidores ativos e detalhados) e + os dados do SIORG (estrutura de cargos disponíveis). Essa junção é feita com + base no CPF do titular do cargo. Quando os campos do SIAPE estão nulos, o cargo + provavelmente está vago. A tabela pode ser utilizada para identificar cargos + vagos por unidade organizacional. + + meta: + tags: + - gold + columns: + - name: siorg_cod_unidade + description: Código da unidade organizacional no SIORG. + - name: siorg_nome_unidade + description: Nome da unidade organizacional no SIORG. + - name: siorg_sigla_unidade + description: Sigla da unidade organizacional no SIORG. + - name: siorg_municipio_unidade + description: Município da unidade organizacional no SIORG. + - name: siorg_uf_unidade + description: Unidade federativa (UF) da unidade organizacional no SIORG. + - name: siorg_denominacao_cargo + description: Denominação do cargo conforme registrado no SIORG. + - name: siorg_funcao + description: Função comissionada associada ao cargo no SIORG. + - name: siorg_cod_instancia_cargo + description: Código da instância do cargo no SIORG. + - name: siorg_cpf_titular + description: CPF do titular do cargo segundo o SIORG. + - name: siorg_nome_titular + description: Nome do titular do cargo segundo o SIORG. + + - name: siape_cpf + description: CPF do servidor conforme os dados do SIAPE. + - name: siape_nome_pessoa + description: Nome completo do servidor no SIAPE. + - name: siape_nome_cargo_efetivo + description: Nome do cargo efetivo ocupado pelo servidor no SIAPE. + - name: siape_nome_funcao_comissionada + description: Nome da função comissionada ocupada pelo servidor no SIAPE. + - name: siape_cod_uorg + description: Código da unidade de exercício do servidor no SIAPE. + - name: siape_nome_uorg + description: Nome da unidade de exercício do servidor no SIAPE. + - name: siape_sigla_uorg + description: Sigla da unidade de exercício do servidor no SIAPE. + - name: siape_uf_uorg + description: Unidade federativa (UF) da unidade de exercício no SIAPE. + - name: siape_situacao_funcional + description: Situação funcional atual do servidor segundo o SIAPE. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + + - name: hierarquia + description: > + Tabela que correlaciona dados do SIAPE (dados funcionais e pessoais dos servidores) + com a estrutura organizacional do SIORG, atribuindo uma métrica de hierarquia aos cargos + com base na codificação da função. A hierarquia é determinada por uma fórmula baseada + na categoria e no nível do cargo. A tabela também indica se o cargo é de carreira ou de + nomeação livre, além de consolidar informações sobre o servidor e sua chefia imediata. + + meta: + tags: + - gold + columns: + - name: codigo_siape + description: Código da função no SIAPE. + - name: codigo_siorg + description: Código da função no SIORG, com espaços removidos. + - name: codigo_combinacao_siape + description: Código combinado derivado da função no SIAPE, usado para correlação. + - name: codigo_combinacao_siorg + description: Código combinado derivado da função no SIORG, usado para correlação. + - name: matricula_siape + description: Matrícula funcional do servidor no SIAPE. + - name: cpf + description: CPF do servidor. + - name: cpf_chefia_imediata + description: CPF da chefia imediata do servidor. + - name: cod_situacao_funcional + description: Código da situação funcional do servidor. + - name: nome_situacao_funcional + description: Descrição da situação funcional do servidor. + - name: hierarquia_cargo + description: Indicador numérico da hierarquia do cargo (quanto menor, maior o cargo). + - name: servidor + description: Nome do servidor ocupante do cargo. + - name: dt_nascimento + description: Data de nascimento do servidor. + - name: nome_sexo + description: Sexo do servidor. + - name: nome_estado_civil + description: Estado civil do servidor. + - name: nome_nacionalidade + description: Nacionalidade do servidor. + - name: nome_cor + description: Cor/raça do servidor. + - name: uf_nascimento + description: Unidade federativa de nascimento do servidor. + - name: nome_municipio_nascimento + description: Município de nascimento do servidor. + - name: nome_chefia + description: Nome da chefia imediata do servidor. + - name: codigounidade + description: Código da unidade organizacional associada ao cargo. + - name: codigounidadepai + description: Código da unidade organizacional pai (superior). + - name: caminho_unidade + description: Caminho hierárquico completo da unidade. + - name: ordem_grandeza + description: Nível hierárquico da unidade na estrutura organizacional. + - name: nomeunidade + description: Nome da unidade organizacional. + - name: siglaunidade + description: Sigla da unidade organizacional. + - name: nome_cargo + description: Nome ou denominação do cargo ocupado. + - name: servidores_carreira + description: Indica se o servidor está em cargo de carreira ou em nomeação livre. + - name: pdg + description: Modalidade de participação do servidor no PGD, com valores padronizados para análise. + - name: unidade_exercicio + description: Unidade de exercício tratada para exibição nas análises de hierarquia. + - name: nome_municipio_uorg + description: Município da unidade organizacional associado à sigla da unidade. + - name: cod_escolaridade_principal + description: Código da principal escolaridade cadastrada para o servidor. + - name: nome_escolaridade_principal + description: Nome da principal escolaridade cadastrada para o servidor. + - name: nome_deficiencia_fisica + description: Descrição da deficiência física cadastrada para o servidor, quando houver. + - name: nome_cargo_emprego + description: Nome do cargo ou emprego do servidor conforme dados detalhados. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + - name: resumo_quadro_pessoal + description: > + Tabela resumo com a distribuição dos servidores públicos por cargo efetivo, gênero, + situação funcional e unidade federativa da unidade de exercício (UF). A granularidade é agregada, + e cada linha representa uma combinação única desses atributos, com a respectiva contagem de servidores. + + meta: + tags: + - gold + columns: + - name: cargo_efetivo + description: > + Nome do cargo efetivo ocupado pelo servidor. Caso não informado, é preenchido com 'N/A'. + - name: genero + description: > + Gênero do servidor (masculino, feminino ou outro), conforme informado no cadastro pessoal. + Em casos de ausência de informação, é preenchido com 'N/A'. + - name: situacao_funcional + description: > + Situação funcional atual do servidor ( Ativo, Aposentado, Cedido, etc). + Valores ausentes são substituídos por 'N/A'. + - name: localidade_uf + description: > + Unidade Federativa (UF) da unidade organizacional onde o servidor exerce suas funções. + Valores ausentes são preenchidos com 'N/A'. + - name: quantidade_servidores + description: > + Quantidade total de servidores distintos (com base no CPF) que se enquadram na combinação dos atributos anteriores. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + - name: distribuicao_genero + description: > + Tabela agregada que apresenta a distribuição percentual de servidores por gênero, + enriquecida com informações de modalidade PGD, escolaridade e deficiência física. + A análise considera apenas servidores ativos com dados completos nas bases do SIAPE, + SIORG e dados de uorg. A granularidade é por gênero, com cálculo de percentual em relação ao total. + + meta: + tags: + - gold + columns: + - name: genero + description: Gênero do servidor (masculino, feminino, etc.) conforme cadastrado nos dados pessoais. + - name: quantidade_servidores + description: Quantidade total de servidores pertencentes a este gênero. + - name: percentual_distribuicao + description: > + Percentual de servidores deste gênero em relação ao total de servidores, + calculado como COUNT(*) * 1.0 / SUM(COUNT(*)) OVER (). + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + - name: distribuicao_raca_cor + description: > + Tabela agregada que apresenta a distribuição de servidores por raça/cor, + enriquecida com informações de modalidade PGD, escolaridade e deficiência física. + A análise considera apenas servidores ativos com dados completos nas bases do SIAPE, + SIORG e dados de uorg. A granularidade é por raça/cor, ordenada pela quantidade de servidores. + + meta: + tags: + - gold + columns: + - name: cor_raca + description: > + Cor ou raça do servidor (branca, parda, preta, amarela, indígena, não informada) + conforme autodeclaração registrada nos dados pessoais do SIAPE. + - name: quantidade_servidores + description: Quantidade total de servidores que se autodeclararam desta cor/raça. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + - name: distribuicao_situacao_funcional + description: > + Tabela agregada que apresenta a distribuição de servidores por situação funcional, + categorizando e padronizando as diferentes situações registradas no SIAPE. + As situações funcionais são normalizadas em categorias como Ativo permanente, Cedido, + Requisitado, Aposentado, Pensionista, Cargo comissionado e Estagiário. + A análise inclui dados enriquecidos com informações de modalidade PGD e unidade organizacional. + + meta: + tags: + - gold + columns: + - name: situacao_funcional + description: > + Situação funcional normalizada do servidor (Ativo permanente, Cedido, Requisitado, + Aposentado, Pensionista, Cargo comissionado, Estagiário), derivada do campo + nome_situacao_funcional através de regras de categorização. + - name: situacao_funcional_original + description: > + Nome original da situação funcional conforme registrado no SIAPE, antes da normalização + (ATIVO PERMANENTE, ATIVO EM OUTRO ORGAO, APOSENTADO, etc.). + - name: quantidade_servidores + description: Quantidade total de servidores que se enquadram nesta combinação de situação funcional. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + - name: kpis_servidores + description: > + Tabela de KPIs (Key Performance Indicators) consolidados sobre o quadro de pessoal, + incluindo métricas de contagem total de servidores, servidores ativos permanentes, + aposentados, estagiários e terceirizados. Esta tabela fornece uma visão rápida e consolidada + dos principais indicadores quantitativos de recursos humanos da organização. + Cada linha representa um KPI específico com seu respectivo valor. + + meta: + tags: + - gold + - kpi + columns: + - name: kpi + description: > + Nome do indicador (total_servidores, servidores_ativos_permanentes, aposentados, + estagiarios, terceirizados). + - name: valor + description: > + Valor numérico do KPI, representando a contagem de CPFs ou IDs únicos conforme a métrica. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + - name: distribuicao_mapa_uf + description: > + Tabela com a distribuição geográfica completa de servidores por Unidade Federativa (UF). + Retorna todos os 27 estados brasileiros com suas respectivas contagens de servidores + e percentuais. Estados sem servidores aparecem com valor 0. Útil para visualizações + em mapas e análises de distribuição geográfica do quadro de pessoal. + + meta: + tags: + - gold + - dashboard + columns: + - name: sigla_uf + description: Sigla da Unidade Federativa (AC, AL, AM, AP, BA, CE, DF, ES, GO, MA, MG, MS, MT, PA, PB, PE, PI, PR, RJ, RN, RO, RR, RS, SC, SE, SP, TO). + - name: nome_uf + description: Nome completo da Unidade Federativa em maiúsculas. + - name: valor + description: Quantidade total de servidores com unidade de exercício neste estado. + - name: percentual + description: Percentual de servidores deste estado em relação ao total, formatado como string com símbolo '%'. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + - name: tabela_servidores_agregada + description: > + Tabela agregada de servidores com informações consolidadas por cargo, gênero, + situação funcional e localização (cidade/estado). Cada linha representa uma + combinação única desses atributos com a respectiva contagem de servidores. + Útil para análises detalhadas e exibição em tabelas de dashboard. + + meta: + tags: + - gold + - dashboard + columns: + - name: cargo + description: Nome do cargo ocupado pelo servidor. + - name: genero + description: Gênero do servidor normalizado (Masculino, Feminino). + - name: situacao + description: Situação funcional normalizada (Ativo Permanente, Aposentado, Ativo em outro órgão, Estagiário, Cedido/Requisitado). + - name: cidade + description: Nome do município da unidade de exercício formatado em título (primeira letra maiúscula). + - name: estado + description: Sigla da UF da unidade de exercício em maiúsculas. + - name: total + description: Quantidade total de servidores únicos (CPF) nesta combinação de atributos. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: distribuicao_raca_cor_sexo + description: > + Tabela agregada que apresenta a distribuição de servidores por raça/cor e sexo. + A análise utiliza dados da tabela de hierarquia para calcular a quantidade de servidores + por gênero em cada categoria de raça/cor e situação funcional. + O campo 'feminino' é multiplicado por -1 para facilitar visualizações em pirâmide. + meta: + tags: + - gold + columns: + - name: nome_cor + description: Cor ou raça do servidor conforme autodeclaração. + - name: feminino + description: Quantidade de servidores do sexo feminino (valor negativo). + - name: masculino + description: Quantidade de servidores do sexo masculino. + - name: nome_situacao_funcional + description: Descrição da situação funcional do servidor. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/tabela_servidores_agregada.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/tabela_servidores_agregada.sql new file mode 100644 index 00000000..2486bf81 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/gold/tabela_servidores_agregada.sql @@ -0,0 +1,61 @@ +-- Modelo para gerar tabela de servidores com agregação por cargo, gênero, situação e +-- localização +-- Agrupa os dados de servidores para visualização em tabelas detalhadas +with + servidores_completos as ( + select + df.cpf, + df.nome_cargo, + dp.nome_sexo as genero, + df.nome_situacao_funcional as situacao, + du.nome_municipio_uorg as cidade, + du.uf_uorg as estado, + greatest(df.dt_ingest, dp.dt_ingest, du.dt_ingest) as dt_ingest + from {{ ref("dados_funcionais") }} df + inner join {{ ref("dados_pessoais") }} dp on df.cpf = dp.cpf + inner join {{ ref("dados_uorg") }} du on df.sigla_uorg_exercicio = du.sigla_uorg + where + df.nome_cargo is not null + and dp.nome_sexo is not null + and df.nome_situacao_funcional is not null + and du.nome_municipio_uorg is not null + and du.uf_uorg is not null + ), + + servidores_agregados as ( + select + nome_cargo as cargo, + case + when upper(genero) = 'MASCULINO' + then 'Masculino' + when upper(genero) = 'FEMININO' + then 'Feminino' + else genero + end as genero, + case + when upper(situacao) = 'ATIVO PERMANENTE' + then 'Ativo Permanente' + when upper(situacao) = 'APOSENTADO' + then 'Aposentado' + when upper(situacao) = 'ATIVO EM OUTRO ORGAO' + then 'Ativo em outro órgão' + when upper(situacao) = 'ESTAGIARIO SIGEPE' + then 'Estagiário' + when + upper(situacao) like '%CEDIDO%' + or upper(situacao) like '%REQUISITADO%' + then 'Cedido/Requisitado' + else situacao + end as situacao, + initcap(cidade) as cidade, + upper(estado) as estado, + count(distinct cpf) as total, + max(dt_ingest) as dt_ingest + from servidores_completos + group by nome_cargo, genero, situacao, cidade, estado + ) + +select cargo, genero, situacao, cidade, estado, total, dt_ingest +from servidores_agregados +where total > 0 +order by total desc, cargo, genero diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/afastamento_consolidado.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/afastamento_consolidado.sql new file mode 100644 index 00000000..6feb49e1 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/afastamento_consolidado.sql @@ -0,0 +1,120 @@ +with + + dados_afastamento_totais as ( + select distinct + adiantamento_salario_ferias, + ano_exercicio, + dt_fim, + dt_fim_aquisicao, + dt_ini, + dt_inicio_aquisicao, + dt_inicio_ferias_interrompidas, + dias_restantes, + gratificacao_natalina, + numero_parcela, + parcela_continuacao_interrupcao, + parcela_interrompida, + qtde_dias, + cpf, + cod_diploma_afastamento, + cod_ocorrencia, + dt_publicacao_afastamento, + desc_diploma_afastamento, + desc_ocorrencia, + numero_diploma_afastamento, + gr_matricula, + 'dados_afastamento' as origem_dados, -- identificar a fonte, + dt_ingest + from {{ ref("dados_afastamento") }} + + union all + + select distinct + adiantamento_salario_ferias, + ano_exercicio, + dt_fim, + dt_fim_aquisicao, + dt_ini, + dt_inicio_aquisicao, + dt_inicio_ferias_interrompidas, + dias_restantes, + gratificacao_natalina, + numero_parcela, + parcela_continuacao_interrupcao, + parcela_interrompida, + qtde_dias, + cpf, + cod_diploma_afastamento, + cod_ocorrencia, + dt_publicacao_afastamento, + desc_diploma_afastamento, + desc_ocorrencia, + numero_diploma_afastamento, + null as gr_matricula, -- não tem na afastamneto historico ... + 'afastamento_historico' as origem_dados, -- identificar a fonte, + dt_ingest + from {{ ref("afastamento_historico") }} + ), + + nomes_dt as (select distinct nome_pessoa, cpf from {{ ref("dados_pessoais") }}), + + funcoes_chefia as ( + select distinct cpf, cod_funcao, sigla_uorg_exercicio + from {{ ref("dados_funcionais") }} + ), + + -- Retirando duplicatas entre afastamento_historico e dados_afastamento + grupamentos as ( + select *, rank() over (partition by cpf order by dt_ini) as ordenacao + from dados_afastamento_totais + ), + + prioridades as ( + select + *, + row_number() over ( + partition by cpf, ordenacao + order by + case + when origem_dados = 'dados_afastamento' + then 1 + when origem_dados = 'afastamento_historico' + then 2 + end + ) as prioridade + from grupamentos + ), + + resultado as ( + select + adiantamento_salario_ferias, + ano_exercicio, + dt_fim, + dt_fim_aquisicao, + dt_ini, + dt_inicio_aquisicao, + dt_inicio_ferias_interrompidas, + dias_restantes, + gratificacao_natalina, + numero_parcela, + parcela_continuacao_interrupcao, + parcela_interrompida, + qtde_dias, + cpf, + cod_diploma_afastamento, + cod_ocorrencia, + dt_publicacao_afastamento, + desc_diploma_afastamento, + desc_ocorrencia, + numero_diploma_afastamento, + gr_matricula, + origem_dados, + dt_ingest + from prioridades + where prioridade = 1 + ) + +select * +from resultado +left join nomes_dt using (cpf) +left join funcoes_chefia using (cpf) diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/dados_funcionais_enriquecidos.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/dados_funcionais_enriquecidos.sql new file mode 100644 index 00000000..04351d5f --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/dados_funcionais_enriquecidos.sql @@ -0,0 +1,89 @@ +select distinct + df.cod_atividade_funcao, + df.cod_funcao, + df.cod_jornada, + df.cod_ocorr_ingresso_orgao, + df.cod_ocorr_ingresso_serv_publico, + df.cod_orgao, + df.cod_padrao, + df.cod_situacao_funcional, + df.cod_uorg_exercicio, + df.cod_upag, + df.cod_orgao_origem, + df.cpf_chefia_imediata, + df.dt_exercicio_no_orgao, + df.dt_fim_vale_ar, + df.dt_ingresso_funcao, + df.dt_ocorr_ingresso_orgao, + df.dt_ocorr_ingresso_serv_publico, + df.email_chefia_imediata, + df.email_institucional, + df.email_servidor, + df.ident_unica, + df.matricula_siape, + df.modalidade_pgd, + df.nome_atividade_funcao, + df.nome_chefe_uorg, + df.nome_funcao, + df.nome_jornada, + df.nome_ocorr_ingresso_orgao, + df.nome_ocorr_ingresso_serv_publico, + df.nome_orgao, + df.nome_regime_juridico, + df.nome_situacao_funcional, + df.nome_uorg_exercicio, + df.nome_upag, + df.participa_pgd, + df.percentual_ts, + df.sigla_orgao, + df.sigla_orgao_origem, + df.sigla_regime_juridico, + df.sigla_uorg_exercicio, + df.sigla_upag, + df.cpf, + df.cod_cargo, + df.cod_classe, + df.cod_ocorr_aposentadoria, + df.dt_ini_vale_ar, + df.dt_ocorr_aposentadoria, + df.nome_cargo, + df.nome_classe, + df.nome_ocorr_aposentadoria, + df.sigla_nivel_cargo, + df.tipo_vale_ar, + df.cod_ocorr_isencao_ir, + df.dt_ini_ocorr_isencao_ir, + df.nome_ocorr_isencao_ir, + df.cod_uorg_lotacao, + df.nome_uorg_lotacao, + df.sigla_uorg_lotacao, + df.dt_fim_ocorr_isencao_ir, + df.cod_ocorr_exclusao, + df.dt_ocorr_exclusao, + df.nome_ocorr_exclusao, + df.dt_uorg_lotacao, + df.cod_vale_transporte, + df.valor_vale_transporte, + df.dt_uorg_exercicio, + df.pontuacao_desempenho, + case + when df.modalidade_pgd is null + then 'Não participa' + when df.modalidade_pgd = 'parcial' + then 'Parcial' + when df.modalidade_pgd = 'integral' + then 'Integral' + when df.modalidade_pgd = 'presencial' + then 'Presencial' + when df.modalidade_pgd = 'no exterior' + then 'No exterior' + end as pdg, + case + when df.nome_situacao_funcional = 'ATIVO EM OUTRO ORGAO' + then 'Ativo em outro órgão' + else df.sigla_uorg_exercicio + end as unidade_exercicio, + du.nome_municipio_uorg, + greatest(df.dt_ingest, du.dt_ingest) as dt_ingest +from {{ ref("dados_funcionais") }} as df +inner join {{ ref("dados_uorg") }} as du on df.sigla_uorg_exercicio = du.sigla_uorg diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/quantitativo_alocados_ocupados.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/quantitativo_alocados_ocupados.sql new file mode 100644 index 00000000..a1f3ba38 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/quantitativo_alocados_ocupados.sql @@ -0,0 +1,108 @@ +with + siape_sem_duplicatas as (select distinct * from {{ ref("dados_funcionais") }}), + siorg_sem_duplicatas as ( + select distinct * from {{ ref("estrutura_organizacional_cargos") }} + ), + + codigos_siorg as ( + select funcao, nomeunidade, siglaunidade, denominacao, count(*) as qtd_vagas_cargo, max(dt_ingest) as dt_ingest + from siorg_sem_duplicatas + group by funcao, nomeunidade, siglaunidade, denominacao + ), + + codigos_siape as ( + select + cod_funcao, + nome_uorg_exercicio, + sigla_uorg_exercicio, + nome_cargo, + count(*) as qtd_vagas_ocupadas, + max(dt_ingest) as dt_ingest + from siape_sem_duplicatas + where cod_funcao is not null and dt_ocorr_aposentadoria is null + group by cod_funcao, nome_uorg_exercicio, sigla_uorg_exercicio, nome_cargo + ), + + codigo_siorg_combinado as ( + select + replace(funcao, ' ', '') as funcao, + nomeunidade, + case + when siglaunidade = 'GABIN-IPEA' then 'GABIN' else siglaunidade + end as siglaunidade, + denominacao, + substring(replace(funcao, ' ', ''), 1, 1) || substring( + replace(funcao, ' ', ''), length(replace(funcao, ' ', '')) - 2, 3 + ) as codigo_combinacao_siorg, + qtd_vagas_cargo, + dt_ingest + from codigos_siorg + ), + + codigo_siape_combinado as ( + select + cod_funcao, + nome_uorg_exercicio, + sigla_uorg_exercicio, + nome_cargo, + substring(cod_funcao, 1, 1) || substring( + cod_funcao, length(cod_funcao) - 2, 3 + ) as codigo_combinacao_siape, + qtd_vagas_ocupadas, + dt_ingest + from codigos_siape + ), + + primeira_correlacao as ( + select + siorg.funcao, + siorg.nomeunidade, + siorg.siglaunidade, + siorg.denominacao, + siorg.codigo_combinacao_siorg, + siorg.qtd_vagas_cargo, + siape.cod_funcao, + siape.nome_uorg_exercicio, + siape.sigla_uorg_exercicio, + siape.nome_cargo, + siape.codigo_combinacao_siape, + siape.qtd_vagas_ocupadas, + greatest(siorg.dt_ingest, siape.dt_ingest) as dt_ingest, + case + when + siorg.codigo_combinacao_siorg is not null + and siape.codigo_combinacao_siape is not null + then 'inner' + when + siorg.codigo_combinacao_siorg is not null + and siape.codigo_combinacao_siape is null + then 'left' + when + siorg.codigo_combinacao_siorg is null + and siape.codigo_combinacao_siape is not null + then 'right' + end as tipo_correlacao + from codigo_siorg_combinado as siorg + full join + codigo_siape_combinado as siape + on siorg.codigo_combinacao_siorg = siape.codigo_combinacao_siape + and siorg.siglaunidade = siape.sigla_uorg_exercicio + ) + +select + cod_funcao as codigo_siape, + funcao as codigo_siorg, + coalesce(nomeunidade, nome_uorg_exercicio) as nomeunidade, + coalesce(siglaunidade, sigla_uorg_exercicio) as siglaunidade, + coalesce(denominacao, nome_cargo) as nome_cargo, + qtd_vagas_cargo, + coalesce(qtd_vagas_ocupadas, 0) as qtd_vagas_ocupadas, + case + when qtd_vagas_cargo is null + then null + when qtd_vagas_ocupadas is null + then qtd_vagas_cargo + else (qtd_vagas_cargo - qtd_vagas_ocupadas) + end as qtd_cargos_vagos, + dt_ingest +from primeira_correlacao diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/resumo_atividade_.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/resumo_atividade_.sql new file mode 100644 index 00000000..15fc04e0 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/resumo_atividade_.sql @@ -0,0 +1,15 @@ +with dados_funcionais_extract as ( + select + sigla_nivel_cargo as nivel, + cod_classe || '-' || cod_padrao as classe_padrao, + nome_situacao_funcional, + nome_cargo, + count(1) as qtd, + max(dt_ingest) as dt_ingest + from {{ ref('dados_funcionais') }} + where nome_situacao_funcional != 'APOSENTADO' + group by 1, 2, 3, 4 +) + +select * +from dados_funcionais_extract diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/schema.yml b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/schema.yml new file mode 100644 index 00000000..d4306e28 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/schema.yml @@ -0,0 +1,387 @@ +version: 2 + +models: + + + # Silver + - name: dados_funcionais_enriquecidos + description: > + Tabela silver que enriquece os dados funcionais dos servidores com campos + padronizados de participação no PGD e informações da unidade organizacional + de exercício. + meta: + tags: + - silver + columns: + - name: cpf + description: CPF do servidor, com apenas dígitos. + - name: modalidade_pgd + description: Modalidade original de participação no Programa de Gestão e Desempenho (PGD). + - name: pdg + description: Modalidade de participação no PGD, com valores padronizados para análise. + - name: nome_situacao_funcional + description: Descrição da situação funcional do servidor. + - name: sigla_uorg_exercicio + description: Sigla da unidade de exercício. + - name: unidade_exercicio + description: Unidade de exercício tratada para exibição nas análises. + - name: nome_municipio_uorg + description: Município da unidade organizacional de exercício. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + - name: afastamento_consolidado + description: > + Tabela gold que consolida os registros de afastamento dos servidores, integrando as fontes + `dados_afastamento` e `afastamento_historico`, com enriquecimento de informações vindas de + `dados_pessoais` (nome do servidor) e `dados_funcionais` (função e unidade de exercício). + O processo inclui deduplicação inteligente priorizando os registros mais confiáveis, + utilizando lógica de ranking e partição por CPF e data de início do afastamento. + meta: + tags: + - silver + columns: + - name: adiantamento_salario_ferias + description: Indica se houve adiantamento de salário junto às férias no período do afastamento. + - name: ano_exercicio + description: Ano de exercício a que o afastamento se refere. + - name: dt_fim + description: Data de término do afastamento. + - name: dt_fim_aquisicao + description: Data final do período aquisitivo das férias relacionadas ao afastamento. + - name: dt_ini + description: Data de início do afastamento. + - name: dt_inicio_aquisicao + description: Data inicial do período aquisitivo de férias. + - name: dt_inicio_ferias_interrompidas + description: Data de início das férias interrompidas, se houver. + - name: dias_restantes + description: Quantidade de dias restantes de férias ou afastamento. + - name: gratificacao_natalina + description: Indica se houve gratificação natalina no período do afastamento. + - name: numero_parcela + description: Número da parcela relacionada ao afastamento. + - name: parcela_continuacao_interrupcao + description: Indica se o afastamento é continuação ou interrupção de uma parcela anterior. + - name: parcela_interrompida + description: Indica se a parcela foi interrompida. + - name: qtde_dias + description: Quantidade total de dias do afastamento. + - name: cpf + description: CPF do servidor. + - name: cod_diploma_afastamento + description: Código do diploma legal que ampara o afastamento. + - name: cod_ocorrencia + description: Código da ocorrência de afastamento. + - name: dt_publicacao_afastamento + description: Data de publicação do afastamento no diário oficial ou sistema correspondente. + - name: desc_diploma_afastamento + description: Descrição textual do diploma legal de afastamento. + - name: desc_ocorrencia + description: Descrição da ocorrência relacionada ao afastamento. + - name: numero_diploma_afastamento + description: Número do diploma legal do afastamento. + - name: gr_matricula + description: Número da matrícula do servidor no sistema GRH, quando disponível. + - name: origem_dados + description: Origem do dado de afastamento (`dados_afastamento` ou `afastamento_historico`). + - name: nome_pessoa + description: Nome completo do servidor, obtido a partir da tabela de dados pessoais. + - name: cod_funcao + description: Código da função de chefia ou cargo de confiança ocupado pelo servidor no momento do afastamento. + - name: sigla_uorg_exercicio + description: Sigla da unidade organizacional onde o servidor exercia suas funções. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + + - name: quantitativo_alocados_ocupados + description: > + Tabela gold que consolida e compara os quantitativos de cargos ocupados e cargos previstos + em funções de chefia, correlacionando os dados dos sistemas SIAPE e SIORG. A lógica aplica + uma combinação heurística de códigos de função com base na estrutura dos códigos e siglas + das unidades organizacionais, com objetivo de identificar inconsistências, sobras ou + vacâncias entre as vagas disponíveis e as efetivamente ocupadas. + meta: + tags: + - silver + columns: + - name: codigo_siape + description: Código da função do servidor registrado no SIAPE (`cod_funcao`), representando funções ocupadas. + - name: codigo_siorg + description: Código da função de chefia conforme registrado no SIORG (`funcao`), representando vagas previstas. + - name: nomeunidade + description: Nome da unidade organizacional, obtido da base SIORG ou SIAPE (com prioridade para SIORG). + - name: siglaunidade + description: Sigla da unidade organizacional, unificada a partir das duas fontes (com prioridade para SIORG). + - name: nome_cargo + description: Nome ou denominação do cargo/função de chefia, com base em SIORG ou SIAPE. + - name: qtd_vagas_cargo + description: Quantidade de vagas previstas para o cargo de chefia na estrutura organizacional (dados do SIORG). + - name: qtd_vagas_ocupadas + description: Quantidade de cargos de chefia efetivamente ocupados, com base nos registros de servidores no SIAPE. + - name: qtd_cargos_vagos + description: > + Diferença entre o número de vagas previstas (`qtd_vagas_cargo`) e as ocupadas (`qtd_vagas_ocupadas`). + Indica o total de cargos vagos. Retorna `null` se a quantidade de vagas previstas não estiver disponível. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + - name: servidores_detalhados + description: > + Tabela gold que consolida dados pessoais, funcionais, educacionais e organizacionais dos servidores. + Integra informações de múltiplas fontes do SIAPE, combinando registros de identificação, vínculo funcional, + escolaridade, endereço da unidade de exercício e metadados de transações. Essa visão é útil para análises + completas do perfil dos servidores ativos e inativos, permitindo estudos sociodemográficos, funcionais e + institucionais detalhados. + meta: + tags: + - silver + columns: + # Dados pessoais + - name: cpf + description: CPF do servidor, utilizado como chave primária de junção entre as bases. + - name: nome_pessoa + description: Nome completo do servidor. + - name: dt_nascimento + description: Data de nascimento do servidor. + - name: nome_cor + description: Cor/raça autodeclarada do servidor. + - name: nome_estado_civil + description: Estado civil declarado. + - name: nome_mae + description: Nome da mãe do servidor. + - name: nome_pai + description: Nome do pai do servidor. + - name: nome_municipio_nascimento + description: Município de nascimento do servidor. + - name: nome_nacionalidade + description: Nacionalidade declarada. + - name: nome_sexo + description: Sexo/gênero do servidor. + + # Dados funcionais + - name: matricula_siape + description: Matrícula SIAPE do servidor. + - name: nome_funcao + description: Nome da função de chefia ou cargo comissionado exercido, se houver. + - name: cod_funcao + description: Código da função exercida. + - name: nome_cargo + description: Nome do cargo efetivo. + - name: cod_cargo + description: Código do cargo efetivo. + - name: nome_jornada + description: Jornada de trabalho do servidor. + - name: dt_ingresso_funcao + description: Data de ingresso na função atual. + - name: nome_regime_juridico + description: Regime jurídico do vínculo funcional. + - name: nome_situacao_funcional + description: Situação funcional (ativo, cedido, aposentado etc). + - name: participa_pgd + description: Indica se o servidor participa do Programa de Gestão (teletrabalho). + + # Escolaridade e titulação + - name: nome_escolaridade_principal + description: Nível de escolaridade mais elevado do servidor. + - name: nome_titulacao_principal + description: Título acadêmico mais elevado do servidor ( mestrado, doutorado). + + # Unidade organizacional + - name: nome_uorg_exercicio + description: Nome da unidade organizacional onde o servidor exerce atualmente. + - name: sigla_uorg_exercicio + description: Sigla da unidade de exercício. + - name: nome_uorg_lotacao + description: Nome da unidade de lotação original. + - name: sigla_uorg_lotacao + description: Sigla da unidade de lotação. + - name: nome_orgao_funcional + description: Nome do órgão funcional ao qual o servidor está vinculado. + - name: sigla_orgao_funcional + description: Sigla do órgão funcional. + + # Endereço da unidade + - name: logradouro_uorg + description: Nome do logradouro da unidade. + - name: numero_endereco_uorg + description: Número do endereço da unidade. + - name: complemento_endereco_uorg + description: Complemento do endereço. + - name: bairro_uorg + description: Bairro onde a unidade está localizada. + - name: cep_uorg + description: CEP da unidade. + - name: nome_municipio_uorg + description: Município da unidade. + - name: uf_uorg + description: Unidade federativa da unidade. + + # Contatos institucionais + - name: email_institucional + description: Email institucional do servidor. + - name: email_servidor + description: Email alternativo do servidor. + - name: email_chefia_imediata + description: Email da chefia imediata. + - name: telefone_uorg + description: Telefone da unidade de exercício. + - name: fax_uorg + description: Fax da unidade de exercício. + + # Metadados e auditoria + - name: ident_unica_funcional + description: Identificador único funcional, usado para rastreamento interno. + - name: dt_ultima_transacao_servidor + description: Data da última transação registrada no sistema para o servidor. + + # Informações complementares + - name: cod_ocorr_aposentadoria + description: Código da ocorrência de aposentadoria (se houver). + - name: dt_ocorr_aposentadoria + description: Data da aposentadoria. + - name: cod_vale_transporte + description: Código do vale transporte. + - name: valor_vale_transporte + description: Valor mensal do vale transporte recebido. + - name: pontuacao_desempenho + description: Pontuação de desempenho do servidor, se aplicável. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + + + - name: tabela_correlacao_cargos + description: > + Tabela final que correlaciona cargos entre os sistemas SIAPE e SIORG, utilizando + combinações de códigos e unidades organizacionais. Cada linha representa um servidor + com informações enriquecidas da função e da chefia, além da hierarquia do cargo e + dados pessoais básicos. Essa tabela é útil para analisar discrepâncias, estruturar + a hierarquia funcional e verificar se as funções atribuídas no SIAPE estão + corretamente refletidas no SIORG. + + meta: + tags: + - silver + columns: + - name: codigo_siape + description: Código da função no SIAPE. + - name: codigo_siorg + description: Código da função no SIORG. + - name: codigo_combinacao_siape + description: Código combinado utilizado para comparar cargos no SIAPE. + - name: codigo_combinacao_siorg + description: Código combinado utilizado para comparar cargos no SIORG. + - name: matricula_siape + description: Matrícula do servidor no sistema SIAPE. + - name: cpf + description: CPF do servidor titular do cargo. + - name: cpf_chefia_imediata + description: CPF da chefia imediata do servidor. + - name: cod_situacao_funcional + description: Código da situação funcional do servidor no SIAPE. + - name: nome_situacao_funcional + description: Descrição da situação funcional. + - name: hierarquia_cargo + description: Valor numérico que representa a posição hierárquica do cargo. Quanto menor, mais alto o cargo. + - name: servidor + description: Nome do servidor titular do cargo. + - name: dt_nascimento + description: Data de nascimento do servidor. + - name: nome_sexo + description: Sexo do servidor. + - name: nome_estado_civil + description: Estado civil do servidor. + - name: nome_nacionalidade + description: Nacionalidade do servidor. + - name: nome_cor + description: Cor/raça autodeclarada pelo servidor. + - name: uf_nascimento + description: Unidade Federativa (UF) de nascimento. + - name: nome_municipio_nascimento + description: Município de nascimento do servidor. + - name: nome_chefia + description: Nome da chefia imediata, obtido pelo CPF da chefia. + - name: codigounidade + description: Código da unidade organizacional onde o cargo está lotado. + - name: codigounidadepai + description: Código da unidade organizacional imediatamente superior. + - name: caminho_unidade + description: Caminho hierárquico da unidade organizacional, representando sua posição na estrutura. + - name: ordem_grandeza + description: Nível de profundidade da unidade na hierarquia institucional. + - name: nomeunidade + description: Nome da unidade organizacional. + - name: siglaunidade + description: Sigla da unidade organizacional. + - name: nome_cargo + description: Denominação do cargo ocupado, conforme SIAPE ou SIORG. + - name: servidores_carreira + description: Classificação se o cargo é de carreira ou nomeação livre. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + - name: unidades_organizacionais_siorg_siape + description: > + Tabela que correlaciona as unidades organizacionais do SIAPE e do SIORG, + com base na correspondência entre os códigos e siglas das unidades. + Essa tabela é utilizada para identificar quais unidades estão presentes + em ambos os sistemas, apenas no SIAPE ou apenas no SIORG. + + meta: + tags: + - silver + columns: + - name: nome_unidade + description: Nome da unidade organizacional, proveniente do SIAPE ou SIORG. + - name: sigla_uorg + description: Sigla da unidade organizacional. Pode vir do SIAPE (dados_uorg) ou do SIORG (unidade_organizacional). + - name: codigo_unidade_siape + description: Código da unidade organizacional segundo o SIAPE (extraído de dados_uorg). + - name: codigo_unidade_siorg + description: Código da unidade organizacional segundo o SIORG (coluna codigounidade). + - name: tipo_correlacao + description: > + Tipo de correspondência encontrada entre os sistemas: + - "ambos": unidade presente no SIAPE e SIORG. + - "apenas_siorg": presente apenas no SIORG. + - "apenas_siape": presente apenas no SIAPE. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + + - name: resumo_atividade_ + description: > + Tabela que consolida o quantitativo de servidores ativos por nível de cargo, classe/padrão, situação funcional e nome do cargo. + Exclui os servidores com situação funcional 'APOSENTADO'. Derivada do modelo `dados_funcionais`. + meta: + tags: + - silver + columns: + - name: nivel + description: > + Sigla do nível do cargo (ex: DAS, FG, etc.). + - name: classe_padrao + description: Combinação de código de classe e padrão no formato 'COD_CLASSE-COD_PADRAO'. + - name: nome_situacao_funcional + description: Situação funcional do servidor (excluindo APOSENTADO). + - name: nome_cargo + description: Nome do cargo efetivo do servidor. + - name: qtd + description: Quantidade de servidores nessa combinação de atributos. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/servidores_completos.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/servidores_completos.sql new file mode 100644 index 00000000..b92ada3d --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/servidores_completos.sql @@ -0,0 +1,101 @@ +-- Modelo intermediário que centraliza os enriquecimentos de dados de servidores +-- Combina informações de hierarquia, dados funcionais, organizacionais e pessoais +-- Este modelo evita duplicação de código nos modelos gold +with + hierarquia_enriquecida as ( + select + ph.codigo_siape, + ph.codigo_siorg, + ph.codigo_combinacao_siape, + ph.codigo_combinacao_siorg, + ph.matricula_siape, + ph.cpf, + ph.cpf_chefia_imediata, + ph.cod_situacao_funcional, + ph.nome_situacao_funcional, + ph.hierarquia_cargo, + ph.servidor, + ph.dt_nascimento, + ph.nome_sexo, + ph.nome_estado_civil, + ph.nome_nacionalidade, + ph.nome_cor, + ph.uf_nascimento, + ph.nome_municipio_nascimento, + ph.nome_chefia, + ph.codigounidade, + ph.codigounidadepai, + ph.caminho_unidade, + ph.ordem_grandeza, + ph.nomeunidade, + ph.siglaunidade, + ph.nome_cargo, + ph.servidores_carreira, + ph.dt_ingest as dt_ingest_ph, + df.dt_ingest as dt_ingest_df, + case + when df.modalidade_pgd is null + then 'Não participa' + when df.modalidade_pgd = 'parcial' + then 'Parcial' + when df.modalidade_pgd = 'integral' + then 'Integral' + when df.modalidade_pgd = 'presencial' + then 'Presencial' + when df.modalidade_pgd = 'no exterior' + then 'No exterior' + end as pdg, + case + when ph.nome_situacao_funcional = 'ATIVO EM OUTRO ORGAO' + then 'Ativo em outro órgão' + else siglaunidade + end as unidade_exercicio + from {{ ref("hierarquia") }} ph + inner join {{ ref("dados_funcionais") }} df on ph.cpf = df.cpf + ), + + servidores_enriquecidos as ( + select distinct ph.*, du.nome_municipio_uorg, du.dt_ingest as dt_ingest_du + from hierarquia_enriquecida ph + inner join {{ ref("dados_uorg") }} du on ph.siglaunidade = du.sigla_uorg + order by caminho_unidade, hierarquia_cargo + ) + +select distinct + se.codigo_siape, + se.codigo_siorg, + se.codigo_combinacao_siape, + se.codigo_combinacao_siorg, + se.matricula_siape, + se.cpf, + se.cpf_chefia_imediata, + se.cod_situacao_funcional, + se.nome_situacao_funcional, + se.hierarquia_cargo, + se.servidor, + se.dt_nascimento, + se.nome_sexo, + se.nome_estado_civil, + se.nome_nacionalidade, + se.nome_cor, + se.uf_nascimento, + se.nome_municipio_nascimento, + se.nome_chefia, + se.codigounidade, + se.codigounidadepai, + se.caminho_unidade, + se.ordem_grandeza, + se.nomeunidade, + se.siglaunidade, + se.nome_cargo, + se.servidores_carreira, + se.pdg, + se.unidade_exercicio, + se.nome_municipio_uorg, + sd.cod_escolaridade_principal, + sd.nome_escolaridade_principal, + sd.nome_deficiencia_fisica, + sd.nome_cargo as nome_cargo_emprego, + greatest(se.dt_ingest_ph, se.dt_ingest_df, se.dt_ingest_du, sd.dt_ingest) as dt_ingest +from servidores_enriquecidos se +inner join {{ ref("servidores_detalhados") }} sd on se.cpf = sd.cpf diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/servidores_detalhados.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/servidores_detalhados.sql new file mode 100644 index 00000000..03a04a6e --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/servidores_detalhados.sql @@ -0,0 +1,159 @@ +with + educacao_principal as ( + select cpf, cod_escolaridade, nome_escolaridade, cod_titulacao, nome_titulacao, dt_ingest as dt_ingest_ep + from {{ ref("dados_escolares") }} + ), + uorg_completo as ( + select + du.bairro_uorg, + du.cep_uorg, + du.codigo_matricula, + du.codigo_municipio_uorg, + du.codigo_orgao, + du.codigo_orgao_uorg, + du.email_uorg, + du.tipo_endereco_uorg, + du.logradouro_uorg, + du.nome_municipio_uorg, + du.nome_uorg, + du.telefone_uorg, + du.numero_endereco_uorg, + du.sigla_uorg, + du.uf_uorg, + du.cpf, + du.complemento_endereco_uorg, + du.fax_uorg, + du.dt_ingest as dt_ingest_du + -- lu.dt_ultima_transacao AS dt_ultima_transacao_uorg, os codigos não batem e a + -- informação aparentemente ja existe... + -- lu.nome AS nome_uorg_lista + from {{ ref("dados_uorg") }} du + -- LEFT JOIN {{ ref('lista_uorgs') }} lu + ) +select + dp.cpf, + dp.nome_pessoa, + dp.dt_nascimento, + dp.grupo_sanguineo, + dp.nome_cor, + dp.cod_cor, + dp.nome_estado_civil, + dp.cod_estado_civil, + dp.nome_mae, + dp.nome_pai, + dp.nome_municipio_nascimento, + dp.nome_nacionalidade, + dp.cod_nacionalidade, + dp.nome_sexo, + dp.cod_sexo, + dp.num_pispasep, + dp.uf_nascimento, + dp.cod_deficiencia_fisica, + dp.nome_deficiencia_fisica, + dp.dt_chegada_brasil, + dp.nome_pais_origem, + + df.cod_atividade_funcao, + df.cod_funcao, + df.cod_jornada, + df.cod_ocorr_ingresso_orgao, + df.cod_ocorr_ingresso_serv_publico, + df.cod_orgao as cod_orgao_funcional, + df.cod_padrao, + df.cod_situacao_funcional, + df.cod_uorg_exercicio, + df.cod_upag, + df.cod_orgao_origem, + df.cpf_chefia_imediata, + df.dt_exercicio_no_orgao, + df.dt_fim_vale_ar, + df.dt_ingresso_funcao, + df.dt_ocorr_ingresso_orgao, + df.dt_ocorr_ingresso_serv_publico, + df.email_chefia_imediata, + df.email_institucional, + df.email_servidor, + df.ident_unica as ident_unica_funcional, + df.matricula_siape, + df.modalidade_pgd, + df.nome_atividade_funcao, + df.nome_chefe_uorg, + df.nome_funcao, + df.nome_jornada, + df.nome_ocorr_ingresso_orgao, + df.nome_ocorr_ingresso_serv_publico, + df.nome_orgao as nome_orgao_funcional, + df.nome_regime_juridico, + df.nome_situacao_funcional, + df.nome_uorg_exercicio, + df.nome_upag, + df.participa_pgd, + df.percentual_ts, + df.sigla_orgao as sigla_orgao_funcional, + df.sigla_orgao_origem, + df.sigla_regime_juridico, + df.sigla_uorg_exercicio, + df.sigla_upag, + df.cod_cargo, + df.cod_classe, + df.cod_ocorr_aposentadoria, + df.dt_ini_vale_ar, + df.dt_ocorr_aposentadoria, + df.nome_cargo, + df.nome_classe, + df.nome_ocorr_aposentadoria, + df.sigla_nivel_cargo, + df.tipo_vale_ar, + df.cod_ocorr_isencao_ir, + df.dt_ini_ocorr_isencao_ir, + df.nome_ocorr_isencao_ir, + df.cod_uorg_lotacao, + df.nome_uorg_lotacao, + df.sigla_uorg_lotacao, + df.dt_fim_ocorr_isencao_ir, + df.cod_ocorr_exclusao, + df.dt_ocorr_exclusao, + df.nome_ocorr_exclusao, + df.dt_uorg_lotacao, + df.cod_vale_transporte, + df.valor_vale_transporte, + df.dt_uorg_exercicio, + df.pontuacao_desempenho, + + ep.nome_escolaridade as nome_escolaridade_principal, + ep.cod_escolaridade as cod_escolaridade_principal, + ep.nome_titulacao as nome_titulacao_principal, + ep.cod_titulacao as cod_titulacao_principal, + + ls.dt_ultima_transacao as dt_ultima_transacao_servidor, + + uorg_c.bairro_uorg, + uorg_c.cep_uorg, + uorg_c.codigo_matricula, + uorg_c.codigo_municipio_uorg, + uorg_c.codigo_orgao, + uorg_c.codigo_orgao_uorg, + uorg_c.email_uorg, + uorg_c.tipo_endereco_uorg, + uorg_c.logradouro_uorg, + uorg_c.nome_municipio_uorg, + uorg_c.nome_uorg, + uorg_c.telefone_uorg, + uorg_c.numero_endereco_uorg, + uorg_c.sigla_uorg, + uorg_c.uf_uorg, + uorg_c.complemento_endereco_uorg, + uorg_c.fax_uorg, + + greatest( + dp.dt_ingest, + df.dt_ingest, + ep.dt_ingest_ep, + uorg_c.dt_ingest_du + ) as dt_ingest + +from {{ ref("dados_pessoais") }} dp +left join {{ ref("dados_funcionais") }} df on dp.cpf = df.cpf +left join educacao_principal ep on dp.cpf = ep.cpf +left join {{ ref("lista_servidores") }} ls on dp.cpf = ls.cpf +left join uorg_completo uorg_c on dp.cpf = uorg_c.cpf diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/tabela_correlacao_cargos.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/tabela_correlacao_cargos.sql new file mode 100644 index 00000000..aa5e5bf3 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/tabela_correlacao_cargos.sql @@ -0,0 +1,187 @@ +with + + correcao_funcao as ( + select + *, + replace(funcao, ' ', '') as funcao_sigla, + dt_ingest as dt_ingest_estrutura + from {{ ref("estrutura_organizacional_cargos") }} + ), + + codigos_siorg as ( + select distinct + funcao_sigla, + eorg.nomeunidade, + eorg.codigounidade, + eorg.ordem_grandeza, + eorg.denominacao, + eorg.codigo_instancia, + eorg.dt_ingest_estrutura, + uo.codigounidadepai, + uo.caminho_unidade, + case + when eorg.siglaunidade = 'GABIN-IPEA' then 'GABIN' else siglaunidade + end as siglaunidade, + substring(funcao_sigla, length(funcao_sigla) - 2, 1) as categoria_cargo, + -- hierarquia do cargo está sendo definida a partir da fórmula: + -- (categoria do cargo * 1000) - nível do cargo + -- quanto menor a hierarquia, maior o cargo + right(funcao_sigla, 2) as nivel_cargo, + cast(substring(funcao_sigla, length(funcao_sigla) - 2, 1) as int) * 1000 + - cast(right(funcao, 2) as int) as hierarquia_cargo, + uo.dt_ingest as dt_ingest_uorg + from correcao_funcao as eorg + inner join + {{ ref("unidade_organizacional") }} as uo + on eorg.codigounidade = uo.codigounidade + ), + + codigos_siape as ( + select distinct + df.cod_funcao, + df.nome_uorg_exercicio, + df.sigla_uorg_exercicio, + df.nome_cargo, + df.matricula_siape, + df.cpf, + df.cpf_chefia_imediata, + df.cod_situacao_funcional, + df.nome_situacao_funcional, + dp.nome_pessoa, + dp.dt_nascimento, + dp.nome_sexo, + dp.nome_estado_civil, + dp.nome_nacionalidade, + dp.nome_cor, + dp.uf_nascimento, + dp.nome_municipio_nascimento, + uo.codigounidade as codigounidade_alternativa, + uo.caminho_unidade as caminho_unidade_alternativa, + uo.codigounidadepai as codigounidadepai_alternativa, + uo.ordem_grandeza as ordem_grandeza_alternativa, + substring(df.cod_funcao, 1, 1) || substring( + df.cod_funcao, length(df.cod_funcao) - 2, 3 + ) as codigo_combinacao_siape, + df.dt_ingest as dt_ingest_funcionais, + dp.dt_ingest as dt_ingest_pessoais, + uo.dt_ingest as dt_ingest_uorg_alt + from {{ ref("dados_funcionais") }} as df + left join {{ ref("dados_pessoais") }} as dp on df.cpf = dp.cpf + left join + {{ ref("unidade_organizacional") }} as uo + on df.sigla_uorg_exercicio = uo.sigla + where dt_ocorr_aposentadoria is null and cod_funcao is not null + ), + + -- select count(*) from codigos_siape; + codigo_siorg_combinado as ( + select + *, + substring(funcao_sigla, 1, 1) || substring( + funcao_sigla, length(funcao_sigla) - 2, 3 + ) as codigo_combinacao_siorg + from codigos_siorg + ), + + numeracao_cargos_siape as ( + select + *, + row_number() over ( + partition by codigo_combinacao_siape, sigla_uorg_exercicio + order by sigla_uorg_exercicio + ) as ordem_siape + from codigos_siape + ), + + numeracao_cargos_siorg as ( + select + *, + row_number() over ( + partition by codigo_combinacao_siorg, siglaunidade order by siglaunidade + ) as ordem_siorg + from codigo_siorg_combinado + ), + + primeira_correlacao as ( + select + *, + case + when + siorg.codigo_combinacao_siorg is not null + and siape.codigo_combinacao_siape is not null + then 'inner' + when + siorg.codigo_combinacao_siorg is not null + and siape.codigo_combinacao_siape is null + then 'left' + when + siorg.codigo_combinacao_siorg is null + and siape.codigo_combinacao_siape is not null + then 'right' + end as tipo_correlacao + from numeracao_cargos_siorg as siorg + full join + numeracao_cargos_siape as siape + on siorg.codigo_combinacao_siorg = siape.codigo_combinacao_siape + and siorg.siglaunidade = siape.sigla_uorg_exercicio + and siorg.ordem_siorg = siape.ordem_siape + ), + + -- select count(*) from primeira_correlacao + tabela_correlacao_cargos as ( + select distinct + pr.cod_funcao as codigo_siape, + pr.funcao_sigla as codigo_siorg, + pr.codigo_combinacao_siape, + pr.codigo_combinacao_siorg, + pr.matricula_siape as matricula_siape, + pr.cpf as cpf, + pr.cpf_chefia_imediata as cpf_chefia_imediata, + pr.cod_situacao_funcional as cod_situacao_funcional, + pr.nome_situacao_funcional as nome_situacao_funcional, + pr.hierarquia_cargo as hierarquia_cargo, + pr.nome_pessoa as servidor, + pr.dt_nascimento as dt_nascimento, + pr.nome_sexo as nome_sexo, + pr.nome_estado_civil as nome_estado_civil, + pr.nome_nacionalidade as nome_nacionalidade, + pr.nome_cor as nome_cor, + pr.uf_nascimento as uf_nascimento, + pr.nome_municipio_nascimento as nome_municipio_nascimento, + dp.nome_pessoa as nome_chefia, + coalesce( + cast(pr.codigounidade as text), cast(pr.codigounidade_alternativa as text) + ) as codigounidade, + coalesce( + cast(pr.codigounidadepai as text), + cast(pr.codigounidadepai_alternativa as text) + ) as codigounidadepai, + coalesce( + cast(pr.caminho_unidade as text), + cast(pr.caminho_unidade_alternativa as text) + ) as caminho_unidade, + coalesce( + cast(pr.ordem_grandeza as text), + cast(pr.ordem_grandeza_alternativa as text) + ) as ordem_grandeza, + coalesce(nomeunidade, nome_uorg_exercicio) as nomeunidade, + coalesce(siglaunidade, sigla_uorg_exercicio) as siglaunidade, + coalesce(denominacao, nome_cargo) as nome_cargo, + case + when cod_situacao_funcional = '04' then 'Nomeação livre' else 'Carreira' + end as servidores_carreira, + + greatest( + pr.dt_ingest_estrutura, + pr.dt_ingest_uorg, + pr.dt_ingest_funcionais, + pr.dt_ingest_pessoais, + pr.dt_ingest_uorg_alt + ) as dt_ingest + from primeira_correlacao as pr + left join {{ ref("dados_pessoais") }} as dp on pr.cpf_chefia_imediata = dp.cpf + order by caminho_unidade, hierarquia_cargo + ) + +select * +from tabela_correlacao_cargos diff --git a/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/unidades_organizacionais_siorg_siape.sql b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/unidades_organizacionais_siorg_siape.sql new file mode 100644 index 00000000..b5af5981 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/pessoas_dbt/silver/unidades_organizacionais_siorg_siape.sql @@ -0,0 +1,54 @@ +with + preparacao as ( + select + du.codigo_orgao::integer as codigo_orgao, + du.codigo_orgao_uorg as combinacao_codigo_lista, + (right(du.codigo_orgao_uorg, 7))::integer as codigo_lista_uorg, + du.sigla_uorg as sigla_uorg, + max(du.dt_ingest) as dt_ingest_du + from {{ ref("dados_uorg") }} du + group by 1, 2, 3, 4 + ), + + join_lista_uorgo_dados_uorg as ( + select + p.codigo_orgao, + p.combinacao_codigo_lista, + p.codigo_lista_uorg, + p.sigla_uorg, + p.dt_ingest_du, + lu.dt_ultima_transacao, + lu.nome as nome_unidade, + max(lu.dt_ingest) as dt_ingest_lu + from preparacao p + join {{ ref("lista_uorgs") }} lu on p.codigo_lista_uorg = lu.codigo + group by 1, 2, 3, 4, 5, 6, 7 + ), + + unidade_organizacional as ( + select distinct + *, case when sigla = 'GABIN-IPEA' then 'GABIN' else sigla end as sigla_unidade + from {{ ref("unidade_organizacional") }} + ), + + tabela_corralacao_uorgs as ( + select + coalesce(a.nome_unidade, uo.nome) as nome_unidade, + coalesce(a.sigla_uorg, sigla_unidade) as sigla_uorg, + a.codigo_lista_uorg as codigo_unidade_siape, + uo.codigounidade as codigo_unidade_siorg, + greatest(a.dt_ingest_du, a.dt_ingest_lu) as dt_ingest, + case + when a.nome_unidade is null and uo.nome is not null + then 'apenas_siorg' + when a.nome_unidade is not null and uo.nome is null + then 'apenas_siape' + when a.nome_unidade is not null and uo.nome is not null + then 'ambos' + end as tipo_correlacao + from join_lista_uorgo_dados_uorg a + full join unidade_organizacional uo on a.sigla_uorg = uo.sigla_unidade + ) + +select * +from tabela_corralacao_uorgs diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_bolsistas.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_bolsistas.sql new file mode 100644 index 00000000..19fe752a --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_bolsistas.sql @@ -0,0 +1,50 @@ +{{ config(materialized="table") }} + +with + bolsistas as ( + select + {{ safe_bigint('bolsistaid') }} as bolsistaid, + {{ safe_bigint('categoriaadicionalbolsistaid') }} as categoriaadicionalbolsistaid, + nomebolsista::text as nomebolsista, + {{ safe_date('datanascimento') }} as datanascimento, + regexp_replace(numerocpf, '[^0-9]', '', 'g')::text as numerocpf, + numerorg::text as numerorg, + expedidorrg::text as expedidorrg, + {{ safe_bigint('coordenadorid') }} as coordenadorid, + {{ safe_bigint('diretoriaid') }} as diretoriaid, + {{ safe_bigint('coordenacaoid') }} as coordenacaoid, + {{ safe_bigint('unidadeipeaid') }} as unidadeipeaid, + email1::text as email1, + email2::text as email2, + email3::text as email3, + telefone1::text as telefone1, + telefone2::text as telefone2, + telefone3::text as telefone3, + regexp_replace(residenciacep, '[^0-9]', '', 'g')::text as residenciacep, + residenciarua::text as residenciarua, + residencianumero::text as residencianumero, + residenciacomplemento::text as residenciacomplemento, + residenciabairro::text as residenciabairro, + bolsistaminicv::text as bolsistaminicv, + linktolattes::text as linktolattes, + {{ safe_bigint('ufid') }} as ufid, + {{ safe_bigint('municipioid') }} as municipioid, + {{ safe_date('bolsadatainicio') }} as bolsadatainicio, + {{ safe_date('bolsadatafinalizacao') }} as bolsadatafinalizacao, + {{ safe_bigint('categoriabolsistaid') }} as categoriabolsistaid, + {{ safe_bigint('projetoid') }} as projetoid, + {{ safe_bigint('sexoid') }} as sexoid, + {{ safe_bigint('situacaopessoaid') }} as situacaopessoaid, + {{ safe_bigint('statuscadastroid') }} as statuscadastroid, + {{ safe_bigint('custoemprojetoid') }} as custoemprojetoid, + login::text as login, + senha::text as senha, + {{ safe_bigint('usuarioid') }} as usuarioid, + {{ safe_bigint('idnosistemalegado') }} as idnosistemalegado, + matriculaipea::text as matriculaipea, + observacoes::text as observacoes + from {{ source("ipea_pro", "bolsistas") }} + ) + +select * +from bolsistas diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_coordenacoes.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_coordenacoes.sql new file mode 100644 index 00000000..6a3bd580 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_coordenacoes.sql @@ -0,0 +1,18 @@ +{{ config(materialized="table") }} + +with + coordenacoes as ( + select + {{ safe_bigint('coordenacaoid') }} as coordenacaoid, + coordenacaonome::text as coordenacaonome, + coordenacaosigla::text as coordenacaosigla, + coordenacaodescricao::text as coordenacaodescricao, + {{ safe_bigint('coordenadorid') }} as coordenadorid, + {{ safe_bigint('diretoriaid') }} as diretoriaid, + {{ safe_boolean('ativa') }} as ativa, + codigosiape::text as codigosiape + from {{ source("ipea_pro", "coordenacoes") }} + ) + +select * +from coordenacoes diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_custosemprojetos.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_custosemprojetos.sql new file mode 100644 index 00000000..9a77e449 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_custosemprojetos.sql @@ -0,0 +1,18 @@ +{{ config(materialized="table") }} + +with + custosemprojetos as ( + select + {{ safe_bigint('custoemprojetoid') }} as custoemprojetoid, + {{ safe_bigint('itemcustoid') }} as itemcustoid, + {{ safe_bigint('quantidadeitemcusto') }} as quantidadeitemcusto, + {{ safe_bigint('projetoid') }} as projetoid, + descricaoitemcusto::text as descricaoitemcusto, + {{ safe_date('datainicial') }} as datainicial, + {{ safe_date('datafinal') }} as datafinal, + {{ safe_numeric('custoespecifico') }} as custoespecifico + from {{ source("ipea_pro", "custosemprojetos") }} + ) + +select * +from custosemprojetos diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_diretorias.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_diretorias.sql new file mode 100644 index 00000000..dc97532e --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_diretorias.sql @@ -0,0 +1,17 @@ +{{ config(materialized="table") }} + +with + diretorias as ( + select + {{ safe_bigint('diretoriaid') }} as diretoriaid, + diretorianome::text as diretorianome, + diretoriasigla::text as diretoriasigla, + diretoriadescricao::text as diretoriadescricao, + {{ safe_bigint('diretorid') }} as diretorid, + {{ safe_bigint('diretoradjuntoid') }} as diretoradjuntoid, + codigosiape::text as codigosiape + from {{ source("ipea_pro", "diretorias") }} + ) + +select * +from diretorias diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_fontesreceitas.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_fontesreceitas.sql new file mode 100644 index 00000000..3ac5fb9c --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_fontesreceitas.sql @@ -0,0 +1,18 @@ +{{ config(materialized="table") }} + +with + fontesreceitas as ( + select + {{ safe_bigint('fontereceitaid') }} as fontereceitaid, + {{ safe_bigint('projetoid') }} as projetoid, + {{ safe_bigint('itemfontereceitaid') }} as itemfontereceitaid, + descricaofonte::text as descricaofonte, + {{ safe_numeric('valortotalfonte') }} as valortotalfonte, + {{ safe_date('datainiciofonte') }} as datainiciofonte, + {{ safe_date('datafinalfonte') }} as datafinalfonte, + {{ safe_numeric('valortotalfonte_bkp') }} as valortotalfonte_bkp + from {{ source("ipea_pro", "fontesreceitas") }} + ) + +select * +from fontesreceitas diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_grupoentidade.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_grupoentidade.sql new file mode 100644 index 00000000..11193ee2 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_grupoentidade.sql @@ -0,0 +1,12 @@ +{{ config(materialized="table") }} + +with + grupoentidade as ( + select + {{ safe_bigint('grupoentidadeid') }} as grupoentidadeid, + nome::text as nome + from {{ source("ipea_pro", "grupoentidade") }} + ) + +select * +from grupoentidade diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_insumofinanceiro.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_insumofinanceiro.sql new file mode 100644 index 00000000..3fa20feb --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_insumofinanceiro.sql @@ -0,0 +1,16 @@ +{{ config(materialized="table") }} + +with + insumofinanceiro as ( + select + {{ safe_bigint('insumofinanceiroid') }} as insumofinanceiroid, + item::text as item, + unidade::text as unidade, + descricao::text as descricao, + {{ safe_boolean('possuivalorunitario') }} as possuivalorunitario, + {{ safe_boolean('possuiquantidade') }} as possuiquantidade + from {{ source("ipea_pro", "insumofinanceiro") }} + ) + +select * +from insumofinanceiro diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_itemfontereceitas.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_itemfontereceitas.sql new file mode 100644 index 00000000..923d0352 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_itemfontereceitas.sql @@ -0,0 +1,14 @@ +{{ config(materialized="table") }} + +with + itemfontereceitas as ( + select + {{ safe_bigint('itemfontereceitaid') }} as itemfontereceitaid, + nomeitemfontereceita::text as nomeitemfontereceita, + descricaoitemfontereceita::text as descricaoitemfontereceita, + observacao::text as observacao + from {{ source("ipea_pro", "itemfontereceitas") }} + ) + +select * +from itemfontereceitas diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_itenscustos.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_itenscustos.sql new file mode 100644 index 00000000..54cecd46 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_itenscustos.sql @@ -0,0 +1,20 @@ +{{ config(materialized="table") }} + +with + itenscustos as ( + select + {{ safe_bigint('itemcustoid') }} as itemcustoid, + nomeitem::text as nomeitem, + descricaoitem::text as descricaoitem, + unidadeitem::text as unidadeitem, + {{ safe_numeric('valorunitarioitem') }} as valorunitarioitem, + {{ safe_bigint('statusitemcadastradoid') }} as statusitemcadastradoid, + {{ safe_bigint('categoriasitenscustoid') }} as categoriasitenscustoid, + {{ safe_bigint('ordem') }} as ordem, + {{ safe_boolean('ativo') }} as ativo, + {{ safe_boolean('padrao') }} as padrao + from {{ source("ipea_pro", "itenscustos") }} + ) + +select * +from itenscustos diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_projetos.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_projetos.sql new file mode 100644 index 00000000..fe189678 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_projetos.sql @@ -0,0 +1,44 @@ +{{ config(materialized="table") }} + +with + projetos as ( + select + {{ safe_bigint('projetoid') }} as projetoid, + {{ safe_bigint('categoriaadicionalprojetoid') }} as categoriaadicionalprojetoid, + tituloprojeto::text as tituloprojeto, + objetivofinalprojeto::text as objetivofinalprojeto, + justificativaprojeto::text as justificativaprojeto, + metodologiaprojeto::text as metodologiaprojeto, + cooperacao::text as cooperacao, + {{ safe_date('datainicio') }} as datainicio, + {{ safe_date('datafim') }} as datafim, + {{ safe_bigint('coordenadorid') }} as coordenadorid, + {{ safe_bigint('diretoriaid') }} as diretoriaid, + {{ safe_bigint('coordenacaoid') }} as coordenacaoid, + {{ safe_bigint('statusetapaid') }} as statusetapaid, + {{ safe_bigint('statusprojetoid') }} as statusprojetoid, + {{ safe_bigint('modalidadeprojetoid') }} as modalidadeprojetoid, + numeroprojeto::text as numeroprojeto, + numeroprojetonoano::text as numeroprojetonoano, + {{ safe_bigint('anoprojeto') }} as anoprojeto, + {{ safe_bigint('usuariocadastranteid') }} as usuariocadastranteid, + {{ safe_bigint('usuarioversaoid') }} as usuarioversaoid, + {{ safe_bigint('numeroversao') }} as numeroversao, + {{ safe_timestamp('horaversao') }} as horaversao, + {{ safe_boolean('projetoexcluido') }} as projetoexcluido, + {{ safe_bigint('usuarioexcluinteid') }} as usuarioexcluinteid, + {{ safe_bigint('rowversion') }} as rowversion, + {{ safe_boolean('projetoassessoria') }} as projetoassessoria, + {{ safe_boolean('projetoestrategico') }} as projetoestrategico, + {{ safe_date('datafimreal') }} as datafimreal, + {{ safe_bigint('statusprojetohistoricoid') }} as statusprojetohistoricoid, + palavras_chave::text as palavras_chave, + planocomunicacao::text as planocomunicacao, + justificativaprojetoestrategico::text as justificativaprojetoestrategico, + {{ safe_boolean('planejamentofinanceiroativo') }} as planejamentofinanceiroativo, + {{ safe_boolean('projetoprioritario') }} as projetoprioritario + from {{ source("ipea_pro", "projetos") }} + ) + +select * +from projetos diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_registrofinanceiroemprojetos.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_registrofinanceiroemprojetos.sql new file mode 100644 index 00000000..e7367edd --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_registrofinanceiroemprojetos.sql @@ -0,0 +1,18 @@ +{{ config(materialized="table") }} + +with + registrofinanceiroemprojetos as ( + select + {{ safe_bigint('registrofinanceiroid') }} as registrofinanceiroid, + descricaoinsumo::text as descricaoinsumo, + {{ safe_numeric('valorunitarioinsumo') }} as valorunitarioinsumo, + {{ safe_bigint('grupoentidadeid') }} as grupoentidadeid, + {{ safe_bigint('insumofinanceiroid') }} as insumofinanceiroid, + {{ safe_bigint('projetoid') }} as projetoid, + {{ safe_bigint('quantidadeinsumo') }} as quantidadeinsumo, + descricaofonte::text as descricaofonte + from {{ source("ipea_pro", "registrofinanceiroemprojetos") }} + ) + +select * +from registrofinanceiroemprojetos diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_servidorespublicos.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_servidorespublicos.sql new file mode 100644 index 00000000..5c71a323 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_servidorespublicos.sql @@ -0,0 +1,57 @@ +{{ config(materialized="table") }} + +with + servidorespublicos as ( + select + {{ safe_bigint('servidorpublicoid') }} as servidorpublicoid, + {{ safe_bigint('categoriaadicionalservidorid') }} as categoriaadicionalservidorid, + nomeservidor::text as nomeservidor, + {{ safe_date('datanascimento') }} as datanascimento, + regexp_replace(numerocpf, '[^0-9]', '', 'g')::text as numerocpf, + numerorg::text as numerorg, + expedidorrg::text as expedidorrg, + {{ safe_bigint('diretoriaid') }} as diretoriaid, + {{ safe_bigint('coordenacaoid') }} as coordenacaoid, + {{ safe_numeric('salarioatual') }} as salarioatual, + {{ safe_numeric('custodoservidor') }} as custodoservidor, + email1::text as email1, + email2::text as email2, + email3::text as email3, + telefone1::text as telefone1, + telefone2::text as telefone2, + telefone3::text as telefone3, + {{ safe_bigint('unidadeipeaid') }} as unidadeipeaid, + regexp_replace(residenciacep, '[^0-9]', '', 'g')::text as residenciacep, + residenciarua::text as residenciarua, + residencianumero::text as residencianumero, + residenciacomplemento::text as residenciacomplemento, + residenciabairro::text as residenciabairro, + servidorminicv::text as servidorminicv, + linktolattes::text as linktolattes, + {{ safe_bigint('ufid') }} as ufid, + {{ safe_bigint('municipioid') }} as municipioid, + {{ safe_bigint('dasid') }} as dasid, + {{ safe_bigint('categoriaid') }} as categoriaid, + matriculasiape::text as matriculasiape, + {{ safe_bigint('sexoid') }} as sexoid, + {{ safe_bigint('statuscadastroid') }} as statuscadastroid, + {{ safe_bigint('situacaoservidorid') }} as situacaoservidorid, + login::text as login, + senha::text as senha, + {{ safe_bigint('usuarioid') }} as usuarioid, + {{ safe_bigint('idnosistemalegado') }} as idnosistemalegado, + {{ safe_bigint('instituicaoid') }} as instituicaoid, + regexp_replace(trabalhocep, '[^0-9]', '', 'g')::text as trabalhocep, + trabalhorua::text as trabalhorua, + trabalhonumero::text as trabalhonumero, + trabalhocomplemento::text as trabalhocomplemento, + trabalhobairro::text as trabalhobairro, + {{ safe_bigint('trabalhoufid') }} as trabalhoufid, + {{ safe_bigint('trabalhomunicipioid') }} as trabalhomunicipioid, + {{ safe_bigint('cargocomissaoid') }} as cargocomissaoid, + ramal::text as ramal + from {{ source("ipea_pro", "servidorespublicos") }} + ) + +select * +from servidorespublicos diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_statusprojetos.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_statusprojetos.sql new file mode 100644 index 00000000..5f8c41ee --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/ipea_pro_statusprojetos.sql @@ -0,0 +1,14 @@ +{{ config(materialized="table") }} + +with + statusprojetos as ( + select + {{ safe_bigint('statusprojetoid') }} as statusprojetoid, + nomestatus::text as nomestatus, + descricaostatus::text as descricaostatus, + {{ safe_boolean('ativo') }} as ativo + from {{ source("ipea_pro", "statusprojetos") }} + ) + +select * +from statusprojetos diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/schema.yml b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/schema.yml new file mode 100644 index 00000000..61cb0b33 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/schema.yml @@ -0,0 +1,1304 @@ +version: 2 + +models: + - name: sgac_projetos_sgac + description: > + Tabela bronze com projetos do SGAC importados do SharePoint. Padroniza + valores textuais `NaN` para nulo, converte campos de data, timestamp, + booleanos e valores financeiros, extrai rótulos legíveis de objetos e + listas JSON do SharePoint, e remove marcações HTML dos campos descritivos. + meta: + tags: + - bronze + - sgac + columns: + - name: id_interno_item + description: Identificador interno do item no SharePoint. + - name: id + description: Identificador do projeto no SharePoint. + - name: titulo + description: Título do projeto. + - name: entidades_externas + description: Entidades externas envolvidas no projeto. + - name: instrumento + description: Tipo de instrumento extraído da referência SharePoint. + - name: instrumento_id + description: Identificador do instrumento no SharePoint. + - name: diretoria_responsavel + description: Diretoria responsável extraída da referência SharePoint. + - name: diretoria_responsavel_id + description: Identificador da diretoria responsável. + - name: objeto + description: Objeto do projeto com marcações HTML removidas. + - name: data_inicio + description: Data de início do projeto. + - name: data_vencimento + description: Data de vencimento do projeto. + - name: total_de_recursos + description: Valor total de recursos do projeto. + - name: numero_do_proc + description: Número do processo associado ao projeto. + - name: coordenador + description: Nome dos coordenadores extraídos da lista SharePoint. + - name: coordenador_json + description: Estrutura JSON original dos coordenadores. + - name: coordenador_claims + description: Claims dos coordenadores em JSON. + - name: nacionalidade + description: Nacionalidade extraída da lista SharePoint. + - name: nacionalidade_id + description: Identificadores de nacionalidade em JSON. + - name: recursos_orcamentarios + description: Valor de recursos orçamentários. + - name: recursos_nao_orcamentarios + description: Valor de recursos não orçamentários. + - name: status + description: Status do projeto extraído da referência SharePoint. + - name: status_id + description: Identificador do status. + - name: eixo_tematico + description: Eixos temáticos extraídos da lista SharePoint. + - name: eixo_tematico_id + description: Identificadores de eixo temático em JSON. + - name: predecessores + description: Projetos predecessores extraídos da lista SharePoint. + - name: predecessores_id + description: Identificadores de predecessores em JSON. + - name: prioridade + description: Prioridade extraída da referência SharePoint. + - name: prioridade_id + description: Identificador da prioridade. + - name: justificativa + description: Justificativa do projeto com marcações HTML removidas. + - name: objetivo_s_ge + description: Objetivos do projeto com marcações HTML removidas. + - name: equipe_tecnica + description: Nomes da equipe técnica extraídos da lista SharePoint. + - name: equipe_tecnica_json + description: Estrutura JSON original da equipe técnica. + - name: equipe_tecnica_claims + description: Claims da equipe técnica em JSON. + - name: codigo + description: Código do projeto, quando informado. + - name: unidades_envolvidas + description: Unidades envolvidas extraídas da lista SharePoint. + - name: unidades_envolvidas_id + description: Identificadores das unidades envolvidas em JSON. + - name: historico_observa_x0 + description: Histórico ou observações com marcações HTML removidas. + - name: a_solicitacao + description: Solicitações relacionadas extraídas da lista SharePoint. + - name: a_solicitacao_id + description: Identificadores de solicitação em JSON. + - name: modificado + description: Data e hora da última modificação no SharePoint. + - name: criado + description: Data e hora de criação no SharePoint. + - name: autor + description: Nome do autor do item no SharePoint. + - name: autor_email + description: E-mail do autor do item no SharePoint. + - name: autor_json + description: Estrutura JSON original do autor. + - name: autor_claims + description: Claims do autor. + - name: editor + description: Nome do último editor do item no SharePoint. + - name: editor_email + description: E-mail do último editor do item no SharePoint. + - name: editor_json + description: Estrutura JSON original do editor. + - name: editor_claims + description: Claims do editor. + - name: link + description: Link do item no SharePoint. + - name: nome + description: Nome do item no SharePoint. + - name: termos_aditivos + description: Termos aditivos informados para o projeto. + - name: equipe + description: Campo de equipe com marcações HTML removidas. + - name: percentual_concluido + description: Percentual concluído do projeto. + - name: corpo + description: Corpo textual do item com marcações HTML removidas. + - name: fiscal_e_substituto + description: Fiscal e substituto informados para o projeto. + - name: numero_siafi + description: Número SIAFI associado ao projeto. + - name: atribuido_a + description: Pessoa à qual o item foi atribuído. + - name: atribuido_a_claims + description: Claims da pessoa atribuída. + - name: dt_ingest + description: Data e hora de ingestão do registro. + + - name: ipea_pro_bolsistas + description: > + Tabela bronze com o cadastro de bolsistas do sistema Sisbolsas. + Reúne dados de identificação pessoal, vínculos administrativos, contatos, + endereço residencial, referências ao projeto e metadados de autenticação + existentes na base operacional do `ipea_pro`. + Nesta camada, as colunas foram tipadas e padronizadas para apoiar análises + e integrações posteriores. + meta: + tags: + - bronze + columns: + - name: bolsistaid + description: Identificador único do bolsista no Sisbolsas. + - name: categoriaadicionalbolsistaid + description: Identificador da categoria adicional associada ao bolsista. + - name: nomebolsista + description: Nome completo do bolsista. + - name: datanascimento + description: Data de nascimento do bolsista. + - name: numerocpf + description: CPF do bolsista, padronizado apenas com dígitos. + - name: numerorg + description: Número do documento de RG do bolsista. + - name: expedidorrg + description: Órgão expedidor do RG do bolsista. + - name: coordenadorid + description: Identificador do coordenador associado ao bolsista. + - name: diretoriaid + description: Identificador da diretoria vinculada ao bolsista. + - name: coordenacaoid + description: Identificador da coordenação vinculada ao bolsista. + - name: unidadeipeaid + description: Identificador da unidade do IPEA associada ao bolsista. + - name: email1 + description: Endereço de e-mail principal do bolsista. + - name: email2 + description: Endereço de e-mail secundário do bolsista. + - name: email3 + description: Endereço de e-mail adicional do bolsista. + - name: telefone1 + description: Telefone principal de contato do bolsista. + - name: telefone2 + description: Telefone secundário de contato do bolsista. + - name: telefone3 + description: Telefone adicional de contato do bolsista. + - name: residenciacep + description: CEP do endereço residencial do bolsista, padronizado apenas com dígitos. + - name: residenciarua + description: Nome da rua ou logradouro residencial do bolsista. + - name: residencianumero + description: Número do endereço residencial do bolsista. + - name: residenciacomplemento + description: Complemento do endereço residencial do bolsista. + - name: residenciabairro + description: Bairro do endereço residencial do bolsista. + - name: bolsistaminicv + description: Minicurrículo ou resumo curricular do bolsista. + - name: linktolattes + description: Link para o currículo Lattes do bolsista. + - name: ufid + description: Identificador da unidade federativa associada ao bolsista. + - name: municipioid + description: Identificador do município associado ao bolsista. + - name: bolsadatainicio + description: Data de início da bolsa do bolsista. + - name: bolsadatafinalizacao + description: Data de finalização da bolsa do bolsista. + - name: categoriabolsistaid + description: Identificador da categoria principal da bolsa. + - name: projetoid + description: Identificador do projeto ao qual o bolsista está vinculado. + - name: sexoid + description: Identificador do sexo cadastrado para o bolsista. + - name: situacaopessoaid + description: Identificador da situação pessoal do bolsista. + - name: statuscadastroid + description: Identificador do status cadastral do bolsista. + - name: custoemprojetoid + description: Identificador do custo do bolsista dentro do projeto. + - name: login + description: Login de acesso do bolsista no sistema. + - name: senha + description: Valor armazenado no campo de senha do cadastro do bolsista. + - name: usuarioid + description: Identificador do usuário relacionado ao cadastro do bolsista. + - name: idnosistemalegado + description: Identificador do bolsista no sistema legado. + - name: matriculaipea + description: Matrícula interna do bolsista no IPEA, quando existente. + - name: observacoes + description: Campo livre com observações complementares sobre o bolsista. + + - name: ipea_pro_coordenacoes + description: > + Tabela bronze com o cadastro de coordenações do sistema Sisbolsas. + Contém identificação, nome, sigla, descrição, responsável, vínculo com + diretoria e indicador de atividade da coordenação. + meta: + tags: + - bronze + columns: + - name: coordenacaoid + description: Identificador único da coordenação. + - name: coordenacaonome + description: Nome da coordenação. + - name: coordenacaosigla + description: Sigla da coordenação. + - name: coordenacaodescricao + description: Descrição textual da coordenação. + - name: coordenadorid + description: Identificador do coordenador responsável pela coordenação. + - name: diretoriaid + description: Identificador da diretoria à qual a coordenação está vinculada. + - name: ativa + description: Indica se a coordenação está ativa no sistema. + - name: codigosiape + description: Código SIAPE associado à coordenação, quando informado. + + - name: ipea_pro_custosemprojetos + description: > + Tabela bronze com os custos planejados ou registrados em projetos do + Sisbolsas. Armazena a relação entre projeto, item de custo, quantidade, + período de vigência e valor específico do custo. + meta: + tags: + - bronze + columns: + - name: custoemprojetoid + description: Identificador único do registro de custo em projeto. + - name: itemcustoid + description: Identificador do item de custo associado ao registro. + - name: quantidadeitemcusto + description: Quantidade prevista ou registrada do item de custo. + - name: projetoid + description: Identificador do projeto ao qual o custo está vinculado. + - name: descricaoitemcusto + description: Descrição complementar do item de custo no contexto do projeto. + - name: datainicial + description: Data inicial de vigência ou referência do custo no projeto. + - name: datafinal + description: Data final de vigência ou referência do custo no projeto. + - name: custoespecifico + description: Valor específico do custo registrado para o projeto. + + - name: ipea_pro_diretorias + description: > + Tabela bronze com o cadastro de diretorias do sistema Sisbolsas. + Reúne identificação, nome, sigla, descrição, dirigentes vinculados e + código SIAPE associado à estrutura administrativa. + meta: + tags: + - bronze + columns: + - name: diretoriaid + description: Identificador único da diretoria. + - name: diretorianome + description: Nome da diretoria. + - name: diretoriasigla + description: Sigla da diretoria. + - name: diretoriadescricao + description: Descrição textual da diretoria. + - name: diretorid + description: Identificador do diretor responsável pela diretoria. + - name: diretoradjuntoid + description: Identificador do diretor adjunto da diretoria. + - name: codigosiape + description: Código SIAPE associado à diretoria, quando disponível. + + - name: ipea_pro_fontesreceitas + description: > + Tabela bronze com o cadastro de fontes de receita vinculadas aos projetos + do Sisbolsas. Contém a referência do projeto, tipo de fonte, descrição, + valores e intervalo temporal da receita. + meta: + tags: + - bronze + columns: + - name: fontereceitaid + description: Identificador único da fonte de receita. + - name: projetoid + description: Identificador do projeto ao qual a fonte de receita está vinculada. + - name: itemfontereceitaid + description: Identificador do item de classificação da fonte de receita. + - name: descricaofonte + description: Descrição textual da fonte de receita. + - name: valortotalfonte + description: Valor total previsto ou registrado para a fonte de receita. + - name: datainiciofonte + description: Data de início da vigência da fonte de receita. + - name: datafinalfonte + description: Data de término da vigência da fonte de receita. + - name: valortotalfonte_bkp + description: Valor total da fonte registrado no campo de backup da base original. + + - name: ipea_pro_grupoentidade + description: > + Tabela bronze com o cadastro de grupos de entidade utilizados pelo + Sisbolsas para classificar registros financeiros ou institucionais. + meta: + tags: + - bronze + columns: + - name: grupoentidadeid + description: Identificador único do grupo de entidade. + - name: nome + description: Nome do grupo de entidade. + + - name: ipea_pro_insumofinanceiro + description: > + Tabela bronze com o cadastro de insumos financeiros do Sisbolsas. + Define itens financeiros, unidade de medida, descrição e indicadores sobre + a existência de valor unitário e quantidade. + meta: + tags: + - bronze + columns: + - name: insumofinanceiroid + description: Identificador único do insumo financeiro. + - name: item + description: Nome ou título do item de insumo financeiro. + - name: unidade + description: Unidade de medida associada ao insumo financeiro. + - name: descricao + description: Descrição detalhada do insumo financeiro. + - name: possuivalorunitario + description: Indica se o insumo financeiro possui valor unitário informado. + - name: possuiquantidade + description: Indica se o insumo financeiro possui quantidade informada. + + - name: ipea_pro_itemfontereceitas + description: > + Tabela bronze com os itens de classificação das fontes de receita do + Sisbolsas. Serve de apoio para categorizar e descrever os tipos de + receita utilizados nos projetos. + meta: + tags: + - bronze + columns: + - name: itemfontereceitaid + description: Identificador único do item de fonte de receita. + - name: nomeitemfontereceita + description: Nome do item de fonte de receita. + - name: descricaoitemfontereceita + description: Descrição detalhada do item de fonte de receita. + - name: observacao + description: Observações complementares sobre o item de fonte de receita. + + - name: ipea_pro_itenscustos + description: > + Tabela bronze com o cadastro de itens de custo utilizados no Sisbolsas. + Contém nome, descrição, unidade, valor unitário de referência, + classificação cadastral, ordenação e indicadores de atividade e padrão. + meta: + tags: + - bronze + columns: + - name: itemcustoid + description: Identificador único do item de custo. + - name: nomeitem + description: Nome do item de custo. + - name: descricaoitem + description: Descrição detalhada do item de custo. + - name: unidadeitem + description: Unidade de medida do item de custo. + - name: valorunitarioitem + description: Valor unitário de referência do item de custo. + - name: statusitemcadastradoid + description: Identificador do status cadastral do item de custo. + - name: categoriasitenscustoid + description: Identificador da categoria do item de custo. + - name: ordem + description: Ordem de exibição ou prioridade do item de custo. + - name: ativo + description: Indica se o item de custo está ativo no sistema. + - name: padrao + description: Indica se o item de custo é considerado padrão. + + - name: ipea_pro_projetos + description: > + Tabela bronze com o cadastro de projetos do sistema Sisbolsas. + Consolida informações de identificação, escopo, justificativa, metodologia, + datas, responsáveis, situação do projeto, versionamento e indicadores + gerenciais utilizados na operação do sistema. + meta: + tags: + - bronze + columns: + - name: projetoid + description: Identificador único do projeto. + - name: categoriaadicionalprojetoid + description: Identificador da categoria adicional associada ao projeto. + - name: tituloprojeto + description: Título do projeto. + - name: objetivofinalprojeto + description: Objetivo final declarado para o projeto. + - name: justificativaprojeto + description: Justificativa cadastrada para o projeto. + - name: metodologiaprojeto + description: Metodologia descrita para execução do projeto. + - name: cooperacao + description: Informação textual sobre cooperação relacionada ao projeto. + - name: datainicio + description: Data de início do projeto. + - name: datafim + description: Data prevista de término do projeto. + - name: coordenadorid + description: Identificador do coordenador responsável pelo projeto. + - name: diretoriaid + description: Identificador da diretoria vinculada ao projeto. + - name: coordenacaoid + description: Identificador da coordenação vinculada ao projeto. + - name: statusetapaid + description: Identificador do status da etapa do projeto. + - name: statusprojetoid + description: Identificador do status do projeto. + - name: modalidadeprojetoid + description: Identificador da modalidade do projeto. + - name: numeroprojeto + description: Número formal do projeto. + - name: numeroprojetonoano + description: Numeração sequencial do projeto dentro do ano. + - name: anoprojeto + description: Ano de referência do projeto. + - name: usuariocadastranteid + description: Identificador do usuário que cadastrou o projeto. + - name: usuarioversaoid + description: Identificador do usuário responsável pela versão do projeto. + - name: numeroversao + description: Número da versão do registro do projeto. + - name: horaversao + description: Data e hora de versionamento do registro do projeto. + - name: projetoexcluido + description: Indica se o projeto foi marcado como excluído. + - name: usuarioexcluinteid + description: Identificador do usuário que realizou a exclusão lógica do projeto. + - name: rowversion + description: Controle numérico de versão da linha na base de origem. + - name: projetoassessoria + description: Indica se o projeto é classificado como projeto de assessoria. + - name: projetoestrategico + description: Indica se o projeto é classificado como estratégico. + - name: datafimreal + description: Data real de encerramento do projeto, quando houver. + - name: statusprojetohistoricoid + description: Identificador do histórico de status do projeto. + - name: palavras_chave + description: Palavras-chave associadas ao projeto. + - name: planocomunicacao + description: Plano de comunicação vinculado ao projeto. + - name: justificativaprojetoestrategico + description: Justificativa para o enquadramento do projeto como estratégico. + - name: planejamentofinanceiroativo + description: Indica se o planejamento financeiro do projeto está ativo. + - name: projetoprioritario + description: Indica se o projeto foi marcado como prioritário. + + - name: ipea_pro_registrofinanceiroemprojetos + description: > + Tabela bronze com os registros financeiros vinculados aos projetos do + Sisbolsas. Relaciona projeto, insumo financeiro, grupo de entidade, + quantidade, valor unitário e descrição da fonte associada. + meta: + tags: + - bronze + columns: + - name: registrofinanceiroid + description: Identificador único do registro financeiro em projeto. + - name: descricaoinsumo + description: Descrição do insumo financeiro associado ao registro. + - name: valorunitarioinsumo + description: Valor unitário do insumo financeiro no registro. + - name: grupoentidadeid + description: Identificador do grupo de entidade associado ao registro. + - name: insumofinanceiroid + description: Identificador do insumo financeiro vinculado ao registro. + - name: projetoid + description: Identificador do projeto ao qual o registro financeiro pertence. + - name: quantidadeinsumo + description: Quantidade do insumo financeiro registrada no projeto. + - name: descricaofonte + description: Descrição da fonte de recursos associada ao registro financeiro. + + - name: ipea_pro_servidorespublicos + description: > + Tabela bronze com o cadastro de servidores públicos do sistema Sisbolsas. + Contém identificação pessoal, vínculos organizacionais, informações de + contato, endereço residencial e de trabalho, custo do servidor e dados de + autenticação presentes na base operacional. + meta: + tags: + - bronze + columns: + - name: servidorpublicoid + description: Identificador único do servidor público no Sisbolsas. + - name: categoriaadicionalservidorid + description: Identificador da categoria adicional associada ao servidor. + - name: nomeservidor + description: Nome completo do servidor público. + - name: datanascimento + description: Data de nascimento do servidor público. + - name: numerocpf + description: CPF do servidor público, padronizado apenas com dígitos. + - name: numerorg + description: Número do RG do servidor público. + - name: expedidorrg + description: Órgão expedidor do RG do servidor público. + - name: diretoriaid + description: Identificador da diretoria vinculada ao servidor. + - name: coordenacaoid + description: Identificador da coordenação vinculada ao servidor. + - name: salarioatual + description: Valor do salário atual do servidor. + - name: custodoservidor + description: Valor do custo total associado ao servidor. + - name: email1 + description: Endereço de e-mail principal do servidor. + - name: email2 + description: Endereço de e-mail secundário do servidor. + - name: email3 + description: Endereço de e-mail adicional do servidor. + - name: telefone1 + description: Telefone principal de contato do servidor. + - name: telefone2 + description: Telefone secundário de contato do servidor. + - name: telefone3 + description: Telefone adicional de contato do servidor. + - name: unidadeipeaid + description: Identificador da unidade do IPEA associada ao servidor. + - name: residenciacep + description: CEP do endereço residencial do servidor, padronizado apenas com dígitos. + - name: residenciarua + description: Rua ou logradouro do endereço residencial do servidor. + - name: residencianumero + description: Número do endereço residencial do servidor. + - name: residenciacomplemento + description: Complemento do endereço residencial do servidor. + - name: residenciabairro + description: Bairro do endereço residencial do servidor. + - name: servidorminicv + description: Minicurrículo ou resumo curricular do servidor. + - name: linktolattes + description: Link para o currículo Lattes do servidor. + - name: ufid + description: Identificador da unidade federativa associada ao servidor. + - name: municipioid + description: Identificador do município associado ao servidor. + - name: dasid + description: Identificador do DAS ou função comissionada associada ao servidor. + - name: categoriaid + description: Identificador da categoria principal do servidor. + - name: matriculasiape + description: Matrícula SIAPE do servidor. + - name: sexoid + description: Identificador do sexo cadastrado para o servidor. + - name: statuscadastroid + description: Identificador do status cadastral do servidor. + - name: situacaoservidorid + description: Identificador da situação do servidor no sistema. + - name: login + description: Login de acesso do servidor no sistema. + - name: senha + description: Valor armazenado no campo de senha do cadastro do servidor. + - name: usuarioid + description: Identificador do usuário relacionado ao cadastro do servidor. + - name: idnosistemalegado + description: Identificador do servidor no sistema legado. + - name: instituicaoid + description: Identificador da instituição vinculada ao servidor. + - name: trabalhocep + description: CEP do endereço de trabalho do servidor, padronizado apenas com dígitos. + - name: trabalhorua + description: Rua ou logradouro do endereço de trabalho do servidor. + - name: trabalhonumero + description: Número do endereço de trabalho do servidor. + - name: trabalhocomplemento + description: Complemento do endereço de trabalho do servidor. + - name: trabalhobairro + description: Bairro do endereço de trabalho do servidor. + - name: trabalhoufid + description: Identificador da unidade federativa do endereço de trabalho. + - name: trabalhomunicipioid + description: Identificador do município do endereço de trabalho. + - name: cargocomissaoid + description: Identificador do cargo em comissão associado ao servidor. + - name: ramal + description: Ramal telefônico do servidor. + + - name: ipea_pro_statusprojetos + description: > + Tabela bronze com o cadastro de status de projetos do Sisbolsas. + Funciona como dimensão de apoio para qualificar a situação dos projetos + registrados no sistema. + meta: + tags: + - bronze + columns: + - name: statusprojetoid + description: Identificador único do status de projeto. + - name: nomestatus + description: Nome do status do projeto. + - name: descricaostatus + description: Descrição detalhada do status do projeto. + - name: ativo + description: Indica se o status de projeto está ativo no sistema. + + - name: sisbolsas_tb_area_formacao + description: > + Tabela bronze com o cadastro de áreas de formação utilizadas pelo + Sisbolsas para classificar formações acadêmicas e perfis de seleção. + meta: + tags: + - bronze + columns: + - name: co_area_formacao + description: Código identificador registrado no campo de origem referente a `co area formacao`. + - name: ds_area_formacao + description: Descrição textual registrada no campo de origem referente a `ds area formacao`. + + - name: sisbolsas_tb_bolsa + description: > + Tabela bronze com o cadastro de bolsas vinculadas às seleções do + Sisbolsas, incluindo situação, período de vigência e duração prevista. + meta: + tags: + - bronze + columns: + - name: co_selecao + description: Código identificador registrado no campo de origem referente a `co selecao`. + - name: nu_bolsa + description: Número registrado no campo de origem referente a `nu bolsa`. + - name: co_situacao_bolsa + description: Código identificador registrado no campo de origem referente a `co situacao bolsa`. + - name: dt_inicio_bolsa + description: Data registrada no campo de origem referente a `dt inicio bolsa`. + - name: dt_fim_bolsa + description: Data registrada no campo de origem referente a `dt fim bolsa`. + - name: qt_duracao + description: Quantidade registrada no campo de origem referente a `qt duracao`. + + - name: sisbolsas_tb_bolsa_coordenador + description: > + Tabela bronze com a vinculação entre bolsas e coordenadores no + Sisbolsas, incluindo CPF, vigência e situação do vínculo. + meta: + tags: + - bronze + columns: + - name: co_bolsa_coordenador + description: Código identificador registrado no campo de origem referente a `co bolsa coordenador`. + - name: co_selecao + description: Código identificador registrado no campo de origem referente a `co selecao`. + - name: nu_bolsa + description: Número registrado no campo de origem referente a `nu bolsa`. + - name: ds_cpf + description: CPF associado ao registro, padronizado apenas com dígitos. + - name: dt_inicio_vigencia + description: Data registrada no campo de origem referente a `dt inicio vigencia`. + - name: dt_fim_vigencia + description: Data registrada no campo de origem referente a `dt fim vigencia`. + - name: st_situacao + description: Status registrado no campo de origem referente a `st situacao`. + + - name: sisbolsas_tb_bolsa_fonterecurso + description: > + Tabela bronze com a associação entre bolsas e fontes financeiras no + Sisbolsas, registrando vigência e situação da fonte de recurso. + meta: + tags: + - bronze + columns: + - name: co_bolsa_fonterecurso + description: Código identificador registrado no campo de origem referente a `co bolsa fonterecurso`. + - name: co_selecao + description: Código identificador registrado no campo de origem referente a `co selecao`. + - name: nu_bolsa + description: Número registrado no campo de origem referente a `nu bolsa`. + - name: co_fonte_financeira + description: Código identificador registrado no campo de origem referente a `co fonte financeira`. + - name: dt_fim_vigencia + description: Data registrada no campo de origem referente a `dt fim vigencia`. + - name: dt_inicio_vigencia + description: Data registrada no campo de origem referente a `dt inicio vigencia`. + - name: st_situacao + description: Status registrado no campo de origem referente a `st situacao`. + + - name: sisbolsas_tb_bolsista + description: > + Tabela bronze com os vínculos de bolsistas às seleções e bolsas do + Sisbolsas, incluindo situação, número SEI e período de vigência. + meta: + tags: + - bronze + columns: + - name: co_usuario + description: Código identificador registrado no campo de origem referente a `co usuario`. + - name: co_selecao + description: Código identificador registrado no campo de origem referente a `co selecao`. + - name: nu_bolsa + description: Número registrado no campo de origem referente a `nu bolsa`. + - name: co_situacao_bolsista + description: Código identificador registrado no campo de origem referente a `co situacao bolsista`. + - name: ds_numero_sei + description: Descrição textual registrada no campo de origem referente a `ds numero sei`. + - name: dt_inicio + description: Data registrada no campo de origem referente a `dt inicio`. + - name: dt_fim + description: Data registrada no campo de origem referente a `dt fim`. + + - name: sisbolsas_tb_chamada_publica + description: > + Tabela bronze com o cadastro de chamadas públicas do Sisbolsas, reunindo + programa, situação, cronograma, valores estimados e metadados da + chamada. + meta: + tags: + - bronze + columns: + - name: co_chamada_publica + description: Código identificador registrado no campo de origem referente a `co chamada publica`. + - name: co_projeto + description: Código identificador registrado no campo de origem referente a `co projeto`. + - name: co_situacao_chamada + description: Código identificador registrado no campo de origem referente a `co situacao chamada`. + - name: co_usuario_criacao + description: Código identificador registrado no campo de origem referente a `co usuario criacao`. + - name: co_programa + description: Código identificador registrado no campo de origem referente a `co programa`. + - name: nu_chamada_publica + description: Número registrado no campo de origem referente a `nu chamada publica`. + - name: nu_ano + description: Número registrado no campo de origem referente a `nu ano`. + - name: ds_chamada_publica + description: Descrição textual registrada no campo de origem referente a `ds chamada publica`. + - name: ds_numero_sei + description: Descrição textual registrada no campo de origem referente a `ds numero sei`. + - name: vl_global_estimado + description: Valor monetário registrado no campo de origem referente a `vl global estimado`. + - name: dt_ini_pesquisa + description: Data registrada no campo de origem referente a `dt ini pesquisa`. + - name: dt_fim_pesquisa + description: Data registrada no campo de origem referente a `dt fim pesquisa`. + - name: dt_publicacao_dou + description: Data registrada no campo de origem referente a `dt publicacao dou`. + - name: dt_previsao_resultado + description: Data registrada no campo de origem referente a `dt previsao resultado`. + - name: dt_ini_bolsa + description: Data registrada no campo de origem referente a `dt ini bolsa`. + - name: dt_criacao + description: Data registrada no campo de origem referente a `dt criacao`. + - name: dt_inicio_inscricao + description: Data registrada no campo de origem referente a `dt inicio inscricao`. + - name: dt_fim_inscricao + description: Data registrada no campo de origem referente a `dt fim inscricao`. + - name: dt_inicio_julgamento + description: Data registrada no campo de origem referente a `dt inicio julgamento`. + - name: dt_fim_julgamento + description: Data registrada no campo de origem referente a `dt fim julgamento`. + - name: dt_publicacao_resultado + description: Data registrada no campo de origem referente a `dt publicacao resultado`. + - name: tp_moeda + description: Tipo registrado no campo de origem referente a `tp moeda`. + - name: dt_fim_recurso + description: Data registrada no campo de origem referente a `dt fim recurso`. + - name: dt_inicio_recurso + description: Data registrada no campo de origem referente a `dt inicio recurso`. + - name: dt_inicio_previsao_bolsa + description: Data registrada no campo de origem referente a `dt inicio previsao bolsa`. + + - name: sisbolsas_tb_chapubli_fontfina + description: > + Tabela bronze com a associação entre chamadas públicas e fontes + financeiras no Sisbolsas. + meta: + tags: + - bronze + columns: + - name: co_chamada_publica + description: Código identificador registrado no campo de origem referente a `co chamada publica`. + - name: co_fonte_financeira + description: Código identificador registrado no campo de origem referente a `co fonte financeira`. + + - name: sisbolsas_tb_chapubli_unidade + description: > + Tabela bronze com a associação entre chamadas públicas e unidades + organizacionais no Sisbolsas. + meta: + tags: + - bronze + columns: + - name: co_chamada_publica + description: Código identificador registrado no campo de origem referente a `co chamada publica`. + - name: co_unidade + description: Código identificador registrado no campo de origem referente a `co unidade`. + + - name: sisbolsas_tb_coordenador + description: > + Tabela bronze com a relação de coordenadores vinculados às chamadas + públicas do Sisbolsas. + meta: + tags: + - bronze + columns: + - name: co_chamada_publica + description: Código identificador registrado no campo de origem referente a `co chamada publica`. + - name: ds_cpf + description: CPF associado ao registro, padronizado apenas com dígitos. + + - name: sisbolsas_tb_dado_formacao + description: > + Tabela bronze com os dados de formação acadêmica cadastrados para + usuários do Sisbolsas, incluindo curso, instituição, área e validação do + diploma. + meta: + tags: + - bronze + columns: + - name: co_dado_formacao + description: Código identificador registrado no campo de origem referente a `co dado formacao`. + - name: co_usuario + description: Código identificador registrado no campo de origem referente a `co usuario`. + - name: co_nivel_escolaridade + description: Código identificador registrado no campo de origem referente a `co nivel escolaridade`. + - name: co_tipo_escolar + description: Código identificador registrado no campo de origem referente a `co tipo escolar`. + - name: st_nivel_escolaridade + description: Status registrado no campo de origem referente a `st nivel escolaridade`. + - name: tp_nacionalidade_diploma + description: Tipo registrado no campo de origem referente a `tp nacionalidade diploma`. + - name: in_diploma_valido + description: Indicador lógico registrado no campo de origem referente a `in diploma valido`. + - name: co_area_formacao + description: Código identificador registrado no campo de origem referente a `co area formacao`. + - name: ds_outra_formacao + description: Descrição textual registrada no campo de origem referente a `ds outra formacao`. + - name: ds_curso + description: Descrição textual registrada no campo de origem referente a `ds curso`. + - name: ds_instituicao + description: Descrição textual registrada no campo de origem referente a `ds instituicao`. + - name: co_estado + description: Código identificador registrado no campo de origem referente a `co estado`. + - name: ds_url_curriculo + description: Descrição textual registrada no campo de origem referente a `ds url curriculo`. + - name: dt_criacao + description: Data registrada no campo de origem referente a `dt criacao`. + - name: co_pais + description: Código identificador registrado no campo de origem referente a `co pais`. + + - name: sisbolsas_tb_dado_pessoal + description: > + Tabela bronze com os dados pessoais dos usuários do Sisbolsas, incluindo + identificação civil, contatos, nacionalidade e informações + complementares. + meta: + tags: + - bronze + columns: + - name: co_dado_pessoal + description: Código identificador registrado no campo de origem referente a `co dado pessoal`. + - name: co_usuario + description: Código identificador registrado no campo de origem referente a `co usuario`. + - name: ds_cpf + description: CPF associado ao registro, padronizado apenas com dígitos. + - name: dt_nascimento + description: Data registrada no campo de origem referente a `dt nascimento`. + - name: co_estado_civil + description: Código identificador registrado no campo de origem referente a `co estado civil`. + - name: tp_sexo + description: Tipo registrado no campo de origem referente a `tp sexo`. + - name: tp_nacionalidade + description: Tipo registrado no campo de origem referente a `tp nacionalidade`. + - name: ds_naturalidade + description: Descrição textual registrada no campo de origem referente a `ds naturalidade`. + - name: ds_rg + description: Descrição textual registrada no campo de origem referente a `ds rg`. + - name: dt_emissao_rg + description: Data registrada no campo de origem referente a `dt emissao rg`. + - name: ds_orgao_emissor + description: Descrição textual registrada no campo de origem referente a `ds orgao emissor`. + - name: co_estado_orgao + description: Código identificador registrado no campo de origem referente a `co estado orgao`. + - name: ds_passaporte + description: Descrição textual registrada no campo de origem referente a `ds passaporte`. + - name: tp_visto + description: Tipo registrado no campo de origem referente a `tp visto`. + - name: dt_validade_visto + description: Data registrada no campo de origem referente a `dt validade visto`. + - name: nu_telefone_principal + description: Número registrado no campo de origem referente a `nu telefone principal`. + - name: nu_telefone_alternativo + description: Número registrado no campo de origem referente a `nu telefone alternativo`. + - name: dt_criacao + description: Data registrada no campo de origem referente a `dt criacao`. + - name: co_pais_passaporte + description: Código identificador registrado no campo de origem referente a `co pais passaporte`. + - name: ds_ddd_principal + description: Descrição textual registrada no campo de origem referente a `ds ddd principal`. + - name: ds_ddd_alternativo + description: Descrição textual registrada no campo de origem referente a `ds ddd alternativo`. + - name: co_etnia + description: Código identificador registrado no campo de origem referente a `co etnia`. + - name: ds_email_alternativo + description: Descrição textual registrada no campo de origem referente a `ds email alternativo`. + - name: tp_fator + description: Tipo registrado no campo de origem referente a `tp fator`. + - name: tp_sanguineo + description: Tipo registrado no campo de origem referente a `tp sanguineo`. + + - name: sisbolsas_tb_dado_profissional + description: > + Tabela bronze com os dados profissionais dos usuários do Sisbolsas, + incluindo situação funcional, vínculo, instituição e cargo. + meta: + tags: + - bronze + columns: + - name: co_dado_profissional + description: Código identificador registrado no campo de origem referente a `co dado profissional`. + - name: tp_situacao_funcional + description: Tipo registrado no campo de origem referente a `tp situacao funcional`. + - name: in_vinculo + description: Indicador lógico registrado no campo de origem referente a `in vinculo`. + - name: tp_setor + description: Tipo registrado no campo de origem referente a `tp setor`. + - name: in_funcao_gratificada + description: Indicador lógico registrado no campo de origem referente a `in funcao gratificada`. + - name: ds_instituicao + description: Descrição textual registrada no campo de origem referente a `ds instituicao`. + - name: ds_empregador + description: Descrição textual registrada no campo de origem referente a `ds empregador`. + - name: tp_cargo + description: Tipo registrado no campo de origem referente a `tp cargo`. + - name: co_usuario + description: Código identificador registrado no campo de origem referente a `co usuario`. + - name: dt_criacao + description: Data registrada no campo de origem referente a `dt criacao`. + + - name: sisbolsas_tb_estado + description: > + Tabela bronze com o cadastro de estados utilizado pelo Sisbolsas para + referência territorial. + meta: + tags: + - bronze + columns: + - name: co_estado + description: Código identificador registrado no campo de origem referente a `co estado`. + - name: ds_uf + description: Descrição textual registrada no campo de origem referente a `ds uf`. + - name: ds_nome + description: Descrição textual registrada no campo de origem referente a `ds nome`. + + - name: sisbolsas_tb_folha_bolsista + description: > + Tabela bronze com os registros de pagamento de bolsistas em folhas do + Sisbolsas, incluindo valores pagos, quantidade de dias e conferência. + meta: + tags: + - bronze + columns: + - name: co_folha_pagamento + description: Código identificador registrado no campo de origem referente a `co folha pagamento`. + - name: co_usuario + description: Código identificador registrado no campo de origem referente a `co usuario`. + - name: co_selecao + description: Código identificador registrado no campo de origem referente a `co selecao`. + - name: nu_bolsa + description: Número registrado no campo de origem referente a `nu bolsa`. + - name: co_dado_bancario + description: Código identificador registrado no campo de origem referente a `co dado bancario`. + - name: co_diretoria + description: Código identificador registrado no campo de origem referente a `co diretoria`. + - name: co_unidade + description: Código identificador registrado no campo de origem referente a `co unidade`. + - name: co_fonte_financeira + description: Código identificador registrado no campo de origem referente a `co fonte financeira`. + - name: nu_dias_pago + description: Número registrado no campo de origem referente a `nu dias pago`. + - name: vl_dia_pago + description: Valor monetário registrado no campo de origem referente a `vl dia pago`. + - name: vl_total_pago + description: Valor monetário registrado no campo de origem referente a `vl total pago`. + - name: in_conferencia + description: Indicador lógico registrado no campo de origem referente a `in conferencia`. + + - name: sisbolsas_tb_folha_pagamento + description: > + Tabela bronze com o cadastro das folhas de pagamento do Sisbolsas, + contendo solicitação, lote, situação, totais e dados de homologação. + meta: + tags: + - bronze + columns: + - name: co_folha_pagamento + description: Código identificador registrado no campo de origem referente a `co folha pagamento`. + - name: nu_solicitacao + description: Número registrado no campo de origem referente a `nu solicitacao`. + - name: nu_mes + description: Número registrado no campo de origem referente a `nu mes`. + - name: nu_ano + description: Número registrado no campo de origem referente a `nu ano`. + - name: nu_lote + description: Número registrado no campo de origem referente a `nu lote`. + - name: co_situacao_folha + description: Código identificador registrado no campo de origem referente a `co situacao folha`. + - name: dt_criacao + description: Data registrada no campo de origem referente a `dt criacao`. + - name: nu_total_integral + description: Número registrado no campo de origem referente a `nu total integral`. + - name: nu_total_parcial + description: Número registrado no campo de origem referente a `nu total parcial`. + - name: co_usuario_criacao + description: Código identificador registrado no campo de origem referente a `co usuario criacao`. + - name: co_usuario_homologacao + description: Código identificador registrado no campo de origem referente a `co usuario homologacao`. + - name: dt_homologacao + description: Data registrada no campo de origem referente a `dt homologacao`. + + - name: sisbolsas_tb_fonte_financeira + description: > + Tabela bronze com o cadastro de fontes financeiras utilizadas pelo + Sisbolsas. + meta: + tags: + - bronze + columns: + - name: co_fonte_financeira + description: Código identificador registrado no campo de origem referente a `co fonte financeira`. + - name: ds_fonte_financeira + description: Descrição textual registrada no campo de origem referente a `ds fonte financeira`. + + - name: sisbolsas_tb_modalidade + description: > + Tabela bronze com o cadastro de modalidades de bolsa do Sisbolsas, + incluindo nível de escolaridade e período de vigência. + meta: + tags: + - bronze + columns: + - name: co_modalidade + description: Código identificador registrado no campo de origem referente a `co modalidade`. + - name: ds_modalidade + description: Descrição textual registrada no campo de origem referente a `ds modalidade`. + - name: co_nivel_escolaridade + description: Código identificador registrado no campo de origem referente a `co nivel escolaridade`. + - name: st_nivel_escolaridade + description: Status registrado no campo de origem referente a `st nivel escolaridade`. + - name: dt_inicio + description: Data registrada no campo de origem referente a `dt inicio`. + - name: dt_fim + description: Data registrada no campo de origem referente a `dt fim`. + + - name: sisbolsas_tb_nivel_escolaridade + description: > + Tabela bronze com o cadastro de níveis de escolaridade utilizados pelo + Sisbolsas. + meta: + tags: + - bronze + columns: + - name: co_nivel_escolaridade + description: Código identificador registrado no campo de origem referente a `co nivel escolaridade`. + - name: ds_nivel_escolaridade + description: Descrição textual registrada no campo de origem referente a `ds nivel escolaridade`. + + - name: sisbolsas_tb_processo_seletivo + description: > + Tabela bronze com os processos seletivos do Sisbolsas, relacionando + candidato, avaliação, classificação, situação de concessão e pontuações. + meta: + tags: + - bronze + columns: + - name: co_processo_seletivo + description: Código identificador registrado no campo de origem referente a `co processo seletivo`. + - name: co_usuario + description: Código identificador registrado no campo de origem referente a `co usuario`. + - name: co_dado_formacao + description: Código identificador registrado no campo de origem referente a `co dado formacao`. + - name: co_selecao + description: Código identificador registrado no campo de origem referente a `co selecao`. + - name: co_dado_profissional + description: Código identificador registrado no campo de origem referente a `co dado profissional`. + - name: co_situacao_concessao + description: Código identificador registrado no campo de origem referente a `co situacao concessao`. + - name: st_processo_seletivo + description: Status registrado no campo de origem referente a `st processo seletivo`. + - name: dt_criacao + description: Data registrada no campo de origem referente a `dt criacao`. + - name: in_nao_apto + description: Indicador lógico registrado no campo de origem referente a `in nao apto`. + - name: in_classificacao + description: Indicador lógico registrado no campo de origem referente a `in classificacao`. + - name: tx_observacao_avaliacao + description: Texto livre registrado no campo de origem referente a `tx observacao avaliacao`. + - name: tx_observacao_classificacao + description: Texto livre registrado no campo de origem referente a `tx observacao classificacao`. + - name: vl_total_criterio + description: Valor monetário registrado no campo de origem referente a `vl total criterio`. + - name: vl_total_geral + description: Valor monetário registrado no campo de origem referente a `vl total geral`. + - name: tx_observacao_entrevista + description: Texto livre registrado no campo de origem referente a `tx observacao entrevista`. + - name: ds_instituicao_bolsa + description: Descrição textual registrada no campo de origem referente a `ds instituicao bolsa`. + - name: nu_posicao + description: Número registrado no campo de origem referente a `nu posicao`. + - name: ds_token_aceite + description: Descrição textual registrada no campo de origem referente a `ds token aceite`. + - name: in_bolsa_ativa + description: Indicador lógico registrado no campo de origem referente a `in bolsa ativa`. + - name: ds_institu_bolsa_ativa + description: Descrição textual registrada no campo de origem referente a `ds institu bolsa ativa`. + - name: in_declaracao_veracidade + description: Indicador lógico registrado no campo de origem referente a `in declaracao veracidade`. + + - name: sisbolsas_tb_programa + description: > + Tabela bronze com o cadastro de programas do Sisbolsas, incluindo + hierarquia entre programas. + meta: + tags: + - bronze + columns: + - name: co_programa + description: Código identificador registrado no campo de origem referente a `co programa`. + - name: ds_programa + description: Descrição textual registrada no campo de origem referente a `ds programa`. + - name: co_programa_pai + description: Código identificador registrado no campo de origem referente a `co programa pai`. + + - name: sisbolsas_tb_selecao + description: > + Tabela bronze com o cadastro de seleções do Sisbolsas, contendo + modalidade, requisitos, valores, quantidade de bolsas e parâmetros de + avaliação. + meta: + tags: + - bronze + columns: + - name: co_selecao + description: Código identificador registrado no campo de origem referente a `co selecao`. + - name: co_chamada_publica + description: Código identificador registrado no campo de origem referente a `co chamada publica`. + - name: co_modalidade + description: Código identificador registrado no campo de origem referente a `co modalidade`. + - name: co_estado + description: Código identificador registrado no campo de origem referente a `co estado`. + - name: co_area_formacao + description: Código identificador registrado no campo de origem referente a `co area formacao`. + - name: co_nivel_escolaridade + description: Código identificador registrado no campo de origem referente a `co nivel escolaridade`. + - name: tp_atuacao + description: Tipo registrado no campo de origem referente a `tp atuacao`. + - name: st_nivel_escolaridade + description: Status registrado no campo de origem referente a `st nivel escolaridade`. + - name: qt_duracao + description: Quantidade registrada no campo de origem referente a `qt duracao`. + - name: vl_total + description: Valor monetário registrado no campo de origem referente a `vl total`. + - name: qt_bolsa + description: Quantidade registrada no campo de origem referente a `qt bolsa`. + - name: ds_area_especializacao + description: Descrição textual registrada no campo de origem referente a `ds area especializacao`. + - name: ds_outra_formacao + description: Descrição textual registrada no campo de origem referente a `ds outra formacao`. + - name: in_anexo_projeto + description: Indicador lógico registrado no campo de origem referente a `in anexo projeto`. + - name: in_anexo_idioma + description: Indicador lógico registrado no campo de origem referente a `in anexo idioma`. + - name: in_anexo_historico_escolar + description: Indicador lógico registrado no campo de origem referente a `in anexo historico escolar`. + - name: dt_criacao + description: Data registrada no campo de origem referente a `dt criacao`. + - name: co_usuario + description: Código identificador registrado no campo de origem referente a `co usuario`. + - name: co_cidade + description: Código identificador registrado no campo de origem referente a `co cidade`. + - name: nu_passo_avaliacao + description: Número registrado no campo de origem referente a `nu passo avaliacao`. + - name: st_avaliacao + description: Status registrado no campo de origem referente a `st avaliacao`. + - name: in_entrevista + description: Indicador lógico registrado no campo de origem referente a `in entrevista`. + - name: nu_selecao + description: Número registrado no campo de origem referente a `nu selecao`. + - name: in_anexo_outro + description: Indicador lógico registrado no campo de origem referente a `in anexo outro`. + - name: ds_anexo_outro + description: Descrição textual registrada no campo de origem referente a `ds anexo outro`. + + - name: sisbolsas_tb_situacao_chamada + description: > + Tabela bronze com o catálogo de situações possíveis para chamadas + públicas no Sisbolsas. + meta: + tags: + - bronze + columns: + - name: co_situacao_chamada + description: Código identificador registrado no campo de origem referente a `co situacao chamada`. + - name: ds_situacao_chamada + description: Descrição textual registrada no campo de origem referente a `ds situacao chamada`. + + - name: sisbolsas_tb_situacao_concessao + description: > + Tabela bronze com o catálogo de situações de concessão utilizadas nos + processos seletivos do Sisbolsas. + meta: + tags: + - bronze + columns: + - name: co_situacao_concessao + description: Código identificador registrado no campo de origem referente a `co situacao concessao`. + - name: ds_situacao_concessao + description: Descrição textual registrada no campo de origem referente a `ds situacao concessao`. + + - name: sisbolsas_tb_unidade + description: > + Tabela bronze com o cadastro de unidades organizacionais do Sisbolsas, + incluindo estado, sigla e diretoria associada. + meta: + tags: + - bronze + columns: + - name: co_unidade + description: Código identificador registrado no campo de origem referente a `co unidade`. + - name: ds_unidade + description: Descrição textual registrada no campo de origem referente a `ds unidade`. + - name: co_estado + description: Código identificador registrado no campo de origem referente a `co estado`. + - name: ds_sigla + description: Descrição textual registrada no campo de origem referente a `ds sigla`. + - name: co_diretoria + description: Código identificador registrado no campo de origem referente a `co diretoria`. + + - name: sisbolsas_tb_usuario + description: > + Tabela bronze com o cadastro de usuários do Sisbolsas, incluindo + identificação, login, e-mail, perfil, senha e dados de redefinição de + acesso. + meta: + tags: + - bronze + columns: + - name: co_usuario + description: Código identificador registrado no campo de origem referente a `co usuario`. + - name: ds_nome + description: Descrição textual registrada no campo de origem referente a `ds nome`. + - name: ds_login + description: Descrição textual registrada no campo de origem referente a `ds login`. + - name: co_perfil + description: Código identificador registrado no campo de origem referente a `co perfil`. + - name: ds_email + description: Descrição textual registrada no campo de origem referente a `ds email`. + - name: ds_senha + description: Descrição textual registrada no campo de origem referente a `ds senha`. + - name: dt_criacao + description: Data registrada no campo de origem referente a `dt criacao`. + - name: co_tipo_login + description: Código identificador registrado no campo de origem referente a `co tipo login`. + - name: st_usuario + description: Status registrado no campo de origem referente a `st usuario`. + - name: co_anexo + description: Código identificador registrado no campo de origem referente a `co anexo`. + - name: ds_token_redefinir_senha + description: Descrição textual registrada no campo de origem referente a `ds token redefinir senha`. + - name: dt_token_senha + description: Data registrada no campo de origem referente a `dt token senha`. diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sgac_projetos_sgac.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sgac_projetos_sgac.sql new file mode 100644 index 00000000..a18f08cd --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sgac_projetos_sgac.sql @@ -0,0 +1,77 @@ +{{ config(materialized="table") }} + +with + projetos_sgac as ( + select + ({{ safe_numeric("id_interno_item", 18, 0) }})::bigint as id_interno_item, + ({{ safe_numeric("id", 18, 0) }})::bigint as id, + {{ safe_text("titulo") }} as titulo, + {{ safe_text("entidades_externas") }} as entidades_externas, + {{ extract_jsonb_key_values("instrumento", "Value") }} as instrumento, + ({{ safe_numeric("instrumento_id", 18, 0) }})::bigint as instrumento_id, + {{ extract_jsonb_key_values("diretoria_responsavel", "Value") }} + as diretoria_responsavel, + ({{ safe_numeric("diretoria_responsavel_id", 18, 0) }})::bigint + as diretoria_responsavel_id, + {{ clean_sharepoint_html("objeto") }} as objeto, + {{ safe_date("data_inicio") }} as data_inicio, + {{ safe_date("data_vencimento") }} as data_vencimento, + {{ safe_numeric("total_de_recursos", 18, 2) }} as total_de_recursos, + {{ safe_text("numero_do_proc") }} as numero_do_proc, + {{ extract_jsonb_key_values("coordenador", "DisplayName") }} as coordenador, + {{ sharepoint_jsonb("coordenador") }} as coordenador_json, + {{ sharepoint_jsonb("coordenador_claims") }} as coordenador_claims, + {{ extract_jsonb_key_values("nacionalidade", "Value") }} as nacionalidade, + {{ sharepoint_jsonb("nacionalidade_id") }} as nacionalidade_id, + {{ safe_numeric("recursos_orcament_x00", 18, 2) }} + as recursos_orcamentarios, + {{ safe_numeric("recursos_orcament_x0", 18, 2) }} + as recursos_nao_orcamentarios, + {{ extract_jsonb_key_values("status", "Value") }} as status, + ({{ safe_numeric("status_id", 18, 0) }})::bigint as status_id, + {{ extract_jsonb_key_values("eixo_tematico", "Value") }} as eixo_tematico, + {{ sharepoint_jsonb("eixo_tematico_id") }} as eixo_tematico_id, + {{ extract_jsonb_key_values("predecessores", "Value") }} as predecessores, + {{ sharepoint_jsonb("predecessores_id") }} as predecessores_id, + {{ extract_jsonb_key_values("prioridade", "Value") }} as prioridade, + ({{ safe_numeric("prioridade_id", 18, 0) }})::bigint as prioridade_id, + {{ clean_sharepoint_html("justificativa") }} as justificativa, + {{ clean_sharepoint_html("objetivo_s_ge") }} as objetivo_s_ge, + {{ extract_jsonb_key_values("equipe_tecnica", "DisplayName") }} as equipe_tecnica, + {{ sharepoint_jsonb("equipe_tecnica") }} as equipe_tecnica_json, + {{ sharepoint_jsonb("equipe_tecnica_claims") }} as equipe_tecnica_claims, + {{ safe_text("codigo") }} as codigo, + {{ extract_jsonb_key_values("unidades_envolvidas", "Value") }} + as unidades_envolvidas, + {{ sharepoint_jsonb("unidades_envolvidas_id") }} as unidades_envolvidas_id, + {{ clean_sharepoint_html("historico_observa_x0") }} as historico_observa_x0, + {{ extract_jsonb_key_values("a_solicitacao", "Value") }} as a_solicitacao, + {{ sharepoint_jsonb("a_solicitacao_id") }} as a_solicitacao_id, + {{ safe_timestamp("modificado") }} as modificado, + {{ safe_timestamp("criado") }} as criado, + {{ extract_jsonb_key_values("autor", "DisplayName") }} as autor, + {{ extract_jsonb_key_values("autor", "Email", fallback_to_text=false) }} + as autor_email, + {{ sharepoint_jsonb("autor") }} as autor_json, + {{ safe_text("autor_claims") }} as autor_claims, + {{ extract_jsonb_key_values("editor", "DisplayName") }} as editor, + {{ extract_jsonb_key_values("editor", "Email", fallback_to_text=false) }} + as editor_email, + {{ sharepoint_jsonb("editor") }} as editor_json, + {{ safe_text("editor_claims") }} as editor_claims, + {{ safe_text("link") }} as link, + {{ safe_text("nome") }} as nome, + {{ safe_text("termos_aditivos") }} as termos_aditivos, + {{ clean_sharepoint_html("equipe") }} as equipe, + {{ safe_numeric("percentual_concluido", 18, 4) }} as percentual_concluido, + {{ clean_sharepoint_html("corpo") }} as corpo, + {{ clean_sharepoint_html("fiscal_e_substituto") }} as fiscal_e_substituto, + {{ safe_text("numero_siafi") }} as numero_siafi, + {{ extract_jsonb_key_values("atribuido_a", "DisplayName") }} as atribuido_a, + {{ safe_text("atribuido_a_claims") }} as atribuido_a_claims, + {{ safe_timestamp("dt_ingest") }} as dt_ingest + from {{ source("sgac", "projetos_sgac") }} + ) + +select * +from projetos_sgac diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_area_formacao.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_area_formacao.sql new file mode 100644 index 00000000..e119019e --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_area_formacao.sql @@ -0,0 +1,12 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_area_formacao as ( + select + co_area_formacao::text as co_area_formacao, + ds_area_formacao::text as ds_area_formacao + from {{ source("sisbolsas", "tb_area_formacao") }} + ) + +select * +from sisbolsas_tb_area_formacao diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_bolsa.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_bolsa.sql new file mode 100644 index 00000000..bb07cf7a --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_bolsa.sql @@ -0,0 +1,16 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_bolsa as ( + select + co_selecao::text as co_selecao, + nu_bolsa::text as nu_bolsa, + co_situacao_bolsa::text as co_situacao_bolsa, + dt_inicio_bolsa::text as dt_inicio_bolsa, + dt_fim_bolsa::text as dt_fim_bolsa, + qt_duracao::text as qt_duracao + from {{ source("sisbolsas", "tb_bolsa") }} + ) + +select * +from sisbolsas_tb_bolsa diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_bolsa_coordenador.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_bolsa_coordenador.sql new file mode 100644 index 00000000..bbf40411 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_bolsa_coordenador.sql @@ -0,0 +1,17 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_bolsa_coordenador as ( + select + co_bolsa_coordenador::text as co_bolsa_coordenador, + co_selecao::text as co_selecao, + nu_bolsa::text as nu_bolsa, + regexp_replace(ds_cpf, '[^0-9]', '', 'g')::text as ds_cpf, + dt_inicio_vigencia::text as dt_inicio_vigencia, + dt_fim_vigencia::text as dt_fim_vigencia, + st_situacao::text as st_situacao + from {{ source("sisbolsas", "tb_bolsa_coordenador") }} + ) + +select * +from sisbolsas_tb_bolsa_coordenador diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_bolsa_fonterecurso.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_bolsa_fonterecurso.sql new file mode 100644 index 00000000..c944d5b4 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_bolsa_fonterecurso.sql @@ -0,0 +1,17 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_bolsa_fonterecurso as ( + select + co_bolsa_fonterecurso::text as co_bolsa_fonterecurso, + co_selecao::text as co_selecao, + nu_bolsa::text as nu_bolsa, + co_fonte_financeira::text as co_fonte_financeira, + dt_fim_vigencia::text as dt_fim_vigencia, + dt_inicio_vigencia::text as dt_inicio_vigencia, + st_situacao::text as st_situacao + from {{ source("sisbolsas", "tb_bolsa_fonterecurso") }} + ) + +select * +from sisbolsas_tb_bolsa_fonterecurso diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_bolsista.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_bolsista.sql new file mode 100644 index 00000000..96db66ae --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_bolsista.sql @@ -0,0 +1,17 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_bolsista as ( + select + co_usuario::text as co_usuario, + co_selecao::text as co_selecao, + nu_bolsa::text as nu_bolsa, + co_situacao_bolsista::text as co_situacao_bolsista, + ds_numero_sei::text as ds_numero_sei, + dt_inicio::text as dt_inicio, + dt_fim::text as dt_fim + from {{ source("sisbolsas", "tb_bolsista") }} + ) + +select * +from sisbolsas_tb_bolsista diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_chamada_publica.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_chamada_publica.sql new file mode 100644 index 00000000..355aa901 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_chamada_publica.sql @@ -0,0 +1,35 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_chamada_publica as ( + select + co_chamada_publica::text as co_chamada_publica, + co_projeto::text as co_projeto, + co_situacao_chamada::text as co_situacao_chamada, + co_usuario_criacao::text as co_usuario_criacao, + co_programa::text as co_programa, + nu_chamada_publica::text as nu_chamada_publica, + nu_ano::text as nu_ano, + ds_chamada_publica::text as ds_chamada_publica, + ds_numero_sei::text as ds_numero_sei, + {{ safe_numeric('vl_global_estimado') }} as vl_global_estimado, + dt_ini_pesquisa::text as dt_ini_pesquisa, + dt_fim_pesquisa::text as dt_fim_pesquisa, + dt_publicacao_dou::text as dt_publicacao_dou, + dt_previsao_resultado::text as dt_previsao_resultado, + dt_ini_bolsa::text as dt_ini_bolsa, + dt_criacao::text as dt_criacao, + dt_inicio_inscricao::text as dt_inicio_inscricao, + dt_fim_inscricao::text as dt_fim_inscricao, + dt_inicio_julgamento::text as dt_inicio_julgamento, + dt_fim_julgamento::text as dt_fim_julgamento, + dt_publicacao_resultado::text as dt_publicacao_resultado, + tp_moeda::text as tp_moeda, + dt_fim_recurso::text as dt_fim_recurso, + dt_inicio_recurso::text as dt_inicio_recurso, + dt_inicio_previsao_bolsa::text as dt_inicio_previsao_bolsa + from {{ source("sisbolsas", "tb_chamada_publica") }} + ) + +select * +from sisbolsas_tb_chamada_publica diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_chapubli_fontfina.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_chapubli_fontfina.sql new file mode 100644 index 00000000..f397e676 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_chapubli_fontfina.sql @@ -0,0 +1,12 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_chapubli_fontfina as ( + select + co_chamada_publica::text as co_chamada_publica, + co_fonte_financeira::text as co_fonte_financeira + from {{ source("sisbolsas", "tb_chapubli_fontfina") }} + ) + +select * +from sisbolsas_tb_chapubli_fontfina diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_chapubli_unidade.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_chapubli_unidade.sql new file mode 100644 index 00000000..c2b51d46 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_chapubli_unidade.sql @@ -0,0 +1,12 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_chapubli_unidade as ( + select + co_chamada_publica::text as co_chamada_publica, + co_unidade::text as co_unidade + from {{ source("sisbolsas", "tb_chapubli_unidade") }} + ) + +select * +from sisbolsas_tb_chapubli_unidade diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_coordenador.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_coordenador.sql new file mode 100644 index 00000000..c9476f3f --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_coordenador.sql @@ -0,0 +1,12 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_coordenador as ( + select + co_chamada_publica::text as co_chamada_publica, + regexp_replace(ds_cpf, '[^0-9]', '', 'g')::text as ds_cpf + from {{ source("sisbolsas", "tb_coordenador") }} + ) + +select * +from sisbolsas_tb_coordenador diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_dado_formacao.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_dado_formacao.sql new file mode 100644 index 00000000..fd5d45ce --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_dado_formacao.sql @@ -0,0 +1,25 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_dado_formacao as ( + select + co_dado_formacao::text as co_dado_formacao, + co_usuario::text as co_usuario, + co_nivel_escolaridade::text as co_nivel_escolaridade, + co_tipo_escolar::text as co_tipo_escolar, + st_nivel_escolaridade::text as st_nivel_escolaridade, + tp_nacionalidade_diploma::text as tp_nacionalidade_diploma, + {{ safe_boolean('in_diploma_valido') }} as in_diploma_valido, + co_area_formacao::text as co_area_formacao, + ds_outra_formacao::text as ds_outra_formacao, + ds_curso::text as ds_curso, + ds_instituicao::text as ds_instituicao, + co_estado::text as co_estado, + ds_url_curriculo::text as ds_url_curriculo, + dt_criacao::text as dt_criacao, + co_pais::text as co_pais + from {{ source("sisbolsas", "tb_dado_formacao") }} + ) + +select * +from sisbolsas_tb_dado_formacao diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_dado_pessoal.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_dado_pessoal.sql new file mode 100644 index 00000000..c2cc1e85 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_dado_pessoal.sql @@ -0,0 +1,35 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_dado_pessoal as ( + select + co_dado_pessoal::text as co_dado_pessoal, + co_usuario::text as co_usuario, + regexp_replace(ds_cpf, '[^0-9]', '', 'g')::text as ds_cpf, + dt_nascimento::text as dt_nascimento, + co_estado_civil::text as co_estado_civil, + tp_sexo::text as tp_sexo, + tp_nacionalidade::text as tp_nacionalidade, + ds_naturalidade::text as ds_naturalidade, + ds_rg::text as ds_rg, + dt_emissao_rg::text as dt_emissao_rg, + ds_orgao_emissor::text as ds_orgao_emissor, + co_estado_orgao::text as co_estado_orgao, + ds_passaporte::text as ds_passaporte, + tp_visto::text as tp_visto, + dt_validade_visto::text as dt_validade_visto, + nu_telefone_principal::text as nu_telefone_principal, + nu_telefone_alternativo::text as nu_telefone_alternativo, + dt_criacao::text as dt_criacao, + co_pais_passaporte::text as co_pais_passaporte, + ds_ddd_principal::text as ds_ddd_principal, + ds_ddd_alternativo::text as ds_ddd_alternativo, + co_etnia::text as co_etnia, + ds_email_alternativo::text as ds_email_alternativo, + tp_fator::text as tp_fator, + tp_sanguineo::text as tp_sanguineo + from {{ source("sisbolsas", "tb_dado_pessoal") }} + ) + +select * +from sisbolsas_tb_dado_pessoal diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_dado_profissional.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_dado_profissional.sql new file mode 100644 index 00000000..2d5515b3 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_dado_profissional.sql @@ -0,0 +1,20 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_dado_profissional as ( + select + co_dado_profissional::text as co_dado_profissional, + tp_situacao_funcional::text as tp_situacao_funcional, + {{ safe_boolean('in_vinculo') }} as in_vinculo, + tp_setor::text as tp_setor, + {{ safe_boolean('in_funcao_gratificada') }} as in_funcao_gratificada, + ds_instituicao::text as ds_instituicao, + ds_empregador::text as ds_empregador, + tp_cargo::text as tp_cargo, + co_usuario::text as co_usuario, + dt_criacao::text as dt_criacao + from {{ source("sisbolsas", "tb_dado_profissional") }} + ) + +select * +from sisbolsas_tb_dado_profissional diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_estado.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_estado.sql new file mode 100644 index 00000000..f678150e --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_estado.sql @@ -0,0 +1,13 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_estado as ( + select + co_estado::text as co_estado, + ds_uf::text as ds_uf, + ds_nome::text as ds_nome + from {{ source("sisbolsas", "tb_estado") }} + ) + +select * +from sisbolsas_tb_estado diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_folha_bolsista.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_folha_bolsista.sql new file mode 100644 index 00000000..0092303e --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_folha_bolsista.sql @@ -0,0 +1,22 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_folha_bolsista as ( + select + co_folha_pagamento::text as co_folha_pagamento, + co_usuario::text as co_usuario, + co_selecao::text as co_selecao, + nu_bolsa::text as nu_bolsa, + co_dado_bancario::text as co_dado_bancario, + co_diretoria::text as co_diretoria, + co_unidade::text as co_unidade, + co_fonte_financeira::text as co_fonte_financeira, + nu_dias_pago::text as nu_dias_pago, + {{ safe_numeric('vl_dia_pago') }} as vl_dia_pago, + {{ safe_numeric('vl_total_pago') }} as vl_total_pago, + {{ safe_boolean('in_conferencia') }} as in_conferencia + from {{ source("sisbolsas", "tb_folha_bolsista") }} + ) + +select * +from sisbolsas_tb_folha_bolsista diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_folha_pagamento.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_folha_pagamento.sql new file mode 100644 index 00000000..1bfcd808 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_folha_pagamento.sql @@ -0,0 +1,22 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_folha_pagamento as ( + select + co_folha_pagamento::text as co_folha_pagamento, + nu_solicitacao::text as nu_solicitacao, + nu_mes::text as nu_mes, + nu_ano::text as nu_ano, + nu_lote::text as nu_lote, + co_situacao_folha::text as co_situacao_folha, + dt_criacao::text as dt_criacao, + nu_total_integral::text as nu_total_integral, + nu_total_parcial::text as nu_total_parcial, + co_usuario_criacao::text as co_usuario_criacao, + co_usuario_homologacao::text as co_usuario_homologacao, + dt_homologacao::text as dt_homologacao + from {{ source("sisbolsas", "tb_folha_pagamento") }} + ) + +select * +from sisbolsas_tb_folha_pagamento diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_fonte_financeira.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_fonte_financeira.sql new file mode 100644 index 00000000..3c285f7c --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_fonte_financeira.sql @@ -0,0 +1,12 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_fonte_financeira as ( + select + co_fonte_financeira::text as co_fonte_financeira, + ds_fonte_financeira::text as ds_fonte_financeira + from {{ source("sisbolsas", "tb_fonte_financeira") }} + ) + +select * +from sisbolsas_tb_fonte_financeira diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_modalidade.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_modalidade.sql new file mode 100644 index 00000000..98a13a9b --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_modalidade.sql @@ -0,0 +1,16 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_modalidade as ( + select + co_modalidade::text as co_modalidade, + ds_modalidade::text as ds_modalidade, + co_nivel_escolaridade::text as co_nivel_escolaridade, + st_nivel_escolaridade::text as st_nivel_escolaridade, + dt_inicio::text as dt_inicio, + dt_fim::text as dt_fim + from {{ source("sisbolsas", "tb_modalidade") }} + ) + +select * +from sisbolsas_tb_modalidade diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_nivel_escolaridade.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_nivel_escolaridade.sql new file mode 100644 index 00000000..9741b6f4 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_nivel_escolaridade.sql @@ -0,0 +1,12 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_nivel_escolaridade as ( + select + co_nivel_escolaridade::text as co_nivel_escolaridade, + ds_nivel_escolaridade::text as ds_nivel_escolaridade + from {{ source("sisbolsas", "tb_nivel_escolaridade") }} + ) + +select * +from sisbolsas_tb_nivel_escolaridade diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_processo_seletivo.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_processo_seletivo.sql new file mode 100644 index 00000000..4601551c --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_processo_seletivo.sql @@ -0,0 +1,31 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_processo_seletivo as ( + select + co_processo_seletivo::text as co_processo_seletivo, + co_usuario::text as co_usuario, + co_dado_formacao::text as co_dado_formacao, + co_selecao::text as co_selecao, + co_dado_profissional::text as co_dado_profissional, + co_situacao_concessao::text as co_situacao_concessao, + st_processo_seletivo::text as st_processo_seletivo, + dt_criacao::text as dt_criacao, + {{ safe_boolean('in_nao_apto') }} as in_nao_apto, + {{ safe_boolean('in_classificacao') }} as in_classificacao, + tx_observacao_avaliacao::text as tx_observacao_avaliacao, + tx_observacao_classificacao::text as tx_observacao_classificacao, + {{ safe_numeric('vl_total_criterio') }} as vl_total_criterio, + {{ safe_numeric('vl_total_geral') }} as vl_total_geral, + tx_observacao_entrevista::text as tx_observacao_entrevista, + ds_instituicao_bolsa::text as ds_instituicao_bolsa, + nu_posicao::text as nu_posicao, + ds_token_aceite::text as ds_token_aceite, + {{ safe_boolean('in_bolsa_ativa') }} as in_bolsa_ativa, + ds_institu_bolsa_ativa::text as ds_institu_bolsa_ativa, + {{ safe_boolean('in_declaracao_veracidade') }} as in_declaracao_veracidade + from {{ source("sisbolsas", "tb_processo_seletivo") }} + ) + +select * +from sisbolsas_tb_processo_seletivo diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_programa.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_programa.sql new file mode 100644 index 00000000..40cda55f --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_programa.sql @@ -0,0 +1,13 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_programa as ( + select + co_programa::text as co_programa, + ds_programa::text as ds_programa, + co_programa_pai::text as co_programa_pai + from {{ source("sisbolsas", "tb_programa") }} + ) + +select * +from sisbolsas_tb_programa diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_selecao.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_selecao.sql new file mode 100644 index 00000000..b655087d --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_selecao.sql @@ -0,0 +1,35 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_selecao as ( + select + co_selecao::text as co_selecao, + co_chamada_publica::text as co_chamada_publica, + co_modalidade::text as co_modalidade, + co_estado::text as co_estado, + co_area_formacao::text as co_area_formacao, + co_nivel_escolaridade::text as co_nivel_escolaridade, + tp_atuacao::text as tp_atuacao, + st_nivel_escolaridade::text as st_nivel_escolaridade, + qt_duracao::text as qt_duracao, + {{ safe_numeric('vl_total') }} as vl_total, + qt_bolsa::text as qt_bolsa, + ds_area_especializacao::text as ds_area_especializacao, + ds_outra_formacao::text as ds_outra_formacao, + {{ safe_boolean('in_anexo_projeto') }} as in_anexo_projeto, + {{ safe_boolean('in_anexo_idioma') }} as in_anexo_idioma, + {{ safe_boolean('in_anexo_historico_escolar') }} as in_anexo_historico_escolar, + dt_criacao::text as dt_criacao, + co_usuario::text as co_usuario, + co_cidade::text as co_cidade, + nu_passo_avaliacao::text as nu_passo_avaliacao, + st_avaliacao::text as st_avaliacao, + {{ safe_boolean('in_entrevista') }} as in_entrevista, + nu_selecao::text as nu_selecao, + {{ safe_boolean('in_anexo_outro') }} as in_anexo_outro, + ds_anexo_outro::text as ds_anexo_outro + from {{ source("sisbolsas", "tb_selecao") }} + ) + +select * +from sisbolsas_tb_selecao diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_situacao_chamada.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_situacao_chamada.sql new file mode 100644 index 00000000..310eb587 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_situacao_chamada.sql @@ -0,0 +1,12 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_situacao_chamada as ( + select + co_situacao_chamada::text as co_situacao_chamada, + ds_situacao_chamada::text as ds_situacao_chamada + from {{ source("sisbolsas", "tb_situacao_chamada") }} + ) + +select * +from sisbolsas_tb_situacao_chamada diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_situacao_concessao.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_situacao_concessao.sql new file mode 100644 index 00000000..adedeb23 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_situacao_concessao.sql @@ -0,0 +1,12 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_situacao_concessao as ( + select + co_situacao_concessao::text as co_situacao_concessao, + ds_situacao_concessao::text as ds_situacao_concessao + from {{ source("sisbolsas", "tb_situacao_concessao") }} + ) + +select * +from sisbolsas_tb_situacao_concessao diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_unidade.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_unidade.sql new file mode 100644 index 00000000..ae0ea4bd --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_unidade.sql @@ -0,0 +1,15 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_unidade as ( + select + co_unidade::text as co_unidade, + ds_unidade::text as ds_unidade, + co_estado::text as co_estado, + ds_sigla::text as ds_sigla, + co_diretoria::text as co_diretoria + from {{ source("sisbolsas", "tb_unidade") }} + ) + +select * +from sisbolsas_tb_unidade diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_usuario.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_usuario.sql new file mode 100644 index 00000000..02798275 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/bronze/sisbolsas_tb_usuario.sql @@ -0,0 +1,22 @@ +{{ config(materialized="table") }} + +with + sisbolsas_tb_usuario as ( + select + co_usuario::text as co_usuario, + ds_nome::text as ds_nome, + ds_login::text as ds_login, + co_perfil::text as co_perfil, + ds_email::text as ds_email, + ds_senha::text as ds_senha, + dt_criacao::text as dt_criacao, + co_tipo_login::text as co_tipo_login, + st_usuario::text as st_usuario, + co_anexo::text as co_anexo, + ds_token_redefinir_senha::text as ds_token_redefinir_senha, + dt_token_senha::text as dt_token_senha + from {{ source("sisbolsas", "tb_usuario") }} + ) + +select * +from sisbolsas_tb_usuario diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/silver/bolsistas.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/silver/bolsistas.sql new file mode 100644 index 00000000..9da6d530 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/silver/bolsistas.sql @@ -0,0 +1,127 @@ +with + bolsistas as ( + select + b.co_usuario, + b.co_selecao, + b.nu_bolsa, + b.co_situacao_bolsista, + case + when b.dt_inicio ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(b.dt_inicio from 1 for 10)::date + end as dt_inicio, + case + when b.dt_fim ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(b.dt_fim from 1 for 10)::date + end as dt_fim + from {{ ref("sisbolsas_tb_bolsista") }} as b + ), + + usuarios as ( + select + u.co_usuario, + u.ds_nome, + regexp_replace(u.ds_login, '[^0-9]', '', 'g') as ds_login_cpf + from {{ ref("sisbolsas_tb_usuario") }} as u + ), + + selecoes as ( + select s.co_selecao, s.co_chamada_publica, s.co_modalidade, s.tp_atuacao + from {{ ref("sisbolsas_tb_selecao") }} as s + ), + + chamadas as ( + select cp.co_chamada_publica, cp.co_programa, cp.co_projeto, cp.tp_moeda + from {{ ref("sisbolsas_tb_chamada_publica") }} as cp + ), + + unidade_por_chamada as ( + select + cu.co_chamada_publica, + string_agg(distinct u.ds_sigla, ' | ' order by u.ds_sigla) as unidade, + string_agg(distinct e.ds_uf, ' | ' order by e.ds_uf) as uf_unidade + from {{ ref("sisbolsas_tb_chapubli_unidade") }} as cu + left join {{ ref("sisbolsas_tb_unidade") }} as u on cu.co_unidade = u.co_unidade + left join {{ ref("sisbolsas_tb_estado") }} as e on u.co_estado = e.co_estado + group by 1 + ), + + pagamentos as ( + select + fb.co_usuario, + fb.co_fonte_financeira, + fb.vl_total_pago, + fp.nu_mes, + fp.nu_ano, + case + when fp.nu_ano ~ '^[0-9]{4}$' and fp.nu_mes ~ '^[0-9]{1,2}$' + then + to_date( + fp.nu_ano || '-' || lpad(fp.nu_mes, 2, '0') || '-01', 'YYYY-MM-DD' + ) + end as mes_referencia + from {{ ref("sisbolsas_tb_folha_bolsista") }} as fb + left join + {{ ref("sisbolsas_tb_folha_pagamento") }} as fp + on fb.co_folha_pagamento = fp.co_folha_pagamento + ), + + coordenador_por_selecao as ( + select + bc.co_selecao, + string_agg( + distinct coalesce(uc.ds_nome, bc.ds_cpf), + ' | ' + order by coalesce(uc.ds_nome, bc.ds_cpf) + ) as coordenador + from {{ ref("sisbolsas_tb_bolsa_coordenador") }} as bc + left join + usuarios as uc + on regexp_replace(bc.ds_cpf, '[^0-9]', '', 'g') = uc.ds_login_cpf + group by 1 + ) + +select + uc.unidade as unidade, + ub.ds_nome as bolsista, + proj.tituloprojeto as titulo_projeto, + prog.ds_programa as programa, + mod.ds_modalidade as modalidade, + b.dt_inicio as inicio, + b.dt_fim as termino, + uc.uf_unidade, + p.vl_total_pago as valor, + ff.ds_fonte_financeira as recurso, + coord.coordenador, + p.mes_referencia, + b.co_situacao_bolsista as situacao_bolsista, + case + when s.tp_atuacao = '1' + then 'Presencial' + when s.tp_atuacao = '2' + then 'Não presencial' + end as atividade, + case + when c.tp_moeda = '1' + then 'Real (R$)' + when c.tp_moeda is null + then null + else 'Estrangeira' + end as moeda, + case + when p.nu_mes ~ '^[0-9]{1,2}$' then p.nu_mes::integer + end as mes_referencia_numero, + case when p.nu_ano ~ '^[0-9]{4}$' then p.nu_ano::integer end as ano_referencia +from bolsistas as b +left join usuarios as ub on b.co_usuario = ub.co_usuario +left join selecoes as s on b.co_selecao = s.co_selecao +left join chamadas as c on s.co_chamada_publica = c.co_chamada_publica +left join unidade_por_chamada as uc on c.co_chamada_publica = uc.co_chamada_publica +left join {{ ref("sisbolsas_tb_programa") }} as prog on c.co_programa = prog.co_programa +left join + {{ ref("sisbolsas_tb_modalidade") }} as mod on s.co_modalidade = mod.co_modalidade +left join pagamentos as p on b.co_usuario = p.co_usuario +left join + {{ ref("sisbolsas_tb_fonte_financeira") }} as ff + on p.co_fonte_financeira = ff.co_fonte_financeira +left join coordenador_por_selecao as coord on b.co_selecao = coord.co_selecao +left join {{ ref("ipea_pro_projetos") }} as proj on proj.projetoid::text = c.co_projeto diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/silver/bolsistas_diversidade.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/silver/bolsistas_diversidade.sql new file mode 100644 index 00000000..cadcbded --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/silver/bolsistas_diversidade.sql @@ -0,0 +1,75 @@ +with + bolsistas_base as ( + select distinct b.co_usuario, b.co_selecao + from {{ ref("sisbolsas_tb_bolsista") }} as b + ), + + unidade_por_bolsista as ( + select distinct bb.co_usuario, coalesce(u.ds_sigla, 'SEM_UNIDADE') as unidade + from bolsistas_base as bb + left join {{ ref("sisbolsas_tb_selecao") }} as s on bb.co_selecao = s.co_selecao + left join + {{ ref("sisbolsas_tb_chapubli_unidade") }} as cu + on s.co_chamada_publica = cu.co_chamada_publica + left join {{ ref("sisbolsas_tb_unidade") }} as u on cu.co_unidade = u.co_unidade + ), + + demografia as ( + select + dp.co_usuario, + case when upper(trim(dp.tp_sexo)) = 'F' then 1 else 0 end as is_mulher, + case + when dp.co_etnia is null or upper(trim(dp.co_etnia)) = 'NAN' + then 0 + when split_part(trim(dp.co_etnia), '.', 1) in ('2', '4') + then 1 + else 0 + end as is_negro + from {{ ref("sisbolsas_tb_dado_pessoal") }} as dp + ), + + agregacao as ( + select + ub.unidade, + count(distinct ub.co_usuario) as total_bolsistas, + count( + distinct case when d.is_mulher = 1 then ub.co_usuario end + ) as total_mulheres, + count( + distinct case when d.is_negro = 1 then ub.co_usuario end + ) as total_negros + from unidade_por_bolsista as ub + left join demografia as d on ub.co_usuario = d.co_usuario + group by 1 + ), + + metricas as ( + select + unidade, + total_bolsistas, + total_mulheres, + total_negros, + ceil(total_bolsistas * 0.40)::integer as meta_minima_mulheres_40, + ceil(total_bolsistas * 0.30)::integer as meta_minima_negros_30, + round( + (total_mulheres::numeric / nullif(total_bolsistas, 0)) * 100, 2 + ) as percentual_mulheres, + round( + (total_negros::numeric / nullif(total_bolsistas, 0)) * 100, 2 + ) as percentual_negros + from agregacao + ) + +select + unidade, + total_bolsistas, + total_mulheres, + total_negros, + percentual_mulheres, + percentual_negros, + meta_minima_mulheres_40, + meta_minima_negros_30, + greatest(meta_minima_mulheres_40 - total_mulheres, 0) as mulheres_faltantes_meta, + greatest(meta_minima_negros_30 - total_negros, 0) as negros_faltantes_meta +from metricas +order by total_bolsistas desc, unidade asc diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/silver/chamadas_publicas.sql b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/silver/chamadas_publicas.sql new file mode 100644 index 00000000..e92961e1 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/silver/chamadas_publicas.sql @@ -0,0 +1,382 @@ +with + chamadas_base as ( + select + co_chamada_publica, + co_projeto, + co_situacao_chamada, + co_usuario_criacao, + co_programa, + nu_chamada_publica, + nu_ano, + ds_chamada_publica, + ds_numero_sei, + vl_global_estimado, + dt_ini_pesquisa, + dt_fim_pesquisa, + dt_publicacao_dou, + dt_previsao_resultado, + dt_ini_bolsa, + dt_criacao, + dt_inicio_inscricao, + dt_fim_inscricao, + dt_inicio_julgamento, + dt_fim_julgamento, + dt_publicacao_resultado, + tp_moeda, + dt_fim_recurso, + dt_inicio_recurso, + dt_inicio_previsao_bolsa + from {{ ref("sisbolsas_tb_chamada_publica") }} + ), + + chamadas as ( + select + c.co_chamada_publica, + regexp_replace(btrim(c.co_projeto, E' \t\n\r'), '[.]0+$', '') as co_projeto, + c.co_situacao_chamada, + c.co_usuario_criacao, + c.co_programa, + case + when btrim(c.nu_chamada_publica, E' \t\n\r') ~ '^[0-9]+([.]0+)?$' + then regexp_replace(btrim(c.nu_chamada_publica, E' \t\n\r'), '[.]0+$', '') + else btrim(c.nu_chamada_publica, E' \t\n\r') + end as nu_chamada_publica, + case + when btrim(c.nu_ano, E' \t\n\r') ~ '^[0-9]{4}([.]0+)?$' + then regexp_replace(btrim(c.nu_ano, E' \t\n\r'), '[.]0+$', '')::integer + end as ano_chamada, + c.ds_chamada_publica, + c.ds_numero_sei, + c.vl_global_estimado, + case + when c.dt_ini_pesquisa ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_ini_pesquisa from 1 for 10)::date + end as dt_ini_pesquisa, + case + when c.dt_fim_pesquisa ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_fim_pesquisa from 1 for 10)::date + end as dt_fim_pesquisa, + case + when c.dt_publicacao_dou ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_publicacao_dou from 1 for 10)::date + end as dt_publicacao_dou, + case + when c.dt_previsao_resultado ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_previsao_resultado from 1 for 10)::date + end as dt_previsao_resultado, + case + when c.dt_ini_bolsa ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_ini_bolsa from 1 for 10)::date + end as dt_ini_bolsa, + case + when c.dt_criacao ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_criacao from 1 for 10)::date + end as dt_criacao, + case + when c.dt_inicio_inscricao ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_inicio_inscricao from 1 for 10)::date + end as dt_inicio_inscricao, + case + when c.dt_fim_inscricao ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_fim_inscricao from 1 for 10)::date + end as dt_fim_inscricao, + case + when c.dt_inicio_julgamento ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_inicio_julgamento from 1 for 10)::date + end as dt_inicio_julgamento, + case + when c.dt_fim_julgamento ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_fim_julgamento from 1 for 10)::date + end as dt_fim_julgamento, + case + when c.dt_publicacao_resultado ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_publicacao_resultado from 1 for 10)::date + end as dt_publicacao_resultado, + case + when c.dt_fim_recurso ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_fim_recurso from 1 for 10)::date + end as dt_fim_recurso, + case + when c.dt_inicio_recurso ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_inicio_recurso from 1 for 10)::date + end as dt_inicio_recurso, + case + when c.dt_inicio_previsao_bolsa ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then substring(c.dt_inicio_previsao_bolsa from 1 for 10)::date + end as dt_inicio_previsao_bolsa, + c.tp_moeda, + case + when c.tp_moeda = '1' + then 'Real (R$)' + when c.tp_moeda is null + then null + else 'Estrangeira' + end as moeda + from chamadas_base as c + ), + + usuarios as ( + select + u.co_usuario, + u.ds_nome, + regexp_replace(u.ds_login, '[^0-9]', '', 'g') as ds_login_cpf + from {{ ref("sisbolsas_tb_usuario") }} as u + ), + + programas as ( + select p.co_programa, p.ds_programa + from {{ ref("sisbolsas_tb_programa") }} as p + ), + + situacoes as ( + select s.co_situacao_chamada, s.ds_situacao_chamada + from {{ ref("sisbolsas_tb_situacao_chamada") }} as s + ), + + unidades as ( + select + cu.co_chamada_publica, + string_agg(distinct u.ds_sigla, ' | ' order by u.ds_sigla) as unidades_sigla, + string_agg(distinct u.ds_unidade, ' | ' order by u.ds_unidade) as unidades_nome, + string_agg(distinct e.ds_uf, ' | ' order by e.ds_uf) as ufs + from {{ ref("sisbolsas_tb_chapubli_unidade") }} as cu + left join {{ ref("sisbolsas_tb_unidade") }} as u on cu.co_unidade = u.co_unidade + left join {{ ref("sisbolsas_tb_estado") }} as e on u.co_estado = e.co_estado + group by 1 + ), + + modalidades as ( + select + s.co_chamada_publica, + count(distinct s.co_selecao) as total_selecoes, + string_agg(distinct m.ds_modalidade, ' | ' order by m.ds_modalidade) + as modalidades, + sum( + case + when s.qt_bolsa ~ '^[0-9]+(\\.[0-9]+)?$' + then s.qt_bolsa::numeric + end + ) as cotas, + sum(s.vl_total) as valor_total_selecoes, + max( + case + when s.qt_duracao ~ '^[0-9]+(\\.[0-9]+)?$' + then s.qt_duracao::numeric + end + ) as prazo_meses + from {{ ref("sisbolsas_tb_selecao") }} as s + left join {{ ref("sisbolsas_tb_modalidade") }} as m on s.co_modalidade = m.co_modalidade + group by 1 + ), + + processos as ( + select + p.co_selecao, + p.in_classificacao, + p.in_nao_apto, + p.in_bolsa_ativa + from {{ ref("sisbolsas_tb_processo_seletivo") }} as p + ), + + processos_agg as ( + select + s.co_chamada_publica, + count(*) as total_processos, + sum(case when p.in_classificacao is true then 1 else 0 end) + as total_classificados, + sum(case when p.in_nao_apto is true then 1 else 0 end) as total_nao_aptos, + sum(case when p.in_bolsa_ativa is true then 1 else 0 end) + as total_bolsas_ativas, + sum( + case + when p.in_classificacao is true + and not coalesce(p.in_bolsa_ativa, false) + then 1 + else 0 + end + ) as reservas + from processos as p + left join {{ ref("sisbolsas_tb_selecao") }} as s on p.co_selecao = s.co_selecao + group by 1 + ), + + fontes_chamada as ( + select + cf.co_chamada_publica, + string_agg( + distinct ff.ds_fonte_financeira, + ' | ' + order by ff.ds_fonte_financeira + ) as fontes_recurso_chamada + from {{ ref("sisbolsas_tb_chapubli_fontfina") }} as cf + left join + {{ ref("sisbolsas_tb_fonte_financeira") }} as ff + on cf.co_fonte_financeira = ff.co_fonte_financeira + group by 1 + ), + + coordenadores_chamada as ( + select + c.co_chamada_publica, + string_agg( + distinct coalesce(u.ds_nome, c.ds_cpf), + ' | ' + order by coalesce(u.ds_nome, c.ds_cpf) + ) as coordenador_chamada + from {{ ref("sisbolsas_tb_coordenador") }} as c + left join usuarios as u on c.ds_cpf = u.ds_login_cpf + group by 1 + ), + + projetos as ( + select + p.projetoid, + p.projetoid::text as co_projeto, + p.tituloprojeto, + p.numeroprojeto, + p.anoprojeto, + p.coordenadorid, + p.diretoriaid, + p.statusprojetoid + from {{ ref("ipea_pro_projetos") }} as p + ), + + diretorias as ( + select d.diretoriaid, d.diretorianome, d.diretoriasigla + from {{ ref("ipea_pro_diretorias") }} as d + ), + + coordenadores_projeto as ( + select sp.servidorpublicoid, sp.nomeservidor + from {{ ref("ipea_pro_servidorespublicos") }} as sp + ), + + status_projetos as ( + select s.statusprojetoid, s.nomestatus + from {{ ref("ipea_pro_statusprojetos") }} as s + ), + + fontes_projeto as ( + select + fr.projetoid::text as co_projeto, + string_agg( + distinct coalesce(ifr.nomeitemfontereceita, fr.descricaofonte), + ' | ' + order by coalesce(ifr.nomeitemfontereceita, fr.descricaofonte) + ) as fontes_recurso_projeto, + sum(fr.valortotalfonte) as valor_total_fontes_projeto + from {{ ref("ipea_pro_fontesreceitas") }} as fr + left join + {{ ref("ipea_pro_itemfontereceitas") }} as ifr + on fr.itemfontereceitaid = ifr.itemfontereceitaid + group by 1 + ), + + fontes_projeto_por_ano_raw as ( + select + fr.projetoid, + coalesce( + extract(year from fr.datainiciofonte), + extract(year from fr.datafinalfonte) + )::integer as ano, + sum(fr.valortotalfonte) as valor_ano + from {{ ref("ipea_pro_fontesreceitas") }} as fr + group by 1, 2 + ), + + fontes_projeto_por_ano as ( + select + f.projetoid::text as co_projeto, + jsonb_object_agg(f.ano, f.valor_ano order by f.ano) + filter (where f.ano is not null) as valor_por_ano + from fontes_projeto_por_ano_raw as f + group by 1 + ) + +select + c.co_chamada_publica as chamada_id, + c.ds_numero_sei as processo_sei, + concat_ws('/', c.nu_chamada_publica, c.ano_chamada::text) as numero_chamada, + c.ano_chamada, + concat_ws('/', c.nu_chamada_publica, c.ano_chamada::text) as numero_chamada_ano, + c.ds_chamada_publica as descricao_chamada, + prog.ds_programa as tipo_chamada, + c.co_programa as programa_id, + sit.ds_situacao_chamada as situacao_chamada, + c.co_situacao_chamada as situacao_chamada_id, + c.co_projeto as projeto_id, + proj.tituloprojeto as projeto_titulo, + proj.numeroprojeto as projeto_numero, + proj.anoprojeto as projeto_ano, + dir.diretorianome as diretoria, + dir.diretoriasigla as diretoria_sigla, + coalesce(coord_proj.nomeservidor, coord_chamada.coordenador_chamada) + as coordenador, + coord_chamada.coordenador_chamada, + coord_proj.nomeservidor as coordenador_projeto, + status_projeto.nomestatus as status_projeto, + unidades.unidades_sigla, + unidades.unidades_nome, + unidades.ufs, + modalidades.modalidades, + modalidades.total_selecoes, + modalidades.prazo_meses, + c.dt_ini_pesquisa as inicio_pesquisa, + c.dt_fim_pesquisa as fim_pesquisa, + case + when c.dt_ini_pesquisa is not null and c.dt_fim_pesquisa is not null + then (c.dt_fim_pesquisa - c.dt_ini_pesquisa) + end as prazo_pesquisa_dias, + c.dt_inicio_inscricao as inicio_inscricao, + c.dt_fim_inscricao as fim_inscricao, + c.dt_inicio_julgamento as inicio_julgamento, + c.dt_fim_julgamento as fim_julgamento, + c.dt_publicacao_dou as publicacao_dou, + c.dt_previsao_resultado as previsao_resultado, + c.dt_publicacao_resultado as publicacao_resultado, + c.dt_inicio_previsao_bolsa as inicio_previsao_bolsa, + c.dt_ini_bolsa as inicio_bolsa, + c.dt_inicio_recurso as inicio_recurso, + c.dt_fim_recurso as fim_recurso, + c.dt_criacao as data_criacao, + c.moeda, + coalesce( + c.vl_global_estimado, + modalidades.valor_total_selecoes, + fontes_projeto.valor_total_fontes_projeto + ) as valor_total_chamada, + c.vl_global_estimado as valor_global_estimado, + modalidades.valor_total_selecoes, + fontes_projeto.valor_total_fontes_projeto, + fontes_projeto_por_ano.valor_por_ano, + coalesce( + fontes_chamada.fontes_recurso_chamada, + fontes_projeto.fontes_recurso_projeto + ) as fonte_recurso, + fontes_chamada.fontes_recurso_chamada, + fontes_projeto.fontes_recurso_projeto, + modalidades.cotas, + processos.reservas, + processos.total_processos, + processos.total_classificados, + processos.total_nao_aptos, + processos.total_bolsas_ativas, + usuario_criacao.ds_nome as usuario_criacao +from chamadas as c +left join programas as prog on c.co_programa = prog.co_programa +left join situacoes as sit on c.co_situacao_chamada = sit.co_situacao_chamada +left join unidades on c.co_chamada_publica = unidades.co_chamada_publica +left join modalidades on c.co_chamada_publica = modalidades.co_chamada_publica +left join processos_agg as processos on c.co_chamada_publica = processos.co_chamada_publica +left join fontes_chamada on c.co_chamada_publica = fontes_chamada.co_chamada_publica +left join coordenadores_chamada as coord_chamada + on c.co_chamada_publica = coord_chamada.co_chamada_publica +left join projetos as proj on c.co_projeto = proj.co_projeto +left join diretorias as dir on proj.diretoriaid = dir.diretoriaid +left join coordenadores_projeto as coord_proj + on proj.coordenadorid = coord_proj.servidorpublicoid +left join status_projetos as status_projeto + on proj.statusprojetoid = status_projeto.statusprojetoid +left join fontes_projeto on c.co_projeto = fontes_projeto.co_projeto +left join fontes_projeto_por_ano on c.co_projeto = fontes_projeto_por_ano.co_projeto +left join usuarios as usuario_criacao on c.co_usuario_criacao = usuario_criacao.co_usuario diff --git a/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/silver/schema.yml b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/silver/schema.yml new file mode 100644 index 00000000..0aabe4b6 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sistema_sisbolsas/silver/schema.yml @@ -0,0 +1,199 @@ +version: 2 + +models: + - name: bolsistas + description: > + Modelo silver de bolsistas do Sisbolsas. Parte de `sisbolsas_tb_bolsista` + e consolida dados de usuário, seleção/chamada, unidade/UF, projeto, + programa, modalidade, pagamentos e coordenador. + meta: + tags: + - silver + columns: + - name: unidade + description: Sigla da unidade associada à chamada pública. + - name: bolsista + description: Nome do bolsista. + - name: titulo_projeto + description: Título do projeto vinculado à chamada pública. + - name: programa + description: Programa da chamada pública. + - name: modalidade + description: Modalidade da seleção. + - name: inicio + description: Data de início da bolsa do bolsista. + - name: termino + description: Data de término da bolsa do bolsista. + - name: atividade + description: Tipo de atuação da seleção (Presencial ou Não presencial). + - name: uf_unidade + description: UF da unidade vinculada à chamada pública. + - name: valor + description: Valor pago ao bolsista na folha. + - name: moeda + description: Tipo de moeda da chamada pública. + - name: recurso + description: Fonte financeira utilizada no pagamento. + - name: coordenador + description: Coordenador(es) associado(s) à bolsa/chamada. + - name: mes_referencia + description: Mês de referência do pagamento (primeiro dia do mês). + - name: mes_referencia_numero + description: Número do mês de referência do pagamento. + - name: ano_referencia + description: Ano de referência do pagamento. + - name: situacao_bolsista + description: Situação do bolsista conforme `co_situacao_bolsista`. + + - name: bolsistas_diversidade + description: > + Indicadores de diversidade de bolsistas por unidade (sigla), com totais, + percentuais e metas mínimas de participação de mulheres (40%) e negros (30%). + meta: + tags: + - silver + columns: + - name: unidade + description: Sigla da unidade (ou `SEM_UNIDADE` quando não há vínculo). + - name: total_bolsistas + description: Total de bolsistas distintos na unidade. + - name: total_mulheres + description: Total de bolsistas mulheres na unidade. + - name: total_negros + description: Total de bolsistas negros na unidade (etnia preta ou parda). + - name: percentual_mulheres + description: Percentual de mulheres sobre o total de bolsistas da unidade. + - name: percentual_negros + description: Percentual de negros sobre o total de bolsistas da unidade. + - name: meta_minima_mulheres_40 + description: Quantidade mínima de mulheres para atingir 40% do total da unidade. + - name: meta_minima_negros_30 + description: Quantidade mínima de negros para atingir 30% do total da unidade. + - name: mulheres_faltantes_meta + description: Quantidade de mulheres faltantes para atingir a meta mínima de 40%. + - name: negros_faltantes_meta + description: Quantidade de negros faltantes para atingir a meta mínima de 30%. + + - name: chamadas_publicas + description: > + Modelo silver com uma linha por chamada pública do Sisbolsas, integrando + dados da chamada, seleções, unidades, fontes de recurso e informações de + projeto do ipea_pro para apoiar relatórios e filtros por situação. + meta: + tags: + - silver + columns: + - name: chamada_id + description: Identificador da chamada pública (co_chamada_publica). + - name: processo_sei + description: Número do processo SEI vinculado à chamada. + - name: numero_chamada + description: Número/ano concatenado da chamada pública (nu_chamada_publica/nu_ano). + - name: ano_chamada + description: Ano da chamada pública, normalizado a partir de nu_ano. + - name: numero_chamada_ano + description: Número/ano concatenado da chamada pública. + - name: descricao_chamada + description: Descrição ou título da chamada pública. + - name: tipo_chamada + description: Tipo da chamada pública baseado no programa (ds_programa). + - name: programa_id + description: Identificador do programa da chamada pública. + - name: situacao_chamada + description: Situação textual da chamada pública. + - name: situacao_chamada_id + description: Código da situação da chamada pública. + - name: projeto_id + description: Identificador do projeto associado à chamada pública. + - name: projeto_titulo + description: Título do projeto no ipea_pro. + - name: projeto_numero + description: Número do projeto no ipea_pro. + - name: projeto_ano + description: Ano do projeto no ipea_pro. + - name: diretoria + description: Nome da diretoria do projeto. + - name: diretoria_sigla + description: Sigla da diretoria do projeto. + - name: coordenador + description: Coordenador consolidado (projeto ou chamada). + - name: coordenador_chamada + description: Coordenadores cadastrados na chamada pública. + - name: coordenador_projeto + description: Coordenador do projeto no ipea_pro. + - name: status_projeto + description: Status do projeto no ipea_pro. + - name: unidades_sigla + description: Siglas das unidades vinculadas à chamada pública. + - name: unidades_nome + description: Nomes das unidades vinculadas à chamada pública. + - name: ufs + description: UFs das unidades vinculadas à chamada pública. + - name: modalidades + description: Modalidades agregadas das seleções da chamada. + - name: total_selecoes + description: Quantidade de seleções vinculadas à chamada pública. + - name: prazo_meses + description: Maior duração (em meses) registrada nas seleções da chamada. + - name: inicio_pesquisa + description: Data de início da pesquisa informada na chamada pública. + - name: fim_pesquisa + description: Data de término da pesquisa informada na chamada pública. + - name: prazo_pesquisa_dias + description: Diferença em dias entre início e término da pesquisa. + - name: inicio_inscricao + description: Data de início da inscrição da chamada pública. + - name: fim_inscricao + description: Data de término da inscrição da chamada pública. + - name: inicio_julgamento + description: Data de início do julgamento da chamada pública. + - name: fim_julgamento + description: Data de término do julgamento da chamada pública. + - name: publicacao_dou + description: Data de publicação no DOU da chamada pública. + - name: previsao_resultado + description: Data de previsão do resultado da chamada pública. + - name: publicacao_resultado + description: Data de publicação do resultado da chamada pública. + - name: inicio_previsao_bolsa + description: Data de previsão de início das bolsas. + - name: inicio_bolsa + description: Data de início das bolsas da chamada pública. + - name: inicio_recurso + description: Data de início do período de recurso da chamada pública. + - name: fim_recurso + description: Data de término do período de recurso da chamada pública. + - name: data_criacao + description: Data de criação do registro da chamada pública. + - name: moeda + description: Moeda indicada para a chamada pública. + - name: valor_total_chamada + description: Valor total consolidado da chamada pública. + - name: valor_global_estimado + description: Valor global estimado registrado na chamada pública. + - name: valor_total_selecoes + description: Soma do valor total das seleções vinculadas à chamada. + - name: valor_total_fontes_projeto + description: Soma do valor total das fontes do projeto no ipea_pro. + - name: valor_por_ano + description: Mapa JSON com soma por ano das fontes do projeto. + - name: fonte_recurso + description: Fonte de recurso consolidada (Sisbolsas ou ipea_pro). + - name: fontes_recurso_chamada + description: Fontes financeiras associadas à chamada pública. + - name: fontes_recurso_projeto + description: Fontes financeiras associadas ao projeto no ipea_pro. + - name: cotas + description: Total de bolsas/vagas a partir de qt_bolsa das seleções. + - name: reservas + description: Classificados sem bolsa ativa (proxy para reservas). + - name: total_processos + description: Total de processos seletivos vinculados à chamada. + - name: total_classificados + description: Total de processos com in_classificacao verdadeiro. + - name: total_nao_aptos + description: Total de processos com in_nao_apto verdadeiro. + - name: total_bolsas_ativas + description: Total de processos com bolsa ativa. + - name: usuario_criacao + description: Usuário que criou a chamada pública. diff --git a/airflow_lappis/dags/dbt/ipea/models/sources.yml b/airflow_lappis/dags/dbt/ipea/models/sources.yml new file mode 100644 index 00000000..4af6b869 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/sources.yml @@ -0,0 +1,165 @@ +version: 2 + +sources: + - name: compras_gov + schema: compras_gov + tables: + - name: contratos + - name: faturas + - name: empenhos + - name: cronograma + - name: terceirizados + + - name: siafi + schema: siafi + tables: + - name: empenhos_tesouro + - name: estagios_tesouro + - name: nc_tesouro + - name: pf_tesouro + - name: visao_orcamentaria_total + + - name: transfere_gov + schema: transfere_gov + tables: + - name: notas_de_credito + - name: planos_acao + - name: programacao_financeira + - name: programas + - name: siape + schema: siape + tables: + - name: lista_uorgs + - name: lista_servidores + - name: dados_uorg + - name: dados_pessoais + - name: dados_pa + - name: dados_funcionais + - name: dados_financeiros + - name: dados_escolares + - name: dados_dependentes + - name: dados_curriculo + - name: dados_afastamento + - name: afastamento_historico + + - name: siorg + schema: siorg + tables: + - name: estrutura_organizacional_cargos + - name: cargos_funcao + - name: unidade_organizacional + + - name: transferegov_emendas + schema: transferegov_emendas + tables: + - name: programas_especiais + - name: planos_acao_especiais + - name: empenhos_especiais + - name: ordens_bancarias_especiais + - name: historico_pagamentos_especiais + - name: plano_trabalho_especial + - name: documentos_habeis_especiais + - name: relatorio_gestao_especial + - name: relatorios_gestao_novo_especial + - name: executor_especial + - name: metas_especiais + - name: finalidades_especiais + + - name: dados_abertos + schema: camara_deputados + tables: + - name: deputados + + - name: senado_federal + schema: senado_federal + tables: + - name: senadores + + - name: ipea_pro + schema: ipea_pro + tables: + - name: bolsistas + - name: coordenacoes + - name: custosemprojetos + - name: diretorias + - name: fontesreceitas + - name: grupoentidade + - name: insumofinanceiro + - name: itemfontereceitas + - name: itenscustos + - name: projetos + - name: registrofinanceiroemprojetos + - name: servidorespublicos + - name: statusprojetos + + - name: sisbolsas + schema: sisbolsas + tables: + - name: tb_area_formacao + - name: tb_bolsa + - name: tb_bolsa_coordenador + - name: tb_bolsa_fonterecurso + - name: tb_bolsista + - name: tb_chamada_publica + - name: tb_chapubli_fontfina + - name: tb_chapubli_unidade + - name: tb_coordenador + - name: tb_dado_formacao + - name: tb_dado_pessoal + - name: tb_dado_profissional + - name: tb_estado + - name: tb_folha_bolsista + - name: tb_folha_pagamento + - name: tb_fonte_financeira + - name: tb_modalidade + - name: tb_nivel_escolaridade + - name: tb_processo_seletivo + - name: tb_programa + - name: tb_selecao + - name: tb_situacao_chamada + - name: tb_situacao_concessao + - name: tb_unidade + - name: tb_usuario + + - name: sgac + schema: sgac + tables: + - name: projetos_sgac + + - name: censo_demografico + schema: censo_demografico + tables: + - name: mulheres_tabela_1_tabela_base_do_sidra_10056 + - name: mulheres_tabela_2_tabela_base_do_sidra_10061_parte_1 + - name: mulheres_tabela_2_tabela_base_do_sidra_10061_parte_2 + - name: mulheres_tabela_3_tabela_base_do_sidra_10063_parte_1 + - name: mulheres_tabela_3_tabela_base_do_sidra_10063_parte_2 + - name: mulheres_tabela_3_tabela_base_do_sidra_10063_parte_3 + - name: mulheres_tabela_4_tabela_base_do_sidra_10141_parte_1 + - name: mulheres_tabela_4_tabela_base_do_sidra_10141_parte_2 + - name: mulheres_tabela_5_tabela_base_do_sidra_10253_parte_1 + - name: mulheres_tabela_5_tabela_base_do_sidra_10253_parte_2 + - name: mulheres_tabela_6_tabela_base_do_sidra_10283_parte_1 + - name: mulheres_tabela_6_tabela_base_do_sidra_10283_parte_2 + - name: mulheres_tabela_7_tabela_base_do_sidra_10282_parte_1 + - name: mulheres_tabela_7_tabela_base_do_sidra_10282_parte_2 + - name: mulheres_tabela_8_tabela_base_do_sidra_10329_parte_1 + - name: mulheres_tabela_8_tabela_base_do_sidra_10329_parte_2 + - name: mulheres_tabela_9_tabela_base_do_sidra_10331_parte_1 + - name: mulheres_tabela_9_tabela_base_do_sidra_10331_parte_2 + - name: mulheres_tabela_10_tabela_sidra_10077_e_10078 + - name: mulheres_tabela_11_tabela_base_do_sidra_9882_parte_1 + - name: mulheres_tabela_11_tabela_base_do_sidra_9882_parte_2 + - name: mulheres_tabela_12_br_gr_uf_mu + - name: mulheres_tabela_13_br_gr_uf_mu + - name: mulheres_tabela_14_br_gr_uf_mu + - name: mulheres_tabela_15_br_gr_uf_mu + - name: mulheres_tabela_16_br_gr_uf_mu + - name: mulheres_tabela_17_br_gr_uf_mu + # Quilombolas — alfabetização e características dos domicílios (prefixo Q_A_C_D_) + - name: Q_A_C_D_apendice_01 + - name: Q_A_C_D_indice_tabelas_de_resultados + - name: Q_A_C_D_indice_tabelas_selecionadas + - name: Q_A_C_D_tabela_de_resultado_01 + - name: Q_A_C_D_tabela_de_resultado_02 + - name: Q_A_C_D_tabela_complementar_01 diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/bronze/nc_tesouro.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/bronze/nc_tesouro.sql new file mode 100644 index 00000000..a12edbbf --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/bronze/nc_tesouro.sql @@ -0,0 +1,31 @@ +with + + notas_credito as ( + select + {{ target.schema }}.parse_date(emissao_mes) as emissao_mes, + to_date(emissao_dia, 'DD/MM/YYYY') as emissao_dia, + nc, + nc_transferencia, + nc_fonte_recursos, + nc_fonte_recursos_descricao, + ptres, + nc_evento, + nc_evento_descricao as nc_evento_descr, + ug_responsavel, + ug_responsavel_descricao, + natureza_despesa as nc_natureza_despesa, + natureza_despesa_detalhada as nc_natureza_despesa_descricao, + plano_interno, + plano_detalhado_descricao1, + plano_detalhado_descricao2, + favorecido_doc, + favorecido_doc_descricao, + replace(nc_valor_linha, ',', '.')::numeric(15, 2) as nc_valor_linha, + {{ parse_financial_value("movimento_liquido") }} as movimento_liquido, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("siafi", "nc_tesouro") }} + ) + +-- +select * +from notas_credito diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/bronze/pf_tesouro.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/bronze/pf_tesouro.sql new file mode 100644 index 00000000..c9b0d593 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/bronze/pf_tesouro.sql @@ -0,0 +1,32 @@ +with + + programacoes_financeira as ( + select + {{ target.schema }}.parse_date(emissao_mes) as emissao_mes, + to_date(emissao_dia, 'DD/MM/YYYY') as emissao_dia, + ug_emitente, + ug_emitente_descricao, + ug_favorecido, + ug_favorecido_descricao, + pf_evento, + pf_evento_descricao, + right(pf, 12) as pf, + pf_inscricao, + pf_acao, + pf_acao_descricao, + pf_fonte_recursos, + pf_fonte_recursos_descricao, + pf_vinculacao_pagamento, + pf_vinculacao_pagamento_descricao, + pf_categoria_gasto, + pf_recurso, + pf_recurso_descricao, + doc_observacao, + replace(pf_valor_linha, ',', '.')::numeric(15, 2) as pf_valor_linha, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("siafi", "pf_tesouro") }} + ) + +-- +select * +from programacoes_financeira diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/bronze/planos_acao.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/bronze/planos_acao.sql new file mode 100644 index 00000000..5ffb801a --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/bronze/planos_acao.sql @@ -0,0 +1,36 @@ +with + + planos_acao as ( + select + id_plano_acao, + id_programa, + sigla_unidade_descentralizada, + case + when sigla_unidade_descentralizada = 'IPEA' + then 'beneficiario' + else 'emitente' + end as ted_beneficiario_emitente, + unidade_descentralizada, + sigla_unidade_responsavel_execucao, + unidade_responsavel_execucao, + vl_total_plano_acao::numeric(15, 2) as vl_total_plano_acao, + to_date(dt_inicio_vigencia, 'YYYY-mm-dd') as dt_inicio_vigencia, + to_date(dt_fim_vigencia, 'YYYY-mm-dd') as dt_fim_vigencia, + tx_objeto_plano_acao, + tx_justificativa_plano_acao, + in_forma_execucao_direta, + in_forma_execucao_particulares, + in_forma_execucao_descentralizada, + tx_situacao_plano_acao, + aa_ano_plano_acao, + vl_beneficiario_especifico::numeric(15, 2) as vl_beneficiario_especifico, + vl_chamamento_publico::numeric(15, 2) as vl_chamamento_publico, + sq_instrumento, + aa_instrumento, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transfere_gov", "planos_acao") }} + ) + +-- +select * +from planos_acao diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/bronze/schema.yml b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/bronze/schema.yml new file mode 100644 index 00000000..2e2bfc38 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/bronze/schema.yml @@ -0,0 +1,234 @@ +version: 2 + +models: + + # TED DBT + - name: pf_tesouro + description: > + Esta tabela contém registros de Programações Financeiras extraídas do SIAFI, detalhando informações como tipo de programação, data de execução e valor. + A tabela é atualizada diariamente e reflete as autorizações de movimentação financeira no âmbito da execução orçamentária federal. + meta: + tags: + - bronze + columns: + - name: emissao_mes + description: > + Mês de emissão da Programação Financeira. + - name: emissao_dia + description: > + Dia de emissão da Programação Financeira. + - name: ug_emitente + description: > + Código da Unidade Gestora responsável pela emissão da Programação Financeira. + - name: ug_emitente_descricao + description: > + Nome ou descrição da Unidade Gestora emitente da Programação Financeira. + - name: ug_favorecido + description: > + Código da Unidade Gestora favorecida pela Programação Financeira. + - name: ug_favorecido_descricao + description: > + Nome ou descrição da Unidade Gestora favorecida pela Programação Financeira. + - name: pf_evento + description: > + Código do evento contábil associado à Programação Financeira, representando a natureza da transação. + - name: pf_evento_descricao + description: > + Descrição do evento contábil vinculado à Programação Financeira. + - name: pf + description: > + Número identificador único da Programação Financeira. + - name: pf_inscricao + description: > + Número de inscrição da Programação Financeira, utilizado para controle e acompanhamento. + - name: pf_acao + description: > + Código da ação orçamentária associada à Programação Financeira. + - name: pf_acao_descricao + description: > + Descrição da ação orçamentária vinculada à Programação Financeira. + - name: pf_fonte_recursos + description: > + Código da fonte de recursos associada à Programação Financeira, indicando a origem dos recursos utilizados. + - name: pf_fonte_recursos_descricao + description: > + Descrição textual da fonte de recursos vinculada à Programação Financeira. + - name: pf_vinculacao_pagamento + description: > + Código da vinculação de pagamento associada à Programação Financeira. + - name: pf_vinculacao_pagamento_descricao + description: > + Descrição da vinculação de pagamento vinculada à Programação Financeira. + - name: pf_categoria_gasto + description: > + Código da categoria de gasto associada à Programação Financeira, conforme classificação orçamentária. + - name: pf_recurso + description: > + Código do recurso associado à Programação Financeira. + - name: pf_recurso_descricao + description: > + Descrição do recurso vinculado à Programação Financeira. + - name: doc_observacao + description: > + Observações adicionais relacionadas à Programação Financeira. + - name: pf_valor_linha + description: > + Valor monetário individual da linha da Programação Financeira. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + data_tests: + - verificacao_tipagem: + nome_tabela: 'ted.pf_tesouro' + nome_coluna: 'emissao_dia' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'ted.pf_tesouro' + nome_coluna: 'pf_valor_linha' + tipo_esperado: 'numeric' + + - name: nc_tesouro + description: > + Esta tabela registra informações detalhadas sobre as Notas de Crédito emitidas no âmbito da execução orçamentária e financeira do governo federal. + As Notas de Crédito representam autorizações para a realização de despesas, sendo fundamentais para o controle e acompanhamento da execução orçamentária. + meta: + tags: + - bronze + columns: + - name: emissao_mes + description: > + Mês de emissão da Nota de Crédito. + - name: emissao_dia + description: > + Dia de emissão da Nota de Crédito. + - name: nc + description: > + Número identificador único da Nota de Crédito. + - name: nc_transferencia + description: > + Indicador de que a Nota de Crédito refere-se a uma transferência de recursos entre unidades gestoras. + - name: nc_fonte_recursos + description: > + Código da fonte de recursos associada à Nota de Crédito, indicando a origem dos recursos utilizados. + - name: nc_fonte_recursos_descricao + description: > + Descrição textual da fonte de recursos vinculada à Nota de Crédito. + - name: ptres + description: > + Código do Plano Interno de Trabalho (PTRES) relacionado à Nota de Crédito, utilizado para detalhar a alocação dos recursos. + - name: nc_evento + description: > + Código do evento contábil associado à Nota de Crédito, representando a natureza da transação. + - name: nc_evento_descricao + description: > + Descrição do evento contábil vinculado à Nota de Crédito. + - name: ug_responsavel + description: > + Código da Unidade Gestora responsável pela emissão da Nota de Crédito. + - name: ug_responsavel_descricao + description: > + Nome ou descrição da Unidade Gestora responsável pela emissão da Nota de Crédito. + - name: natureza_despesa + description: > + Código da natureza da despesa associada à Nota de Crédito, conforme classificação orçamentária. + - name: natureza_despesa_detalhada + description: > + Descrição detalhada da natureza da despesa vinculada à Nota de Crédito. + - name: plano_interno + description: > + Código do plano interno relacionado à Nota de Crédito, utilizado para controle interno da execução orçamentária. + - name: plano_detalhado_descricao1 + description: > + Primeira descrição detalhada do plano interno associado à Nota de Crédito. + - name: plano_detalhado_descricao2 + description: > + Segunda descrição detalhada do plano interno associado à Nota de Crédito. + - name: favorecido_doc + description: > + Documento de identificação (CPF ou CNPJ) do favorecido pela Nota de Crédito. + - name: favorecido_doc_descricao + description: > + Nome ou razão social do favorecido identificado na Nota de Crédito. + - name: nc_valor_linha + description: > + Valor monetário individual da linha da Nota de Crédito. + - name: movimento_liquido + description: > + Valor líquido do movimento financeiro associado à Nota de Crédito, após deduções ou acréscimos aplicáveis. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + + - name: planos_acao + description: > + Esta tabela armazena informações detalhadas sobre os Planos de Ação vinculados a programas públicos. + Ela consolida dados sobre as unidades envolvidas na execução, prazos de vigência, valores, justificativas, + formas de execução e instrumentos utilizados. Serve como base para o acompanhamento, análise e auditoria + da implementação de ações públicas em diferentes formatos de execução. + meta: + tags: + - bronze + columns: + - name: id_plano_acao + description: > + Identificador único do Plano de Ação. + - name: id_programa + description: > + Identificador único do programa ao qual o Plano de Ação está vinculado. + - name: sigla_unidade_descentralizada + description: > + Sigla da unidade descentralizada responsável pelo Plano de Ação. + - name: unidade_descentralizada + description: > + Nome completo da unidade descentralizada responsável pelo Plano de Ação. + - name: sigla_unidade_responsavel_execucao + description: > + Sigla da unidade responsável pela execução do Plano de Ação. + - name: unidade_responsavel_execucao + description: > + Nome completo da unidade responsável pela execução do Plano de Ação. + - name: vl_total_plano_acao + description: > + Valor total previsto para execução do Plano de Ação. + - name: dt_inicio_vigencia + description: > + Data de início da vigência do Plano de Ação. + - name: dt_fim_vigencia + description: > + Data final da vigência do Plano de Ação. + - name: tx_objeto_plano_acao + description: > + Descrição do objeto do Plano de Ação, informando seu propósito e escopo. + - name: tx_justificativa_plano_acao + description: > + Justificativa para a elaboração e execução do Plano de Ação. + - name: in_forma_execucao_direta + description: > + Indicador booleano que sinaliza se a execução do plano ocorrerá de forma direta. + - name: in_forma_execucao_particulares + description: > + Indicador booleano que sinaliza se a execução contará com a participação de particulares. + - name: in_forma_execucao_descentralizada + description: > + Indicador booleano que sinaliza se a execução será realizada de forma descentralizada. + - name: tx_situacao_plano_acao + description: > + Situação atual do Plano de Ação ( em elaboração, aprovado, em execução, concluído). + - name: aa_ano_plano_acao + description: > + Ano de referência do Plano de Ação. + - name: vl_beneficiario_especifico + description: > + Valor destinado especificamente a beneficiários definidos no Plano de Ação. + - name: vl_chamamento_publico + description: > + Valor previsto para execução por meio de chamamento público. + - name: sq_instrumento + description: > + Código sequencial do instrumento jurídico ou administrativo associado ao Plano de Ação. + - name: aa_instrumento + description: > + Ano de referência do instrumento associado ao Plano de Ação. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_execucao_financeira_teds_.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_execucao_financeira_teds_.sql new file mode 100644 index 00000000..96006e4f --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_execucao_financeira_teds_.sql @@ -0,0 +1,64 @@ +-- Transformando o resumo orçamentário em categorias para utilizar no gráfico de barras empilhadas +select + plano_acao, + num_transf, + '1.1 Orçamento recebido' as tipo, + (orcamento_recebido - orcamento_devolvido) as valor, + '1. Orçamento recebido' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all +--------------------------------------------------------------------------------------------- +select + plano_acao, + num_transf, + '2.1 Financeiro recebido' as tipo, + (financeiro_recebido - (financeiro_devolvido + financeiro_cancelado)) as valor, + '2. Visão geral repasses financeiros' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +select + plano_acao, + num_transf, + '2.2 Financeiro a receber (em relação ao orçamento)' as tipo, + (orcamento_recebido - orcamento_devolvido - (financeiro_recebido - (financeiro_devolvido + financeiro_cancelado))) as valor, + '2. Visão geral repasses financeiros' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +select + plano_acao, + num_transf, + '3.1 Despesas pagas no exercício' as tipo, + despesas_pagas_exercicio as valor, + '3. Detalhe da execução financeira' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +select + plano_acao, + num_transf, + '3.2 Despesas pagas RAP' as tipo, + despesas_pagas_rap as valor, + '3. Detalhe da execução financeira' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +select + plano_acao, + num_transf, + '3.3 Saldo financeiro' as tipo, + (financeiro_recebido - (financeiro_devolvido + financeiro_cancelado + despesas_pagas_exercicio + despesas_pagas_rap)) as valor, + '3. Detalhe da execução financeira' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_execucao_orcamentaria_teds_.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_execucao_orcamentaria_teds_.sql new file mode 100644 index 00000000..57365577 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_execucao_orcamentaria_teds_.sql @@ -0,0 +1,79 @@ +-- Transformando o resumo orçamentário em categorias para utilizar no gráfico de barras empilhadas +select + plano_acao, + num_transf, + '1.1 Destaque total recebido' as tipo, + (orcamento_recebido - orcamento_devolvido) as valor, + '1. Destaque orçamentário' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +select + plano_acao, + num_transf, + '1.2 Destaque a receber' as tipo, + (valor_firmado - orcamento_recebido + orcamento_devolvido) as valor, + '1. Destaque orçamentário' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +--------------------------------------------------------------------------------------------- + +union all + +select + plano_acao, + num_transf, + '2.1 Empenhado (total-anulado)' as tipo, + (empenhado - empenho_anulado) as valor, + '2. Visão geral da exec. orçamentária' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +select + plano_acao, + num_transf, + '2.2 A empenhar' as tipo, + (orcamento_recebido - orcamento_devolvido) - (empenhado - empenho_anulado) as valor, + '2. Visão geral da exec. orçamentária' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +--------------------------------------------------------------------------------------------- + +union all + +select + plano_acao, + num_transf, + '3.1 Despesas pagas no exercício' as tipo, + despesas_pagas_exercicio as valor, + '3. Detalhe da exec. orçamentária' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +select + plano_acao, + num_transf, + '3.2 Despesas pagas (RAP)' as tipo, + despesas_pagas_rap as valor, + '3. Detalhe da exec. orçamentária' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +select + plano_acao, + num_transf, + '3.3 Saldo empenho' as tipo, + (empenhado - empenho_anulado) - (despesas_pagas_exercicio + despesas_pagas_rap) as valor, + '3. Detalhe da exec. orçamentária' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_execucao_rap_teds_.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_execucao_rap_teds_.sql new file mode 100644 index 00000000..60b26ba3 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_execucao_rap_teds_.sql @@ -0,0 +1,31 @@ +-- Transformando o resumo orçamentário em categorias para utilizar no gráfico de barras empilhadas +select + plano_acao, + num_transf, + '1.1 Restos a pagar inscritos' as tipo, + restos_a_pagar as valor, + '1. Restos a pagar inscritos' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +select + plano_acao, + num_transf, + '2.1 Despesas pagas RAP' as tipo, + despesas_pagas_rap as valor, + '2. Detalhe da exec. orçamentária - Restos a Pagar' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +select + plano_acao, + num_transf, + '2.2 Saldo RAP' as tipo, + restos_a_pagar - despesas_pagas_rap as valor, + '2. Detalhe da exec. orçamentária - Restos a Pagar' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_resumo_orcamentario_teds_.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_resumo_orcamentario_teds_.sql new file mode 100644 index 00000000..88f16310 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_resumo_orcamentario_teds_.sql @@ -0,0 +1,59 @@ +with + + categorias_emitente as ( + select + plano_acao, + num_transf, + case + when tipo = '2.1 Destaque total enviado' + then '2.1 Destaque total recebido' + when tipo = '2.2 Destaque a enviar' + then '2.2 Destaque a receber' + when tipo = '3.1 Financeiro enviado' + then '4.1 Financeiro recebido' + when tipo = '3.2 Financeiro a enviar (em relação ao orçamento)' + then '4.2 Financeiro a receber (em relação ao orçamento)' + else tipo + end as tipo, + valor, + case + when categoria = '3. Repasse financeiro' + then '4. Repasse financeiro' + else categoria + end as categoria, + dt_ingest + from {{ ref("categorias_resumo_orcamentario_teds_emitente_") }} + ), + + execucao_orcamentaria as ( + select + plano_acao, + num_transf, + '3.1 Total empenhado' as tipo, + (empenhado - empenho_anulado) as valor, + '3. Execução orçamentária' as categoria, + dt_ingest + from {{ ref("ted_resumo_orcamentario") }} + + union all + + select + plano_acao, + num_transf, + '3.2 A empenhar' as tipo, + ( + (orcamento_recebido - orcamento_devolvido) + - (empenhado - empenho_anulado) + ) as valor, + '3. Execução orçamentária' as categoria, + dt_ingest + from {{ ref("ted_resumo_orcamentario") }} + ) + +select * +from categorias_emitente + +union all + +select * +from execucao_orcamentaria diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_resumo_orcamentario_teds_emitente_.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_resumo_orcamentario_teds_emitente_.sql new file mode 100644 index 00000000..5bfecc37 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/categorias_resumo_orcamentario_teds_emitente_.sql @@ -0,0 +1,56 @@ +-- Transformando o resumo orçamentário em categorias para utilizar no gráfico de barras empilhadas +select + plano_acao, + num_transf, + '1.1 Valor firmado' as tipo, + valor_firmado as valor, + '1. Valor firmado' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +--------------------------------------------------------------------------------------------- +select + plano_acao, + num_transf, + '2.1 Destaque total enviado' as tipo, + (orcamento_recebido - orcamento_devolvido) as valor, + '2. Destaque orçamentário' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +select + plano_acao, + num_transf, + '2.2 Destaque a enviar' as tipo, + (valor_firmado - (orcamento_recebido - orcamento_devolvido)) as valor, + '2. Destaque orçamentário' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +--------------------------------------------------------------------------------------------- + +union all + +select + plano_acao, + num_transf, + '3.1 Financeiro enviado' as tipo, + (financeiro_recebido - (financeiro_devolvido + financeiro_cancelado)) as valor, + '3. Repasse financeiro' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} + +union all + +select + plano_acao, + num_transf, + '3.2 Financeiro a enviar (em relação ao orçamento)' as tipo, + (orcamento_recebido - orcamento_devolvido - (financeiro_recebido - (financeiro_devolvido + financeiro_cancelado))) as valor, + '3. Repasse financeiro' as categoria, + dt_ingest +from {{ ref('ted_resumo_orcamentario') }} diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/resumo_programa_plano_acao_.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/resumo_programa_plano_acao_.sql new file mode 100644 index 00000000..f0c62bf4 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/resumo_programa_plano_acao_.sql @@ -0,0 +1,75 @@ +WITH percent_vigencia AS ( + select + planos_acao.id_plano_acao, + planos_acao.tx_objeto_plano_acao as objeto_plano_acao, + planos_acao.dt_inicio_vigencia, + planos_acao.dt_fim_vigencia, + CASE + WHEN planos_acao.dt_fim_vigencia = planos_acao.dt_inicio_vigencia THEN 100 + WHEN CURRENT_DATE < planos_acao.dt_inicio_vigencia THEN 0 + WHEN CURRENT_DATE >= planos_acao.dt_fim_vigencia THEN 1 + ELSE (ROUND( + (CURRENT_DATE - planos_acao.dt_inicio_vigencia)::numeric / + NULLIF((planos_acao.dt_fim_vigencia - planos_acao.dt_inicio_vigencia), 0) * 100, + 2) / 100 + ) + END AS percentual_conclusao, + programas.id_programa as programa, + programas.sigla_unidade_descentralizadora, + programas.sigla_unidade_responsavel_acompanhamento, + programas.tx_nome_institucional_programa as nome_institucional_programa, + planos_acao.dt_ingest as dt_ingest_pa + from {{ ref('planos_acao') }} as planos_acao + inner join {{ source('transfere_gov', 'programas') }} as programas + on planos_acao.id_programa = programas.id_programa +) + +select + ro.plano_acao, + ro.num_transf, + ro.sigla_unidade_descentralizada, + ro.ted_beneficiario_emitente, + ro.valor_firmado, + ro.orcamento_recebido, + ro.orcamento_devolvido, + ro.empenhado, + ro.empenho_anulado, + ro.despesas_pagas_exercicio, + ro.despesas_pagas_rap, + ro.restos_a_pagar, + ro.despesas_liquidada, + ro.financeiro_recebido, + ro.financeiro_devolvido, + ro.financeiro_cancelado, + pv.objeto_plano_acao, + pv.dt_inicio_vigencia, + pv.dt_fim_vigencia, + pv.percentual_conclusao, + pv.programa, + pv.sigla_unidade_descentralizadora as sigla_unidade_descentralizadora_programa, + pv.sigla_unidade_responsavel_acompanhamento, + pv.nome_institucional_programa, + CASE + WHEN ro.ted_beneficiario_emitente = 'emitente' THEN + CASE + WHEN ro.financeiro_recebido >= ro.valor_firmado THEN 1 + WHEN ro.financeiro_recebido = 0 THEN 0 + ELSE (ROUND( + (ro.financeiro_recebido / NULLIF(ro.valor_firmado, 0)) * 100, + 2) / 100 + ) + END + ELSE + CASE + WHEN ro.despesas_pagas_exercicio + ro.despesas_pagas_rap >= ro.valor_firmado THEN 1 + WHEN ro.despesas_pagas_exercicio + ro.despesas_pagas_rap = 0 THEN 0 + ELSE (ROUND( + ((ro.despesas_pagas_exercicio + ro.despesas_pagas_rap) / NULLIF(ro.valor_firmado, 0)) * 100, + 2) / 100 + ) + END + END AS percentual_conclusao_orcamentaria, + greatest(ro.dt_ingest, pv.dt_ingest_pa) as dt_ingest +from {{ ref('ted_resumo_orcamentario') }} as ro +full join percent_vigencia as pv + on ro.plano_acao = pv.id_plano_acao diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/schema.yml b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/schema.yml new file mode 100644 index 00000000..5e7aead9 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/schema.yml @@ -0,0 +1,241 @@ +version: 2 + +models: + + - name: ted_resumo_orcamentario + description: > + Tabela de consolidação orçamentária e financeira por Plano de Ação, baseada em informações de valores firmados, orçamentos recebidos e devolvidos, empenhos, despesas e transferências financeiras. + Os dados são integrados de diferentes fontes (notas de crédito, empenhos e programações financeiras) para fornecer um resumo abrangente da execução orçamentária e financeira de transferências intergovernamentais. + meta: + tags: + - gold + columns: + - name: plano_acao + description: > + Identificador único do Plano de Ação associado aos registros orçamentários e financeiros. + - name: num_transf + description: > + Número da transferência utilizado como chave de ligação entre diferentes fontes de dados. + - name: sigla_unidade_descentralizada + description: > + Sigla da Unidade Descentralizada responsável pelo Plano de Ação. + - name: ted_beneficiario_emitente + description: > + Identifica se a Unidade do Plano de Ação é a beneficiária ou a emitente dos recursos da TED (Transferência Voluntária). Valores possíveis: 'beneficiario', 'emitente' ou 'nao_indicado'. + - name: valor_firmado + description: > + Valor total originalmente acordado no Plano de Ação como meta de transferência de recursos. + - name: orcamento_recebido + description: > + Total de créditos orçamentários efetivamente recebidos por meio de Notas de Crédito. + - name: orcamento_devolvido + description: > + Total de créditos orçamentários devolvidos, identificado por eventos contábeis específicos (ex.: 300301, 300307). + - name: empenhado + description: > + Soma dos valores empenhados, ou seja, das despesas formalizadas no orçamento, excluindo anulações. + - name: empenho_anulado + description: > + Total de valores de empenhos anulados, ou seja, revertidos após sua emissão. + - name: despesas_pagas_exercicio + description: > + Total de despesas pagas no exercício corrente. + - name: despesas_pagas_rap + description: > + Total de despesas pagas com recursos de Restos a Pagar. + - name: restos_a_pagar + description: > + Valor total inscrito como Restos a Pagar, ou seja, despesas empenhadas e não pagas até o fim do exercício. + - name: despesas_liquidada + description: > + Soma das despesas liquidadas, indicando bens ou serviços efetivamente entregues. + - name: financeiro_recebido + description: > + Total de recursos financeiros recebidos via Programação Financeira (TED), considerando apenas ações classificadas como 'TRANSFERENCIA'. + - name: financeiro_devolvido + description: > + Total de recursos financeiros devolvidos, com ações classificadas como 'DEVOLUCAO'. + - name: financeiro_cancelado + description: > + Valor total de cancelamentos financeiros identificados na programação, com ação 'CANCELAMENTO'. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + + - name: categorias_resumo_orcamentario_ted + description: > + Tabela que transforma o resumo orçamentário em categorias (Valor firmado, Destaque orçamentário, Execução orçamentária e Repasse financeiro) para utilização em gráficos de barras empilhadas. + Os dados são derivados do modelo `ted_resumo_orcamentario`. + meta: + tags: + - gold + columns: + - name: plano_acao + description: Identificador do Plano de Ação. + - name: num_transf + description: Número da transferência. + - name: tipo + description: > + Subcategoria detalhada do valor (ex: '1.1 Valor firmado', '2.1 Destaque total recebido'). + - name: valor + description: Valor numérico associado ao tipo. + - name: categoria + description: > + Categoria principal para agrupamento no gráfico (ex: '1. Valor firmado', '2. Destaque orçamentário'). + - name: dt_ingest + description: Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: categorias_resumo_orcamentario_ted_emitente + description: > + Tabela que transforma o resumo orçamentário em categorias (Valor firmado, Destaque orçamentário e Repasse financeiro) + sob a perspectiva do emitente, para utilização em gráficos de barras empilhadas. + Os dados são derivados do modelo `ted_resumo_orcamentario`. + meta: + tags: + - gold + columns: + - name: plano_acao + description: Identificador do Plano de Ação. + - name: num_transf + description: Número da transferência. + - name: tipo + description: > + Subcategoria detalhada do valor (ex: '1.1 Valor firmado', '2.1 Destaque total enviado'). + - name: valor + description: Valor numérico associado ao tipo. + - name: categoria + description: > + Categoria principal para agrupamento no gráfico (ex: '1. Valor firmado', '2. Destaque orçamentário', '3. Repasse financeiro'). + - name: dt_ingest + description: Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: categorias_execucao_financeira_ted + description: > + Tabela que transforma o resumo orçamentário em categorias de execução financeira para utilização em gráficos de barras empilhadas. + Os dados são derivados do modelo `ted_resumo_orcamentario`. + meta: + tags: + - gold + columns: + - name: plano_acao + description: Identificador do Plano de Ação. + - name: num_transf + description: Número da transferência. + - name: tipo + description: > + Subcategoria detalhada do valor (ex: '2.1 Financeiro recebido', '3.3 Saldo financeiro'). + - name: valor + description: Valor numérico associado ao tipo. + - name: categoria + description: > + Categoria principal para agrupamento no gráfico (ex: '2. Visão geral repasses financeiros', '3. Detalhe da execução financeira'). + - name: dt_ingest + description: Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: categorias_execucao_orcamentaria_ted + description: > + Tabela que transforma o resumo orçamentário em categorias de execução orçamentária para utilização em gráficos de barras empilhadas. + Os dados são derivados do modelo `ted_resumo_orcamentario`. + meta: + tags: + - gold + columns: + - name: plano_acao + description: Identificador do Plano de Ação. + - name: num_transf + description: Número da transferência. + - name: tipo + description: > + Subcategoria detalhada do valor (ex: '2.1 Empenhado (total-anulado)', '3.3 Saldo empenho'). + - name: valor + description: Valor numérico associado ao tipo. + - name: categoria + description: > + Categoria principal para agrupamento no gráfico (ex: '2. Visão geral da exec. orçamentária', '3. Detalhe da exec. orçamentária'). + - name: dt_ingest + description: Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: categorias_execucao_rap_ted + description: > + Tabela que transforma o resumo orçamentário em categorias de execução de Restos a Pagar (RAP) para utilização em gráficos de barras empilhadas. + Os dados são derivados do modelo `ted_resumo_orcamentario`. + meta: + tags: + - gold + columns: + - name: plano_acao + description: Identificador do Plano de Ação. + - name: num_transf + description: Número da transferência. + - name: tipo + description: > + Subcategoria detalhada do valor (ex: '1.1 Restos a pagar inscritos', '2.2 Saldo RAP'). + - name: valor + description: Valor numérico associado ao tipo. + - name: categoria + description: > + Categoria principal para agrupamento no gráfico (ex: '1. Restos a pagar inscritos', '2. Detalhe da exec. orçamentária - Restos a Pagar'). + - name: dt_ingest + description: Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: resumo_do_programa_plano_acao + description: > + Tabela consolidada que une o resumo orçamentário (TED) com informações de vigência e programas (Transfere.Gov). + Calcula percentuais de conclusão baseados em datas (vigência) e em valores financeiros/orçamentários. + meta: + tags: + - gold + columns: + - name: plano_acao + description: Identificador do Plano de Ação. + - name: num_transf + description: Número da transferência. + - name: sigla_unidade_descentralizada + description: Sigla da unidade descentralizada. + - name: ted_beneficiario_emitente + description: Identifica se a unidade é beneficiária ou emitente. + - name: valor_firmado + description: Valor total pactuado no Plano de Ação. + - name: orcamento_recebido + description: Valor total do orçamento recebido. + - name: orcamento_devolvido + description: Valor total do orçamento devolvido. + - name: empenhado + description: Valor total empenhado. + - name: empenho_anulado + description: Valor total de empenhos anulados. + - name: despesas_pagas_exercicio + description: Valor das despesas pagas no exercício. + - name: despesas_pagas_rap + description: Valor das despesas pagas em Restos a Pagar. + - name: restos_a_pagar + description: Valor inscrito em Restos a Pagar. + - name: despesas_liquidada + description: Valor das despesas liquidadas. + - name: financeiro_recebido + description: Valor financeiro recebido. + - name: financeiro_devolvido + description: Valor financeiro devolvido. + - name: financeiro_cancelado + description: Valor financeiro cancelado. + - name: dt_ingest + description: Data de ingestão dos dados. + - name: objeto_plano_acao + description: Descrição do objeto do Plano de Ação. + - name: dt_inicio_vigencia + description: Data de início da vigência. + - name: dt_fim_vigencia + description: Data de fim da vigência. + - name: percentual_conclusao + description: Percentual de conclusão baseado na duração da vigência (data atual vs início/fim). + - name: programa + description: Identificador do programa. + - name: sigla_unidade_descentralizada_programa + description: Sigla da unidade descentralizada do programa. + - name: sigla_unidade_responsavel_acompanhamento + description: Sigla da unidade responsável pelo acompanhamento. + - name: nome_institucional_programa + description: Nome institucional do programa. + - name: percentual_conclusao_orcamentaria + description: Percentual de execução financeira ou orçamentária em relação ao valor firmado. diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/ted_resumo_orcamentario.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/ted_resumo_orcamentario.sql new file mode 100644 index 00000000..afab4b90 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/gold/ted_resumo_orcamentario.sql @@ -0,0 +1,116 @@ +-- Armazenando apenas os valores independentes das tabelas +-- valores calculados serão computados no dashboard +with + + -- Valor firmado + valor_firmado_tb as ( + select + id_plano_acao as plano_acao, + vl_total_plano_acao as valor_firmado, + sigla_unidade_descentralizada, + ted_beneficiario_emitente, + dt_ingest as dt_ingest_vf + from {{ ref("planos_acao") }} + ), + + -- Orçamento recebido + -- Orçamento devolvido + valores_orcamentos_tb as ( + select + plano_acao, + num_transf, + sum( + case when nc_evento not in ('300301', '300307') then nc_valor else 0 end + ) as orcamento_recebido, + sum( + case when nc_evento in ('300301', '300307') then nc_valor else 0 end + ) as orcamento_devolvido, + max(dt_ingest) as dt_ingest_vo + from {{ ref("nc_plano_acao") }} + where ptres not in ('-9') + group by plano_acao, num_transf + ), + -- Destaque orçamentario = Orçamento recebido - Orçamento devolvido + -- Destaque a receber = Valor firmado - Destaque orçamentario + -- Empenhado + -- Empenho anulado + -- Utilizado/pago + valores_empenhados_tb as ( + select + plano_acao, + num_transf, + sum( + case when despesas_empenhadas > 0 then despesas_empenhadas else 0 end + ) as empenhado, + sum( + case when despesas_empenhadas < 0 then - despesas_empenhadas else 0 end + ) as empenho_anulado, + sum(despesas_pagas) as despesas_pagas_exercicio, + sum(restos_a_pagar_pagos) as despesas_pagas_rap, + sum(restos_a_pagar_inscritos) as restos_a_pagar, + sum(despesas_liquidadas) as despesas_liquidada, + max(dt_ingest) as dt_ingest_ve + from {{ ref("empenhos_plano_acao") }} + group by plano_acao, num_transf + ), + + -- Saldo empenho = Empenhado - Empenho anulado - Utilizado/pago + -- Financeiro recebido + -- Financeiro devolvido + -- Utilizado/pago + valores_financeiro_tb as ( + select + plano_acao, + num_transf, + sum( + case when pf_acao = 'TRANSFERENCIA' then pf_valor_linha else 0 end + ) as financeiro_recebido, + sum( + case when pf_acao = 'DEVOLUCAO' then pf_valor_linha else 0 end + ) as financeiro_devolvido, + sum( + case when pf_acao = 'CANCELAMENTO' then pf_valor_linha else 0 end + ) as financeiro_cancelado, + max(dt_ingest) as dt_ingest_vfin + from {{ ref("pf_plano_acao") }} + group by plano_acao, num_transf + ), + -- Saldo financeiro = Financeiro recebido - Financeiro devolvido - Utilizado/pago + -- Financeiro a receber = Valor firmado - Financeiro recebido + Financeiro devolvido + join_parcial as ( + select + *, + greatest(vo.dt_ingest_vo, ve.dt_ingest_ve, vfin.dt_ingest_vfin) as dt_ingest_jp + from valores_orcamentos_tb vo + full join valores_empenhados_tb ve using (plano_acao, num_transf) + full join valores_financeiro_tb vfin using (plano_acao, num_transf) + + ) +-- Final +select + plano_acao, + num_transf, + sigla_unidade_descentralizada, + case + when ted_beneficiario_emitente = 'beneficiario' + then 'beneficiario' + when ted_beneficiario_emitente = 'emitente' + then 'emitente' + else 'nao_indicado' + end as ted_beneficiario_emitente, + valor_firmado, + orcamento_recebido, + orcamento_devolvido, + empenhado, + empenho_anulado, + despesas_pagas_exercicio, + despesas_pagas_rap, + restos_a_pagar, + despesas_liquidada, + financeiro_recebido, + financeiro_devolvido, + financeiro_cancelado, + greatest(vf.dt_ingest_vf, jp.dt_ingest_jp) as dt_ingest +from valor_firmado_tb vf +full join join_parcial jp using (plano_acao) +where (plano_acao is not null) or (num_transf is not null) diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/silver/empenhos_plano_acao.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/silver/empenhos_plano_acao.sql new file mode 100644 index 00000000..cfaff6ab --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/silver/empenhos_plano_acao.sql @@ -0,0 +1,35 @@ +with + empenhos_ids as ( + select + *, + -- Uma série de extrações que servirão de identificadores + right(ne_ccor, 12) as ne, + replace( + ( + regexp_match( + ne_ccor_descricao, + '(FERENCIA|NUMERO|Nº|TED|CRICAO|TRANSF.|CAO|TRANSFERENCIA )(\s|^|-|)([0-9]{6}|1\w{5}|[0-9]{3}\.[0-9]{3})(\s|$|\.|,|-|\/)' + ) + )[3], + '.', + '' + ) as num_transf, + {{ target.schema }}.format_nc( + regexp_substr(ne_ccor_descricao, '([0-9]{4}NC[0-9]+)') + ) as nc + from {{ ref("empenhos_tesouro") }} + ), + empenhos_filtrados as ( + select * from empenhos_ids where (nc != '') or (num_transf is not null) + ), + planos_de_acao as ( + select * from {{ ref("num_transf_n_plano_acao") }} where plano_acao is not null + ), + result_table as ( + select distinct * + from empenhos_filtrados + left join planos_de_acao using (num_transf) + ) -- + +select * +from result_table diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/silver/nc_plano_acao.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/silver/nc_plano_acao.sql new file mode 100644 index 00000000..bcf2e990 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/silver/nc_plano_acao.sql @@ -0,0 +1,37 @@ +with + + raw_data as (select * from {{ ref("nc_tesouro") }} nt where nc_transferencia != '-8'), + + planos_de_acao as ( + select distinct * + from {{ ref("num_transf_n_plano_acao") }} + where plano_acao is not null + ), + + result_table as ( + select + pda.plano_acao, + emissao_dia, + nc_transferencia as num_transf, + right(nc, 12) as nc, + nc_fonte_recursos, + ptres, + left(nc, 6) as ug_emitente, + nc_natureza_despesa, + nc_evento, + nc_evento_descr, + -- aplica o sinal correto a depender do tipo de evento + case + when nc_evento in ('300302', '300308', '300311', '300083') + then (-1) * nc_valor_linha + else nc_valor_linha + end as nc_valor, + rd.dt_ingest + from raw_data rd + left join planos_de_acao pda on rd.nc_transferencia = pda.num_transf + ) + +-- +select * +from result_table +order by 1, 2 diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/silver/pf_plano_acao.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/silver/pf_plano_acao.sql new file mode 100644 index 00000000..dd877ade --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/silver/pf_plano_acao.sql @@ -0,0 +1,74 @@ +with + + programacoes_financeira as ( + select + pf, + pf_inscricao as num_transf, + emissao_mes, + emissao_dia, + ug_emitente, + ug_favorecido, + pf_evento, + pf_evento_descricao, + substring(pf_acao_descricao, '(\w+) ') as pf_acao, + pf_valor_linha, + dt_ingest as dt_ingest_pf + from {{ ref("pf_tesouro") }} + ), + + pf_transfere_gov as ( + select + tx_numero_programacao as pf, + ug_emitente_programacao as ug_emitente, + id_plano_acao as plano_acao, + (dt_ingest || '-03:00')::timestamptz as dt_ingest_tg + from {{ source("transfere_gov", "programacao_financeira") }} -- raw + ), + + joined_by_transfere_gov as ( + select + pf, + num_transf, + emissao_mes, + emissao_dia, + ug_emitente, + ug_favorecido, + pf_evento, + pf_evento_descricao, + pf_acao, + pf_valor_linha, + t.plano_acao, + greatest(pf.dt_ingest_pf, t.dt_ingest_tg) as dt_ingest + from programacoes_financeira pf + inner join pf_transfere_gov t using (pf, ug_emitente) + ), + + joined_by_num_transf as ( + select + pf.pf, + pf.num_transf, + pf.emissao_mes, + pf.emissao_dia, + pf.ug_emitente, + pf.ug_favorecido, + pf.pf_evento, + pf.pf_evento_descricao, + pf.pf_acao, + pf.pf_valor_linha, + v.plano_acao, + pf.dt_ingest_pf as dt_ingest + from programacoes_financeira pf + inner join {{ ref("num_transf_n_plano_acao") }} v using (num_transf) + -- Exclui registros que já existem em joined_by_transfere_gov + where not exists ( + select 1 + from pf_transfere_gov t + where t.pf = pf.pf and t.ug_emitente = pf.ug_emitente + ) + ) + +select * +from joined_by_transfere_gov +union all +select * +from joined_by_num_transf \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/silver/schema.yml b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/silver/schema.yml new file mode 100644 index 00000000..55b0523b --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/silver/schema.yml @@ -0,0 +1,125 @@ +version: 2 + +models: + + - name: empenhos_plano_acao + description: > + Esta tabela estabelece a relação entre os empenhos registrados no SIAFI e os respectivos Planos de Ação, + permitindo o rastreamento das despesas públicas desde a origem do crédito até sua execução. + meta: + tags: + - silver + columns: + - name: ne + description: > + Número da Nota de Empenho, extraído dos 12 últimos dígitos do campo `ne_ccor`, representando o identificador do empenho no SIAFI. + - name: num_transf + description: > + Número da transferência identificado na descrição da nota de empenho (`ne_ccor_descricao`), utilizado para vincular o empenho ao Plano de Ação correspondente. + - name: nc + description: > + Código da Nota de Crédito associado ao empenho, extraído da descrição da nota de empenho e formatado conforme padrão estabelecido. + - name: plano_acao + description: > + Identificador do Plano de Ação relacionado ao empenho, obtido a partir da correspondência com o número de transferência (`num_transf`). + - name: ne_ccor + description: > + Campo original contendo o código completo da Nota de Empenho, utilizado como base para extração de identificadores. + - name: ne_ccor_descricao + description: > + Descrição textual da Nota de Empenho, de onde são extraídos os números de transferência e códigos de nota de crédito para associação com os Planos de Ação. + - name: demais_colunas + description: > + Outras colunas provenientes da tabela `empenhos_tesouro`, contendo informações adicionais sobre os empenhos, como data de emissão, valor, unidade gestora, entre outras. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: nc_plano_acao + description: > + Esta tabela estabelece a relação entre as Notas de Crédito registradas no SIAFI e os respectivos Planos de Ação, + permitindo o rastreamento das movimentações financeiras desde a origem do crédito até sua aplicação. + meta: + tags: + - silver + columns: + - name: plano_acao + description: > + Identificador do Plano de Ação relacionado à Nota de Crédito, obtido a partir da correspondência com o número de transferência (`nc_transferencia`). + - name: emissao_dia + description: > + Dia de emissão da Nota de Crédito. + - name: num_transf + description: > + Número da transferência associado à Nota de Crédito, utilizado para vincular a movimentação financeira ao Plano de Ação correspondente. + - name: nc + description: > + Código da Nota de Crédito, extraído dos 12 últimos dígitos do campo `nc`, representando o identificador da nota no SIAFI. + - name: nc_fonte_recursos + description: > + Código da fonte de recursos associada à Nota de Crédito, indicando a origem dos recursos utilizados. + - name: ptres + description: > + Código do Plano Interno de Trabalho (PTRES) vinculado à Nota de Crédito, representando a ação orçamentária correspondente. + - name: ug_emitente + description: > + Código da Unidade Gestora emitente da Nota de Crédito, extraído dos 6 primeiros dígitos do campo `nc`. + - name: nc_natureza_despesa + description: > + Código da natureza da despesa associada à Nota de Crédito, conforme classificação orçamentária. + - name: nc_evento + description: > + Código do evento contábil associado à Nota de Crédito, representando a natureza da transação. + - name: nc_evento_descr + description: > + Descrição do evento contábil vinculado à Nota de Crédito. + - name: nc_valor + description: > + Valor monetário da Nota de Crédito, ajustado conforme o tipo de evento contábil. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + + - name: pf_plano_acao + description: > + Esta tabela consolida as programações financeiras executadas no âmbito do SIAFI que estão relacionadas a Planos de Ação. + meta: + tags: + - silver + columns: + - name: pf + description: > + Número identificador da Programação Financeira, conforme registrado no SIAFI. + - name: num_transf + description: > + Número da transferência extraído do campo `pf_inscricao`, utilizado como chave alternativa para vinculação com Planos de Ação. + - name: emissao_mes + description: > + Mês da emissão da Programação Financeira, no formato numérico (1 a 12). + - name: emissao_dia + description: > + Dia da emissão da Programação Financeira, no formato numérico (1 a 31). + - name: ug_emitente + description: > + Código da Unidade Gestora responsável pela emissão da Programação Financeira. + - name: ug_favorecido + description: > + Código da Unidade Gestora favorecida pela Programação Financeira. + - name: pf_evento + description: > + Código do evento contábil associado à Programação Financeira, que descreve o tipo de movimentação registrada. + - name: pf_evento_descricao + description: > + Descrição do evento contábil da Programação Financeira, fornecendo contexto adicional sobre o tipo de operação realizada. + - name: pf_acao + description: > + Código da ação orçamentária extraído da descrição da programação financeira, representando a finalidade da despesa. + - name: pf_valor_linha + description: > + Valor programado para execução financeira, referente à linha específica da Programação Financeira. + - name: plano_acao + description: > + Identificador do Plano de Ação associado à Programação Financeira, determinado por correspondência direta com o sistema TransfereGov ou via número de transferência. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/views/num_transf_n_plano_acao.sql b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/views/num_transf_n_plano_acao.sql new file mode 100644 index 00000000..b413f619 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/views/num_transf_n_plano_acao.sql @@ -0,0 +1,36 @@ +{{ config(materialized="view") }} + +with + + nc_transfere_gov as ( + select distinct id_plano_acao, tx_numero_nota as nc, ndc.cd_ug_emitente_nota as ug + from {{ source("transfere_gov", "notas_de_credito") }} ndc + where ndc.tx_numero_nota is not null + ), + + nc_siafi as ( + select distinct + left(nc, 6) as ug, right(nc, 12) as nc, nt.nc_transferencia as num_transf + from {{ source("siafi", "nc_tesouro") }} nt + where nc_transferencia != '-8' + ), + + joined as ( + select distinct num_transf, id_plano_acao as plano_acao + from nc_siafi + left join nc_transfere_gov using (nc, ug) + ), + + ranked as ( + select + *, + row_number() over ( + partition by num_transf + order by case when plano_acao is not null then 1 else 2 end + ) as rn + from joined + ) + +select num_transf, plano_acao +from ranked +where rn = 1 diff --git a/airflow_lappis/dags/dbt/ipea/models/ted_dbt/views/schema.yml b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/views/schema.yml new file mode 100644 index 00000000..3ad69e0b --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/models/ted_dbt/views/schema.yml @@ -0,0 +1,16 @@ +version: 2 + +models: + + - name: num_transf_n_plano_acao + description: > + View responsável por mapear o número de transferência (`num_transf`) ao respectivo Plano de Ação (`plano_acao`). + Utiliza dados de Notas de Crédito do TransfereGov e do SIAFI para realizar o cruzamento entre diferentes sistemas. + columns: + - name: num_transf + description: > + Número da transferência financeira obtido a partir dos registros do SIAFI (nota de crédito). Serve como chave para integrar informações entre diferentes fontes, como notas de crédito, programações financeiras e empenhos. + - name: plano_acao + description: > + Identificador único do Plano de Ação, conforme registrado no TransfereGov. É atribuído ao `num_transf` com base no cruzamento entre a nota de crédito (NC) e a unidade gestora emitente (UG). + diff --git a/airflow_lappis/dags/dbt/ipea/profiles.yml b/airflow_lappis/dags/dbt/ipea/profiles.yml new file mode 100755 index 00000000..17f14e26 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/profiles.yml @@ -0,0 +1,11 @@ +ipea: + target: prod + outputs: + prod: + type: postgres + host: "{{ env_var('DB_DW_HOST', 'postgres') }}" + user: "{{ env_var('DB_DW_USER', 'postgres_dw') }}" + password: "{{ env_var('DB_DW_PASSWORD', 'postgres_dw') }}" + port: "{{ env_var('DB_DW_PORT', '5432') | int }}" + dbname: "{{ env_var('DB_DW_DBNAME', 'data_warehouse') }}" + schema: "{{ env_var('DB_DW_SCHEMA', 'ipea') }}" \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/ipea/seeds/estados_brasil.csv b/airflow_lappis/dags/dbt/ipea/seeds/estados_brasil.csv new file mode 100644 index 00000000..eaa791fd --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/seeds/estados_brasil.csv @@ -0,0 +1,28 @@ +sigla_uf,nome_uf +AC,ACRE +AL,ALAGOAS +AP,AMAPÁ +AM,AMAZONAS +BA,BAHIA +CE,CEARÁ +DF,DISTRITO FEDERAL +ES,ESPÍRITO SANTO +GO,GOIÁS +MA,MARANHÃO +MT,MATO GROSSO +MS,MATO GROSSO DO SUL +MG,MINAS GERAIS +PA,PARÁ +PB,PARAÍBA +PR,PARANÁ +PE,PERNAMBUCO +PI,PIAUÍ +RJ,RIO DE JANEIRO +RN,RIO GRANDE DO NORTE +RS,RIO GRANDE DO SUL +RO,RONDÔNIA +RR,RORAIMA +SC,SANTA CATARINA +SP,SÃO PAULO +SE,SERGIPE +TO,TOCANTINS diff --git a/airflow_lappis/dags/dbt/ipea/snapshots/tables_snapshot.yml b/airflow_lappis/dags/dbt/ipea/snapshots/tables_snapshot.yml new file mode 100644 index 00000000..84d36234 --- /dev/null +++ b/airflow_lappis/dags/dbt/ipea/snapshots/tables_snapshot.yml @@ -0,0 +1,36 @@ +snapshots: + - name: contratos_snapshot + relation: ref('contratos') + config: + schema: snapshots + database: analytics + unique_key: id + strategy: check + check_cols: [situacao, num_parcelas, valor_parcela, valor_global, valor_acumulado] + + - name: faturas_snapshot + relation: ref('faturas') + config: + schema: snapshots + database: analytics + unique_key: [id, id_empenho] + strategy: check + check_cols: [situacao, valor, juros, multa, glosa] + + - name: empenhos_snapshot + relation: ref('empenhos') + config: + schema: snapshots + database: analytics + unique_key: [id, contrato_id] + strategy: check + check_cols: [empenhado, aliquidar, liquidado, pago, rpinscrito, rpaliquidar, rpliquidado, rppago] + + - name: cronogramas_snapshot + relation: ref('cronogramas') + config: + schema: snapshots + database: analytics + unique_key: id + strategy: check + check_cols: [valor, retroativo, observacao] \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/.user.yml b/airflow_lappis/dags/dbt/mir/.user.yml new file mode 100755 index 00000000..43198208 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/.user.yml @@ -0,0 +1 @@ +id: d025f823-c5b2-49c6-b826-226fb25f1ad8 diff --git a/airflow_lappis/dags/dbt/mir/cosmos_dag.py b/airflow_lappis/dags/dbt/mir/cosmos_dag.py new file mode 100755 index 00000000..e139edeb --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/cosmos_dag.py @@ -0,0 +1,30 @@ +import os +from datetime import datetime +from cosmos import DbtDag, ProjectConfig, ProfileConfig, ExecutionConfig +from cosmos.constants import DBT_LOG_PATH_ENVVAR + +dbt_log_path = "/tmp/dbt_logs" # NOSONAR +os.makedirs(dbt_log_path, exist_ok=True) +os.environ[DBT_LOG_PATH_ENVVAR] = dbt_log_path + +profile_config = ProfileConfig( + profiles_yml_filepath=f"{os.environ['AIRFLOW_REPO_BASE']}/dags/dbt/mir/profiles.yml", + profile_name="mir", + target_name="prod", +) + +my_cosmos_dag = DbtDag( + project_config=ProjectConfig(f"{os.environ['AIRFLOW_REPO_BASE']}/dags/dbt/mir"), + profile_config=profile_config, + execution_config=ExecutionConfig( + dbt_executable_path=f"{os.environ['AIRFLOW_REPO_BASE']}/.local/bin/dbt", + ), + # Expressãp cron para agendar a execução do DAG diariamente às 01:00 + # Futuralmente isso pode ser substituído por um cronograma mais específico + # com dependências entre os DAGs + schedule_interval=" 0 1 * * *", + start_date=datetime(2025, 1, 1), + catchup=False, + dag_id="mir_cosmos_dag", + default_args={"retries": 2}, +) diff --git a/airflow_lappis/dags/dbt/mir/dbt_project.yml b/airflow_lappis/dags/dbt/mir/dbt_project.yml new file mode 100755 index 00000000..7a65b43b --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/dbt_project.yml @@ -0,0 +1,41 @@ +name: 'mir' + +version: 1.0.0 +config-version: 2 + +profile: mir + +model-paths: ["models"] +analysis-paths: ["analyses"] +seed-paths: ["seeds"] +test-paths: ["tests"] +macro-paths: ["macros"] + +clean-targets: + - "target" + - "dbt_packages" + - "logs" + +models: + mir: + +database: analytics + metadata: + +materialized: incremental + +schema: metadata + emendas_dbt: + +materialized: table + +schema: emendas + dados_abertos_dbt: + +materialized: table + +schema: dados_abertos + empenhos_ted_dbt: + +materialized: table + +schema: siafi_dbt + siconv_dbt: + +materialized: table + +schema: siconv_dbt + + + +on-run-start: + - '{{create_udfs()}}' \ No newline at end of file diff --git a/src/__init__.py b/airflow_lappis/dags/dbt/mir/descriptions.yml similarity index 100% rename from src/__init__.py rename to airflow_lappis/dags/dbt/mir/descriptions.yml diff --git a/airflow_lappis/dags/dbt/mir/macros/create_udfs.sql b/airflow_lappis/dags/dbt/mir/macros/create_udfs.sql new file mode 100644 index 00000000..dd230cf5 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/macros/create_udfs.sql @@ -0,0 +1,10 @@ +{% macro create_udfs() %} + +create schema if not exists {{ target.schema }}; + + {{ create_f_parse_dates() }} + ; + {{ create_f_format_nc() }} + ; + +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/mir/macros/data_quality/row_count_match.sql b/airflow_lappis/dags/dbt/mir/macros/data_quality/row_count_match.sql new file mode 100644 index 00000000..f248e30c --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/macros/data_quality/row_count_match.sql @@ -0,0 +1,14 @@ +{% macro test_row_count_match(model, source_table, target_table) %} + with + source_count as (select count(*) as row_count from {{ source_table }}), + target_count as (select count(*) as row_count from {{ target_table }}), + comparison as ( + select + source_count.row_count as source_row_count, + target_count.row_count as target_row_count + from source_count, target_count + ) + select * + from comparison + where source_row_count != target_row_count +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/mir/macros/data_quality/verificacao_tipagem.sql b/airflow_lappis/dags/dbt/mir/macros/data_quality/verificacao_tipagem.sql new file mode 100644 index 00000000..34c3d392 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/macros/data_quality/verificacao_tipagem.sql @@ -0,0 +1,25 @@ +{% macro test_verificacao_tipagem(model, nome_tabela, nome_coluna, tipo_esperado) %} + with + column_info as ( + select + table_schema, + table_name, -- Nome real da coluna no information_schema + column_name, -- Nome real da coluna no information_schema + data_type + from information_schema.columns + where + table_schema || '.' || table_name = '{{ nome_tabela }}' + and column_name = '{{ nome_coluna }}' + ), + comparison as ( + select + '{{ nome_tabela }}' as nome_tabela, + '{{ nome_coluna }}' as nome_coluna, + '{{ tipo_esperado }}' as tipo_esperado, + data_type as actual_type + from column_info + ) + select * + from comparison + where actual_type != tipo_esperado +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/mir/macros/get_custom_schema.sql b/airflow_lappis/dags/dbt/mir/macros/get_custom_schema.sql new file mode 100755 index 00000000..79444e9a --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/macros/get_custom_schema.sql @@ -0,0 +1,4 @@ +-- built-in schema generator +{% macro generate_schema_name(custom_schema_name, node) -%} + {{ generate_schema_name_for_env(custom_schema_name, node) }} +{%- endmacro %} diff --git a/airflow_lappis/dags/dbt/mir/macros/metadata/generate_metadata.sql b/airflow_lappis/dags/dbt/mir/macros/metadata/generate_metadata.sql new file mode 100644 index 00000000..8bfb115b --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/macros/metadata/generate_metadata.sql @@ -0,0 +1,46 @@ +{% macro get_model_metadata() %} +{# + Esta macro retorna os metadados do modelo atual. + Pode ser usada em post-hooks para registrar metadados automaticamente. +#} + SELECT + '{{ this.schema }}' AS schema_name, + '{{ this.name }}' AS table_name, + '{{ this.database }}' AS database_name, + ('{{ run_started_at }}'::TIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE 'America/Sao_Paulo') AS dt_transform, + '{{ invocation_id }}' AS run_id +{% endmacro %} + + +{% macro register_model_metadata() %} +{# + Esta macro registra os metadados do modelo em uma tabela central. + Deve ser usada como post-hook nos modelos que deseja rastrear. + + Uso no dbt_project.yml: + models: + ipea: + +post-hook: + - "{{ register_model_metadata() }}" +#} + + INSERT INTO {{ target.database }}.metadata.models_metadata ( + schema_name, + table_name, + database_name, + dt_transform, + run_id + ) + VALUES ( + '{{ this.schema }}', + '{{ this.name }}', + '{{ this.database }}', + ('{{ run_started_at }}'::TIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE 'America/Sao_Paulo'), + '{{ invocation_id }}' + ) + ON CONFLICT (schema_name, table_name) + DO UPDATE SET + dt_transform = EXCLUDED.dt_transform, + run_id = EXCLUDED.run_id; + +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/mir/macros/name_formater.sql b/airflow_lappis/dags/dbt/mir/macros/name_formater.sql new file mode 100644 index 00000000..e736bcb3 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/macros/name_formater.sql @@ -0,0 +1,3 @@ +{% macro name_formater(column_name) %} + TRIM(TRANSLATE(UPPER({{ column_name }}), 'ÁÀÂÃÄÅÉÈÊËÍÌÎÏÓÒÔÕÖÚÙÛÜÇÑ', 'AAAAAAEEEEIIIIOOOOOUUUUCN')) +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/mir/macros/parse_financial_value.sql b/airflow_lappis/dags/dbt/mir/macros/parse_financial_value.sql new file mode 100644 index 00000000..437b673c --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/macros/parse_financial_value.sql @@ -0,0 +1,21 @@ +{% macro parse_financial_value(column_name) %} + + case + when {{ column_name }} is null or trim({{ column_name }}) = '' + then 0.00::numeric(15, 2) + when {{ column_name }} like '%NaN%' + then 0.00::numeric(15, 2) + when {{ column_name }} like '(%' + then + regexp_replace( + replace(coalesce({{ column_name }}, '0'), '.', ''), + '(\()?(\d+),(\d+)(\))?', + '-\2.\3' + )::numeric(15, 2) + else + replace( + replace(coalesce({{ column_name }}, '0'), '.', ''), ',', '.' + )::numeric(15, 2) + end + +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/mir/macros/schema.yml b/airflow_lappis/dags/dbt/mir/macros/schema.yml new file mode 100644 index 00000000..694f3b23 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/macros/schema.yml @@ -0,0 +1,24 @@ + +version: 2 + +macros: + - name: create_udfs + description: > + Função que cria as UDFs necessárias para o funcionamento do projeto. + Essa função deve ser chamada no início de cada run para garantir que todas as UDFs estejam disponíveis. + + - name: generate_schema_name + description: > + Função que gera o nome do schema a ser utilizado no projeto. + A função dentro desta macro é built-in do dbt. + + ## UDFS + - name: create_f_parse_dates + description: > + Função que cria a UDF f_parse_dates, que é utilizada para converter texto no formato MÊS(texto)/ANO(numero) em datas. + arguments: + - name: in_text + type: text + description: > + Texto a ser convertido em data. + O texto deve estar no formato MÊS(texto)/ANO(numero). Ex.: FEV/2024 -> 2024-02-01 \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/macros/udfs/f_format_nc.sql b/airflow_lappis/dags/dbt/mir/macros/udfs/f_format_nc.sql new file mode 100644 index 00000000..57b3f716 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/macros/udfs/f_format_nc.sql @@ -0,0 +1,34 @@ +{% macro create_f_format_nc() %} + create or replace function {{ target.schema }}.format_nc(in_text text) + returns text + as $$ + + with + + pre_process as ( + select + left(in_text, 7) as prefix, + right(in_text, 4) as posfix_text + ), + + normalized as ( + select + prefix, + case + when posfix_text ~ '^[0-9]{1,4}$' then posfix_text::numeric + else null + end as posfix + from pre_process + ) + + select + case + when posfix is null then null + else concat(prefix, to_char(posfix, 'FM00000')) + end as result + from normalized + + $$ + language sql + ; +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/mir/macros/udfs/f_parse_dates.sql b/airflow_lappis/dags/dbt/mir/macros/udfs/f_parse_dates.sql new file mode 100644 index 00000000..3fd8693e --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/macros/udfs/f_parse_dates.sql @@ -0,0 +1,44 @@ +-- Essa fun +{% macro create_f_parse_dates() %} + + create or replace function {{ target.schema }}.parse_date(in_text text) + returns date + as + $$ + + with + + split_column as ( + select + split_part(in_text, '/', 1) as mes, + split_part(in_text, '/', 2) as ano + ), + + fixed_month as ( + select + ano, + case + when mes = 'JAN' then '01' + when mes = 'FEV' then '02' + when mes = 'MAR' then '03' + when mes = 'ABR' then '04' + when mes = 'MAI' then '05' + when mes = 'JUN' then '06' + when mes = 'JUL' then '07' + when mes = 'AGO' then '08' + when mes = 'SET' then '09' + when mes = 'OUT' then '10' + when mes = 'NOV' then '11' + when mes = 'DEZ' then '12' + else mes end as mes_num + from split_column + ) + + select + (to_date(ano::numeric - 1 || '-' || '12', 'YYYY-MM') + (mes_num || ' months')::interval)::date as result + from fixed_month + $$ + language sql + ; + +{% endmacro %} diff --git a/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/deputados.sql b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/deputados.sql new file mode 100644 index 00000000..c523a197 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/deputados.sql @@ -0,0 +1,19 @@ +{{ config(materialized="table") }} + +with + deputados_raw as ( + select + -- Conversão de tipos e formatação de colunas + id::integer as id, + nome::text as nome, + siglapartido::text as siglapartido, + siglauf::text as siglauf, + idlegislatura::integer as idlegislatura, + urlfoto::text as urlfoto, + email::text as email, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("camara_deputados", "deputados") }} + ) + +select * +from deputados_raw diff --git a/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/deputados_historico.sql b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/deputados_historico.sql new file mode 100644 index 00000000..d826d702 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/deputados_historico.sql @@ -0,0 +1,72 @@ +{{ config(materialized="table") }} + +with + deputados_raw as ( + select + id::integer as id, + nome::text as nome, + siglapartido::text as sigla_partido, + uripartido::text as uri_partido, + siglauf::text as sigla_uf, + idlegislatura::integer as id_legislatura, + datahora::timestamptz as data_evento, + trim(situacao)::text as situacao, + condicaoeleitoral::text as condicao_eleitoral, + parlamentar_id::integer as parlamentar_id, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("camara_deputados", "deputados_historico") }} + where situacao is not null + and situacao != '' + ), + + -- Legislatura Atual para evitar vazamentos de estado ativo em mandatos extintos + meta_legislatura as ( + select max(id_legislatura) as max_id_leg from deputados_raw + ), + + legislaturas_dim as ( + select + id, + data_fim::timestamptz as data_fim_legislatura + from {{ ref("legislaturas") }} + ), + + calculo_periodos as ( + select + dr.*, + lead(dr.data_evento) over ( + partition by dr.id + order by dr.data_evento asc + ) as proximo_evento_data, + ml.max_id_leg, + ld.data_fim_legislatura + from deputados_raw dr + cross join meta_legislatura ml + left join legislaturas_dim ld + on dr.id_legislatura = ld.id + ) + +select + id, + nome, + sigla_partido, + id_legislatura, + situacao, + data_evento as data_filiacao, + case + -- Caso 1: Existe um próximo registro (segue a cronologia normal) + when proximo_evento_data is not null then proximo_evento_data + + -- Caso 2: É o último registro, mas a situação é de encerramento explícito (Vacância/Fim de Mandato/Falecimento) + when situacao in ('Vacância', 'Fim de Mandato', 'Falecimento') then data_evento + + -- Caso 3: É o último registro, restando qualquer outra situação, mas de uma LEGISLATURA ANTIGA + when id_legislatura < max_id_leg then coalesce(data_fim_legislatura, data_evento) + + -- Caso 4: É o último registro, restando qualquer outra situação, na legislatura ATUAL + when id_legislatura = max_id_leg then null + + else null + end as data_desfiliacao, + dt_ingest +from calculo_periodos \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/legislaturas.sql b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/legislaturas.sql new file mode 100644 index 00000000..5e316a49 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/legislaturas.sql @@ -0,0 +1,15 @@ +{{ config(materialized="table") }} + +with + legislaturas_raw as ( + select + id::integer as id, + data_inicio::date as data_inicio, + data_fim::date as data_fim, + data_eleicao::date as data_eleicao, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("senado_federal", "legislaturas") }} + ) + +select * +from legislaturas_raw diff --git a/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/schema.yml b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/schema.yml new file mode 100644 index 00000000..551995a3 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/schema.yml @@ -0,0 +1,362 @@ +version: 2 + +models: + # Dados Abertos DBT + + ## Bronze + - name: deputados + description: > + Tabela com informações básicas sobre deputados federais obtidas por meio da API de Dados Abertos da Câmara dos Deputados. + Contém dados cadastrais e institucionais como identificador, nome parlamentar, partido, unidade federativa, + legislatura e e-mail institucional. + Esta tabela representa a camada bronze do pipeline, aplicando padronização de tipos, + pequenas transformações e ajuste de fuso horário na data de ingestão. + Os dados são provenientes da fonte dados_abertos.deputados (camada raw) e + passam por conversão explícita de tipos para garantir consistência e rastreabilidade. + meta: + tags: + - bronze + columns: + - name: id + description: > + Identificador único do deputado na API da Câmara. + Utilizado como chave primária e referência para relacionamentos com outras tabelas. + + - name: nome + description: > + Nome parlamentar do deputado conforme registrado na Câmara dos Deputados. + + - name: siglapartido + description: > + Sigla do partido ao qual o deputado está filiado na legislatura informada. + + - name: siglauf + description: > + Sigla da Unidade Federativa (UF) que o deputado representa. + + - name: idlegislatura + description: > + Identificador numérico da legislatura à qual o deputado está vinculado. + + - name: urlfoto + description: > + URL da foto oficial do deputado disponibilizada pela Câmara dos Deputados. + + - name: email + description: > + Endereço de e-mail institucional do deputado na Câmara dos Deputados. + + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + tests: + - verificacao_tipagem: + nome_tabela: 'deputados.deputados' + nome_coluna: 'id' + tipo_esperado: 'integer' + + - verificacao_tipagem: + nome_tabela: 'deputados.deputados' + nome_coluna: 'nome' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'deputados.deputados' + nome_coluna: 'siglapartido' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'deputados.deputados' + nome_coluna: 'siglauf' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'deputados.deputados' + nome_coluna: 'idlegislatura' + tipo_esperado: 'integer' + + - verificacao_tipagem: + nome_tabela: 'deputados' + nome_coluna: 'urlfoto' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'deputados.deputados' + nome_coluna: 'email' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'deputados.deputados' + nome_coluna: 'dt_ingest' + tipo_esperado: 'timestamp with time zone' + + - name: senadores + description: > + Tabela com informações básicas sobre senadores federais obtidas por meio da API de Dados Abertos do Senado Federal. + Contém dados cadastrais e institucionais como identificador, nome parlamentar, sexo, forma de tratamento, + partido, unidade federativa, URLs de foto e página, e e-mail institucional. + Esta tabela representa a camada bronze do pipeline, aplicando padronização de tipos, + pequenas transformações e ajuste de fuso horário na data de ingestão. + Os dados são provenientes da fonte senado_federal.senadores (camada raw) e + passam por conversão explícita de tipos para garantir consistência e rastreabilidade. + meta: + tags: + - bronze + columns: + - name: id + description: > + Identificador único do senador na API do Senado Federal. + Utilizado como chave primária e referência para relacionamentos com outras tabelas. + + - name: nome_parlamentar + description: > + Nome parlamentar do senador conforme registrado no Senado Federal. + + - name: sexo + description: > + Sexo do senador (Masculino ou Feminino). + + - name: forma_tratamento + description: > + Forma de tratamento do senador (ex: Senador, Senadora). + + - name: url_foto + description: > + URL da foto oficial do senador disponibilizada pelo Senado Federal. + + - name: url_pagina + description: > + URL da página de perfil do senador no portal do Senado Federal. + + - name: email + description: > + Endereço de e-mail institucional do senador no Senado Federal. + + - name: sigla_partido + description: > + Sigla do partido ao qual o senador está filiado. + + - name: uf + description: > + Sigla da Unidade Federativa (UF) que o senador representa. + + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + + tests: + - verificacao_tipagem: + nome_tabela: 'senadores.senadores' + nome_coluna: 'id' + tipo_esperado: 'integer' + + - verificacao_tipagem: + nome_tabela: 'senadores.senadores' + nome_coluna: 'nome_parlamentar' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'senadores.senadores' + nome_coluna: 'sexo' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'senadores.senadores' + nome_coluna: 'forma_tratamento' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'senadores.senadores' + nome_coluna: 'url_foto' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'senadores.senadores' + nome_coluna: 'url_pagina' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'senadores.senadores' + nome_coluna: 'email' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'senadores.senadores' + nome_coluna: 'sigla_partido' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'senadores.senadores' + nome_coluna: 'uf' + tipo_esperado: 'text' + + - verificacao_tipagem: + nome_tabela: 'senadores.senadores' + nome_coluna: 'dt_ingest' + tipo_esperado: 'timestamp with time zone' + + - name: legislaturas + description: > + Tabela com informações de legislaturas da Câmara dos Deputados, incluindo + identificador, período de vigência e data de eleição. + Esta tabela representa a camada bronze do pipeline, com padronização de tipos + e ajuste de fuso horário na data de ingestão. + Os dados são provenientes da fonte camara_deputados.legislaturas (camada raw) e + passam por conversão explícita de tipos para garantir consistência e rastreabilidade. + meta: + tags: + - bronze + columns: + - name: id + description: > + Identificador único da legislatura na API da Câmara dos Deputados. + + - name: data_inicio + description: > + Data de início da legislatura. + + - name: data_fim + description: > + Data de término da legislatura. + + - name: data_eleicao + description: > + Data da eleição associada à legislatura. + + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + + tests: + - verificacao_tipagem: + nome_tabela: 'legislaturas.legislaturas' + nome_coluna: 'id' + tipo_esperado: 'integer' + + - verificacao_tipagem: + nome_tabela: 'legislaturas.legislaturas' + nome_coluna: 'data_inicio' + tipo_esperado: 'date' + + - verificacao_tipagem: + nome_tabela: 'legislaturas.legislaturas' + nome_coluna: 'data_fim' + tipo_esperado: 'date' + + - verificacao_tipagem: + nome_tabela: 'legislaturas.legislaturas' + nome_coluna: 'data_eleicao' + tipo_esperado: 'date' + + - verificacao_tipagem: + nome_tabela: 'legislaturas.legislaturas' + nome_coluna: 'dt_ingest' + tipo_esperado: 'timestamp with time zone' + + + - name: deputados_historico + description: > + Tabela contendo todo o detalhamento evolutivo (histórico temporal) dos deputados federais através + da API da Câmara dos Deputados, incluindo trocas de mandatos, filiações, afastamentos e licenças, + bem como o começo e final calculado exato de cada período. + meta: + tags: + - bronze + columns: + - name: id + description: Identificador do deputado na Câmara. + - name: nome + description: Nome oficial do deputado na API da Câmara. + - name: sigla_partido + description: Partido politico atrelado ao registro do histórico de filiação. + - name: id_legislatura + description: Código numérico referente a legislatura do evento histórico do deputado. + - name: situacao + description: Situação legal ou institucional durante aquele período (ex. Exercício, Afastado, Licença, etc). + - name: data_filiacao + description: A data em que o estado do período temporal foi iniciado. Baseado na data evento reportada da Câmara. + - name: data_desfiliacao + description: > + A data fina de encerramento do vínculo/situação, derivado dinamicamente das + ocorrências subsequentes e do fim oficial da legislatura. + - name: dt_ingest + description: Data de ingestão do registro advinda do raw. + + tests: + - verificacao_tipagem: + nome_tabela: 'dados_abertos.deputados_historico' + nome_coluna: 'id' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.deputados_historico' + nome_coluna: 'nome' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.deputados_historico' + nome_coluna: 'id_legislatura' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.deputados_historico' + nome_coluna: 'situacao' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.deputados_historico' + nome_coluna: 'data_filiacao' + tipo_esperado: 'timestamp with time zone' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.deputados_historico' + nome_coluna: 'data_desfiliacao' + tipo_esperado: 'timestamp with time zone' + + - name: senadores_historico + description: > + Tabela que detalha a temporalidade histórica de todas as filiações e trânsitos institucionais + recorrentes dos senadores federais pela API do Senado Federal. + meta: + tags: + - bronze + columns: + - name: parlamentar_id + description: Identificador base do senador. + - name: codigo_partido + description: Código associado com o registro partidário original do Senado. + - name: sigla_partido + description: Sigla partidária ao qual o legislador participava na época da filiação. + - name: nome_partido + description: Descritivo puro com o nome do partido atrelado a este registro historico. + - name: nome + description: Nome atrelado ao parlamentar nesta ocorrência temporal. + - name: data_filiacao + description: A data hora legal e literal em que a nova condição partidária foi assumida no senado. + - name: data_desfiliação + description: A data hora limítrofe oficial sinalizando que esse ciclo se acabou. + - name: ano_filiacao + description: Extração numérica do ano da vinculação que abriu o percurso. + - name: ano_desfiliação + description: Extração em inteiros do ano em que a ocorrência atingiu seu ponto derradeiro. + - name: fonte + description: Informação que aponta de qual engine o historico se baseia. + - name: dt_ingest + description: Timestamp padronizado indicando o horário de registro físico efetuado via job. + + tests: + - verificacao_tipagem: + nome_tabela: 'dados_abertos.senadores_historico' + nome_coluna: 'parlamentar_id' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.senadores_historico' + nome_coluna: 'data_filiacao' + tipo_esperado: 'timestamp with time zone' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.senadores_historico' + nome_coluna: 'data_desfiliação' + tipo_esperado: 'timestamp with time zone' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.senadores_historico' + nome_coluna: 'ano_filiacao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.senadores_historico' + nome_coluna: 'ano_desfiliação' + tipo_esperado: 'integer' diff --git a/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/senadores.sql b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/senadores.sql new file mode 100644 index 00000000..e6de70cc --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/senadores.sql @@ -0,0 +1,20 @@ +{{ config(materialized="table") }} + +with + senadores_raw as ( + select + id::integer as id, + nome_parlamentar::text as nome_parlamentar, + sexo::text as sexo, + forma_tratamento::text as forma_tratamento, + url_foto::text as url_foto, + url_pagina::text as url_pagina, + email::text as email, + sigla_partido::text as sigla_partido, + uf::text as uf, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("senado_federal", "senadores") }} + ) + +select * +from senadores_raw diff --git a/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/senadores_historico.sql b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/senadores_historico.sql new file mode 100644 index 00000000..09917699 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/bronze/senadores_historico.sql @@ -0,0 +1,39 @@ +{{ config(materialized="table") }} + +with + senadores_raw as ( + select + parlamentar_id::integer as parlamentar_id, + partido__codigopartido::text as codigo_partido, + partido__siglapartido::text as sigla_partido, + partido__nomepartido::text as nome_partido, + parlamentar__nome:: text as nome, + case + when lower(trim(datafiliacao::text)) in ('', 'nan', 'null') then null + else datafiliacao::timestamptz + end as data_filiacao, + case + when lower(trim(datadesfiliacao::text)) in ('', 'nan', 'null') then null + else datadesfiliacao::timestamptz + end as data_desfiliação, + case + when trim(anofiliacao::text) ~ '^[0-9]{4}$' then anofiliacao::integer + else null + end as ano_filiacao, + case + when trim(anodesfiliacao::text) ~ '^[0-9]{4}$' then anodesfiliacao::integer + else null + end as ano_desfiliação, + fonte:: text as fonte, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("senado_federal", "senadores_historico") }} + ), + + senadores_filtrados as ( + select * + from senadores_raw + where (data_filiacao is not null and data_filiacao >= '1995-01-01'::timestamptz) -- data confiável + ) + +select * +from senadores_filtrados diff --git a/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/silver/parlamentares.sql b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/silver/parlamentares.sql new file mode 100644 index 00000000..49a49aa5 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/silver/parlamentares.sql @@ -0,0 +1,77 @@ +{{ config(materialized='table') }} + +WITH +bronze_deputados AS ( + SELECT * FROM {{ ref('deputados') }} +), + +bronze_senadores AS ( + SELECT * FROM {{ ref('senadores') }} +), + +sigla_map AS ( + SELECT + TRIM(UPPER(sigla_origem)) AS sigla_origem, + TRIM(UPPER(sigla_canonica)) AS sigla_canonica + FROM {{ ref('partidos_map') }} +), + +parlamentares_unificados AS ( + SELECT + id AS id_parlamentar, + TRIM(UPPER(nome)) AS chave_join_nome, + nome AS nome_parlamentar, + 'Deputado' AS cargo_parlamentar, + siglapartido AS sigla_partido, + siglauf AS uf_parlamentar, + urlfoto AS url_foto, + email + FROM bronze_deputados + + UNION ALL + + SELECT + id AS id_parlamentar, + TRIM(UPPER(nome_parlamentar)) AS chave_join_nome, + nome_parlamentar AS nome_parlamentar, + 'Senador' AS cargo_parlamentar, + sigla_partido AS sigla_partido, + uf AS uf_parlamentar, + url_foto AS url_foto, + email + FROM bronze_senadores +), + +parlamentares_padronizados AS ( + SELECT + p.*, + COALESCE(m.sigla_canonica, p.sigla_partido) AS sigla_partido_padronizada + FROM parlamentares_unificados p + LEFT JOIN sigla_map m + ON TRIM(UPPER(p.sigla_partido)) = m.sigla_origem +), + +partidos_logo AS ( + SELECT + TRIM(UPPER(sigla)) AS chave_join_sigla_partido, + logo_url + FROM {{ ref('partidos_logo') }} +) + +SELECT + p.id_parlamentar, + p.chave_join_nome, + p.nome_parlamentar, + p.cargo_parlamentar, + + p.sigla_partido_padronizada AS sigla_partido, + + p.uf_parlamentar, + p.url_foto, + p.email, + pl.logo_url AS url_logo_partido + +FROM parlamentares_padronizados p + +LEFT JOIN partidos_logo pl + ON TRIM(UPPER(p.sigla_partido_padronizada)) = pl.chave_join_sigla_partido \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/silver/parlamentares_historico.sql b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/silver/parlamentares_historico.sql new file mode 100644 index 00000000..fa7590da --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/silver/parlamentares_historico.sql @@ -0,0 +1,133 @@ +{{ config(materialized='table') }} + +WITH +bronze_deputados_historico AS ( + SELECT * FROM {{ ref('deputados_historico') }} +), + +bronze_deputados AS ( + SELECT * FROM {{ ref('deputados') }} +), + +deputados_lookup AS ( + SELECT DISTINCT ON (id) + id, + nome, + siglauf, + urlfoto, + email, + dt_ingest + FROM bronze_deputados + ORDER BY id, dt_ingest DESC +), + +bronze_senadores_historico AS ( + SELECT * FROM {{ ref('senadores_historico') }} +), + +bronze_senadores AS ( + SELECT * FROM {{ ref('senadores') }} +), + +senadores_lookup AS ( + SELECT DISTINCT ON (id) + id, + nome_parlamentar, + uf, + url_foto, + email, + dt_ingest + FROM bronze_senadores + ORDER BY id, dt_ingest DESC +), + +sigla_map AS ( + SELECT + TRIM(UPPER(sigla_origem)) AS sigla_origem, + MAX(TRIM(UPPER(sigla_canonica))) AS sigla_canonica + FROM {{ ref('partidos_map') }} + GROUP BY 1 +), + +parlamentares_unificados AS ( + SELECT + dh.id AS id_parlamentar, + {{ name_formater("COALESCE(NULLIF(dh.nome, ''), d.nome)") }} AS chave_join_nome, + COALESCE(NULLIF(dh.nome, ''), d.nome) AS nome_parlamentar, + 'Deputado' AS cargo_parlamentar, + dh.sigla_partido AS sigla_partido, + d.siglauf AS uf_parlamentar, + d.urlfoto AS url_foto, + d.email AS email, + dh.data_filiacao, + dh.data_desfiliacao, + dh.id_legislatura, + dh.situacao, + NULL::text AS fonte, + dh.dt_ingest + FROM bronze_deputados_historico dh + LEFT JOIN deputados_lookup d + ON dh.id = d.id + + UNION ALL + + SELECT + sh.parlamentar_id AS id_parlamentar, + {{ name_formater("COALESCE(NULLIF(sh.nome, ''), s.nome_parlamentar)") }} AS chave_join_nome, + COALESCE(NULLIF(sh.nome, ''), s.nome_parlamentar) AS nome_parlamentar, + 'Senador' AS cargo_parlamentar, + sh.sigla_partido AS sigla_partido, + s.uf AS uf_parlamentar, + s.url_foto AS url_foto, + s.email AS email, + sh.data_filiacao, + sh.data_desfiliação AS data_desfiliacao, + NULL::integer AS id_legislatura, + NULL::text AS situacao, + sh.fonte, + sh.dt_ingest + FROM bronze_senadores_historico sh + LEFT JOIN senadores_lookup s + ON sh.parlamentar_id = s.id +), + +parlamentares_padronizados AS ( + SELECT + p.*, + COALESCE(m.sigla_canonica, p.sigla_partido) AS sigla_partido_padronizada + FROM parlamentares_unificados p + LEFT JOIN sigla_map m + ON TRIM(UPPER(p.sigla_partido)) = m.sigla_origem +), + +partidos_logo AS ( + SELECT + TRIM(UPPER(sigla)) AS chave_join_sigla_partido, + MAX(logo_url) AS logo_url + FROM {{ ref('partidos_logo') }} + GROUP BY 1 +) + +SELECT + p.id_parlamentar, + p.chave_join_nome, + p.nome_parlamentar, + p.cargo_parlamentar, + + p.sigla_partido_padronizada AS sigla_partido, + + p.uf_parlamentar, + p.url_foto, + p.email, + p.data_filiacao, + p.data_desfiliacao, + p.id_legislatura, + p.situacao, + p.fonte, + p.dt_ingest, + pl.logo_url AS url_logo_partido + +FROM parlamentares_padronizados p + +LEFT JOIN partidos_logo pl + ON TRIM(UPPER(p.sigla_partido_padronizada)) = pl.chave_join_sigla_partido \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/silver/schema.yml b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/silver/schema.yml new file mode 100644 index 00000000..dd126f89 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/dados_abertos_dbt/silver/schema.yml @@ -0,0 +1,144 @@ +version: 2 + +models: + - name: parlamentares + description: > + Tabela silver que unifica informações de deputados e senadores + em uma visão única de parlamentares, incluindo partido, UF, + foto e logo do partido. + meta: + tags: + - silver + columns: + - name: id_parlamentar + description: > + Identificador único do parlamentar na base unificada + de deputados e senadores. + - name: chave_join_nome + description: > + Chave de junção padronizada (nome em caixa alta e trimado), + utilizada para relacionar parlamentares com outros modelos. + - name: nome_parlamentar + description: > + Nome do parlamentar conforme registrado na base de origem. + - name: cargo_parlamentar + description: > + Cargo do parlamentar (por exemplo, Deputado ou Senador). + - name: sigla_partido + description: > + Sigla do partido ao qual o parlamentar está filiado. + - name: uf_parlamentar + description: > + Sigla da Unidade Federativa (UF) que o parlamentar representa. + - name: url_foto + description: > + URL da foto oficial do parlamentar na base de origem. + - name: email + description: > + Endereço de e-mail institucional do parlamentar. + - name: url_logo_partido + description: > + URL da imagem (logo) do partido do parlamentar, + proveniente do seed partidos_logo. + tests: + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares' + nome_coluna: 'id_parlamentar' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares' + nome_coluna: 'chave_join_nome' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares' + nome_coluna: 'nome_parlamentar' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares' + nome_coluna: 'cargo_parlamentar' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares' + nome_coluna: 'sigla_partido' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares' + nome_coluna: 'uf_parlamentar' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares' + nome_coluna: 'url_foto' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares' + nome_coluna: 'email' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares' + nome_coluna: 'url_logo_partido' + tipo_esperado: 'text' + + - name: parlamentares_historico + description: > + Tabela da camada silver que consolida e uniformiza o panorama evolutivo de deputados e senadores, + fundindo as ocorrrências temporais (datas de filiação, desfiliação, cargo e status partidários). + meta: + tags: + - silver + columns: + - name: id_parlamentar + description: Identificador base unificado que representa o político (proveniente da Câmara ou Senado). + - name: chave_join_nome + description: Chave string de junção formatada em caixa alta e conversões 'unaccent' na rotina TRANSLATE projetada para merge rústico. + - name: nome_parlamentar + description: Nome extenso original proveniente da camada bronze agregada. + - name: cargo_parlamentar + description: Atribuição legal do período ('Deputado' ou 'Senador'). + - name: sigla_partido + description: Partido representativo desta dimensão temporal do parlamentar com padronização via mapas canônicos. + - name: uf_parlamentar + description: Sigla da Unidade Federativa. + - name: url_foto + description: Endereço linkado contendo a foto oficial da sub-entidade. + - name: email + description: Endereço de e-mail de correspondência institucional. + - name: data_filiacao + description: Timestamp contendo o marco de data original do início deste ciclo institucional vigente ou pregresso. + - name: data_desfiliacao + description: Timestamp determinando interrupção real do vínculo computada por cálculos de encerramento da sub-plataforma. + - name: id_legislatura + description: Inteiro referenciando o ciclo se existente (normalmente preenchido por Deputados). + - name: situacao + description: Estado textual representativo do cargo (Afastado, Exercício, Licença). + - name: fonte + description: Tracking text de procedência do pipeline primário. + - name: dt_ingest + description: Ponto de captura original no tempo. + - name: url_logo_partido + description: URL resolvida da logomarca do partido associado pelo macro map de partis. + + tests: + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares_historico' + nome_coluna: 'id_parlamentar' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares_historico' + nome_coluna: 'chave_join_nome' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares_historico' + nome_coluna: 'nome_parlamentar' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares_historico' + nome_coluna: 'data_filiacao' + tipo_esperado: 'timestamp with time zone' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares_historico' + nome_coluna: 'data_desfiliacao' + tipo_esperado: 'timestamp with time zone' + - verificacao_tipagem: + nome_tabela: 'dados_abertos.parlamentares_historico' + nome_coluna: 'id_legislatura' + tipo_esperado: 'integer' diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/documentos_habeis.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/documentos_habeis.sql new file mode 100644 index 00000000..f9acd06c --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/documentos_habeis.sql @@ -0,0 +1,33 @@ +{{ config(materialized="table") }} + +with + documentos_habeis_raw as ( + select + id_dh::integer as id_dh, + id_empenho::integer as id_empenho, + numero_documento_habil::text as numero_documento_habil, + situacao_dh::integer as situacao_dh, + descricao_situacao_dh::text as descricao_situacao_dh, + tipo_documento_dh::text as tipo_documento_dh, + ug_emitente_dh::integer as ug_emitente_dh, + descricao_ug_emitente_dh::text as descricao_ug_emitente_dh, + data_vencimento_dh::date as data_vencimento_dh, + data_emissao_dh::date as data_emissao_dh, + ug_pagadora_dh::integer as ug_pagadora_dh, + descricao_ug_pagadora_dh::text as descricao_ug_pagadora_dh, + variacao_patrimonial_diminuta_dh::text as variacao_patrimonial_diminuta_dh, + passivo_transferencia_constitucional_legal_dh::text as passivo_transferencia_constitucional_legal_dh, + centro_custo_empenho::text as centro_custo_empenho, + codigo_siorg_empenho::integer as codigo_siorg_empenho, + mes_referencia_empenho::text as mes_referencia_empenho, + ano_referencia_empenho::integer as ano_referencia_empenho, + ug_beneficiada_dh::integer as ug_beneficiada_dh, + descricao_ug_beneficiada_dh::text as descricao_ug_beneficiada_dh, + valor_dh::numeric(15, 2) as valor_dh, + valor_rateio_dh::numeric(15, 2) as valor_rateio_dh, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transferegov_emendas", "documentos_habeis_especiais") }} + ) -- + +select * +from documentos_habeis_raw diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/empenhos_especiais.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/empenhos_especiais.sql new file mode 100644 index 00000000..62afcacd --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/empenhos_especiais.sql @@ -0,0 +1,38 @@ +{{ config(materialized="table") }} + +with + empenhos_raw as ( + select + id_empenho::integer as id_empenho, + id_minuta_empenho::text as id_minuta_empenho, + numero_empenho::text as numero_empenho, + situacao_empenho::integer as situacao_empenho, + descricao_situacao_empenho::text as descricao_situacao_empenho, + tipo_documento_empenho::integer as tipo_documento_empenho, + descricao_tipo_documento_empenho::text as descricao_tipo_documento_empenho, + status_processamento_empenho::text as status_processamento_empenho, + ug_responsavel_empenho::integer as ug_responsavel_empenho, + ug_emitente_empenho::integer as ug_emitente_empenho, + descricao_ug_emitente_empenho::text as descricao_ug_emitente_empenho, + fonte_recurso_empenho::text as fonte_recurso_empenho, + plano_interno_empenho::text as plano_interno_empenho, + ptres_empenho::numeric(15, 2) as ptres_empenho, -- verificar possibilidade de .0 + grupo_natureza_despesa_empenho::text as grupo_natureza_despesa_empenho, + natureza_despesa_empenho::text as natureza_despesa_empenho, + subitem_empenho::text as subitem_empenho, + categoria_despesa_empenho::text as categoria_despesa_empenho, + modalidade_despesa_empenho::integer as modalidade_despesa_empenho, + cnpj_beneficiario_empenho::text as cnpj_beneficiario_empenho, + nome_beneficiario_empenho::text as nome_beneficiario_empenho, + uf_beneficiario_empenho::text as uf_beneficiario_empenho, + numero_ro_empenho::text as numero_ro_empenho, + data_emissao_empenho::date as data_emissao_empenho, + prioridade_desbloqueio_empenho::integer as prioridade_desbloqueio_empenho, + valor_empenho::numeric(15, 2) as valor_empenho, + id_plano_acao::integer as id_plano_acao, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transferegov_emendas", "empenhos_especiais") }} + ) -- + +select * +from empenhos_raw diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/executor.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/executor.sql new file mode 100644 index 00000000..000ac71d --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/executor.sql @@ -0,0 +1,28 @@ +{{ config(materialized="table") }} + +with + executor_especial_raw as ( + select + id_plano_acao::integer as id_plano_acao, + id_executor::integer as id_executor, + cnpj_executor::text as cnpj_executor, + nome_executor::text as nome_executor, + objeto_executor::text as objeto_executor, + vl_custeio_executor::numeric(15, 2) as valor_custeio_executor, + vl_investimento_executor::numeric(15, 2) as valor_investimento_executor, + ind_recursos_gerenciados_conta_especifica_executor::text as ind_recursos_gerenciados_conta_especifica_executor, + codigo_banco_executor::text as codigo_banco_executor, + nome_banco_executor::text as nome_banco_executor, + nullif(numero_agencia_executor, 'NaN')::numeric::integer as numero_agencia_executor, + dv_agencia_executor::text as dv_agencia_executor, + nome_agencia_executor::text as nome_agencia_executor, + numero_conta_executor::text as numero_conta_executor, + dv_conta_executor::text as dv_conta_executor, + nullif(codigo_situacao_dado_bancario_executor, 'NaN')::numeric::integer as codigo_situacao_dado_bancario_executor, + descricao_situacao_dado_bancario_executor::text as descricao_situacao_dado_bancario_executor, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transferegov_emendas", "executor_especial") }} + ) + +select * +from executor_especial_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/finalidades.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/finalidades.sql new file mode 100644 index 00000000..d1c494f5 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/finalidades.sql @@ -0,0 +1,16 @@ +{{ config(materialized="table") }} + +with + finalidade_especial_raw as ( + select + id_executor::integer as id_executor, + cd_area_politica_publica_tipo_pt::integer as cd_area_politica_publica_tipo_pt, + area_politica_publica_tipo_pt::text as area_politica_publica_tipo_pt, + cd_area_politica_publica_pt::integer as cd_area_politica_publica_pt, + area_politica_publica_pt::text as area_politica_publica_pt, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transferegov_emendas", "finalidades_especiais") }} + ) + +select * +from finalidade_especial_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/historico_pagamentos.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/historico_pagamentos.sql new file mode 100644 index 00000000..e10a98fd --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/historico_pagamentos.sql @@ -0,0 +1,16 @@ +{{ config(materialized="table") }} + +with + historico_pagamentos_raw as ( + select + id_historico_op_ob::integer as id_historico_op_ob, + data_hora_historico_op::timestamp as data_hora_historico_op, + historico_situacao_op::integer as historico_situacao_op, + descricao_historico_situacao_op::text as descricao_historico_situacao_op, + id_op_ob::integer as id_op_ob, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transferegov_emendas", "historico_pagamentos_especiais") }} + ) -- + +select * +from historico_pagamentos_raw diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/metas.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/metas.sql new file mode 100644 index 00000000..580b4df8 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/metas.sql @@ -0,0 +1,27 @@ +{{ config(materialized="table") }} + +with + meta_especial_raw as ( + select + id_executor::integer as id_executor, + id_meta::integer as id_meta, + sequencial_meta::integer as sequencial_meta, + nome_meta::text as nome_meta, + desc_meta::text as desc_meta, + un_medida_meta::text as un_medida_meta, + qt_uniade_meta::numeric(15, 2) as qt_uniade_meta, + vl_custeio_emenda_especial_meta::numeric(15, 2) as valor_custeio_emenda_especial_meta, + vl_investimento_emenda_especial_meta::numeric(15, 2) as valor_investimento_emenda_especial_meta, + vl_custeio_recursos_proprios_meta::numeric(15, 2) as valor_custeio_recursos_proprios_meta, + vl_investimento_recursos_proprios_meta::numeric(15, 2) as valor_investimento_recursos_proprios_meta, + vl_custeio_rendimento_meta::numeric(15, 2) as valor_custeio_rendimento_meta, + vl_investimento_rendimento_meta::numeric(15, 2) as valor_investimento_rendimento_meta, + vl_custeio_doacao_meta::numeric(15, 2) as valor_custeio_doacao_meta, + vl_investimento_doacao_meta::numeric(15, 2) as valor_investimento_doacao_meta, + qt_meses_meta::integer as qt_meses_meta, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transferegov_emendas", "metas_especiais") }} + ) + +select * +from meta_especial_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/ordens_bancarias.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/ordens_bancarias.sql new file mode 100644 index 00000000..8f0bd2c5 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/ordens_bancarias.sql @@ -0,0 +1,24 @@ +{{ config(materialized="table") }} + +with + ordens_bancarias_raw as ( + select + id_op_ob::integer as id_op_ob, + data_emissao_op::date as data_emissao_op, + numero_ordem_pagamento::text as numero_ordem_pagamento, + vinculacao_op::integer as vinculacao_op, + situacao_op::integer as situacao_op, + descricao_situacao_op::text as descricao_situacao_op, + data_situacao_op::date as data_situacao_op, + data_emissao_ob::date as data_emissao_ob, + numero_ordem_bancaria::text as numero_ordem_bancaria, + numero_ordem_lancamento::text as numero_ordem_lancamento, + data_assinatura_ordenador_despesa_ob::date as data_assinatura_ordenador_despesa_ob, + data_assinatura_gestor_financeiro_ob::date as data_assinatura_gestor_financeiro_ob, + id_dh::integer as id_dh, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transferegov_emendas", "ordens_bancarias_especiais") }} + ) -- + +select * +from ordens_bancarias_raw diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/planos_acoes.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/planos_acoes.sql new file mode 100644 index 00000000..74b72cef --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/planos_acoes.sql @@ -0,0 +1,38 @@ +{{ config(materialized="table") }} + +with + planos_raw as ( + select + id_plano_acao::integer as id_plano_acao, + codigo_plano_acao::text as codigo_plano_acao, + ano_plano_acao::integer as ano_plano_acao, + modalidade_plano_acao::text as modalidade_plano_acao, + situacao_plano_acao::text as situacao_plano_acao, + cnpj_beneficiario_plano_acao::text as cnpj_beneficiario_plano_acao, + nome_beneficiario_plano_acao::text as nome_beneficiario_plano_acao, + uf_beneficiario_plano_acao::text as uf_beneficiario_plano_acao, + codigo_banco_plano_acao::text as codigo_banco_plano_acao, + NULLIF(codigo_situacao_dado_bancario_plano_acao::numeric, 'NaN'::numeric)::integer as codigo_situacao_dado_bancario_plano_acao, + nome_banco_plano_acao::text as nome_banco_plano_acao, + NULLIF(numero_agencia_plano_acao::numeric, 'NaN'::numeric)::integer as numero_agencia_plano_acao, + dv_agencia_plano_acao::text as dv_agencia_plano_acao, + NULLIF(numero_conta_plano_acao::numeric, 'NaN'::numeric)::integer as numero_conta_plano_acao, + dv_conta_plano_acao::text as dv_conta_plano_acao, + nome_parlamentar_emenda_plano_acao::text as nome_parlamentar_emenda_plano_acao, + ano_emenda_parlamentar_plano_acao::text as ano_emenda_parlamentar_plano_acao, + codigo_parlamentar_emenda_plano_acao::text as codigo_parlamentar_emenda_plano_acao, + sequencial_emenda_parlamentar_plano_acao::integer as sequencial_emenda_parlamentar_plano_acao, + numero_emenda_parlamentar_plano_acao::text as numero_emenda_parlamentar_plano_acao, + codigo_emenda_parlamentar_formatado_plano_acao::text as codigo_emenda_parlamentar_formatado_plano_acao, + codigo_descricao_areas_politicas_publicas_plano_acao::text as codigo_descricao_areas_politicas_publicas_plano_acao, + descricao_programacao_orcamentaria_plano_acao::text as descricao_programacao_orcamentaria_plano_acao, + motivo_impedimento_plano_acao::text as motivo_impedimento_plano_acao, + valor_custeio_plano_acao::numeric(15, 2) as valor_custeio_plano_acao, + valor_investimento_plano_acao::numeric(15, 2) as valor_investimento_plano_acao, + id_programa::integer as id_programa, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transferegov_emendas", "planos_acao_especiais") }} + ) -- + +select * +from planos_raw diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/planos_trabalho_especial.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/planos_trabalho_especial.sql new file mode 100644 index 00000000..30a38a1b --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/planos_trabalho_especial.sql @@ -0,0 +1,22 @@ +{{ config(materialized="table") }} + +with + planos_trabalho_raw as ( + select + id_plano_trabalho::integer as id_plano_trabalho, + situacao_plano_trabalho::text as situacao_plano_trabalho, + ind_orcamento_proprio_plano_trabalho::text as ind_orcamento_proprio_plano_trabalho, + data_inicio_execucao_plano_trabalho::timestamp as data_inicio_execucao_plano_trabalho, + data_fim_execucao_plano_trabalho::timestamp as data_fim_execucao_plano_trabalho, + prazo_execucao_meses_plano_trabalho::integer as prazo_execucao_meses_plano_trabalho, + id_plano_acao::integer as id_plano_acao, + classificacao_orcamentaria_pt::text as classificacao_orcamentaria_pt, + ind_justificativa_prorrogacao_atraso_pt::boolean as ind_justificativa_prorrogacao_atraso_pt, + ind_justificativa_prorrogacao_paralizacao_pt::boolean as ind_justificativa_prorrogacao_paralizacao_pt, + justificativa_prorrogacao_pt::text as justificativa_prorrogacao_pt, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transferegov_emendas", "plano_trabalho_especial") }} + ) -- + +select * +from planos_trabalho_raw diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/programas.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/programas.sql new file mode 100644 index 00000000..15c0357c --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/programas.sql @@ -0,0 +1,30 @@ +{{ config(materialized="table") }} + +with + programas_raw as ( + select + id_programa::integer as id_programa, + ano_programa::integer as ano_programa, + modalidade_programa::text as modalidade_programa, + codigo_programa::text as codigo_programa, + id_orgao_superior_programa:: integer as id_orgao_superior_programa, + sigla_orgao_programa::text as sigla_orgao_programa, + nome_orgao_programa::text as nome_orgao_programa, + id_unidade_gestora_programa::integer as id_unidade_gestora_programa, + documentos_origem_programa::text as documentos_origem_programa, + id_unidade_orcamentaria_responsavel_programa::integer as id_unidade_orcamentaria_responsavel_programa, + data_inicio_ciencia_programa::date as data_inicio_ciencia_programa, + data_fim_ciencia_programa::date as data_fim_ciencia_programa, + valor_necessidade_financeira_programa::numeric(15, 2) as valor_necessidade_financeira_programa, + valor_total_disponibilizado_programa::numeric(15, 2) as valor_total_disponibilizado_programa, + valor_impedido_programa::numeric(15, 2) as valor_impedido_programa, + valor_a_disponibilizar_programa::numeric(15, 2) as valor_a_disponibilizar_programa, + valor_documentos_habeis_gerados_programa::numeric(15, 2) as valor_documentos_habeis_gerados_programa, + valor_obs_geradas_programa::numeric(15, 2) as valor_obs_geradas_programa, + valor_disponibilidade_atual_programa::numeric(15, 2) as valor_disponibilidade_atual_programa, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transferegov_emendas", "programas_especiais") }} + ) -- + +select * +from programas_raw diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/relatorio_gestao.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/relatorio_gestao.sql new file mode 100644 index 00000000..b9714838 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/relatorio_gestao.sql @@ -0,0 +1,15 @@ +{{ config(materialized="table") }} + +with + relatorio_gestao_raw as ( + select + id_relatorio_gestao::integer as id_relatorio_gestao, + situacao_relatorio_gestao::text as situacao_relatorio_gestao, + parecer_relatorio_gestao::text as parecer_relatorio_gestao, + id_plano_acao::integer as id_plano_acao, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transferegov_emendas", "relatorio_gestao_especial") }} + ) + +select * +from relatorio_gestao_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/relatorio_gestao_novo.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/relatorio_gestao_novo.sql new file mode 100644 index 00000000..06d5cf48 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/relatorio_gestao_novo.sql @@ -0,0 +1,18 @@ +{{ config(materialized="table") }} + +with + relatorio_gestao_novo_raw as ( + select + id_relatorio_gestao_novo::integer as id_relatorio_gestao_novo, + data_e_hora_relatorio_gestao_novo::timestamp as data_e_hora_relatorio_gestao_novo, + tipo_relatorio_gestao_novo::text as tipo_relatorio_gestao_novo, + valor_executado_relatorio_gestao_novo::numeric(15, 2) as valor_executado_relatorio_gestao_novo, + valor_pendente_relatorio_gestao_novo::numeric(15, 2) as valor_pendente_relatorio_gestao_novo, + situacao_relatorio_gestao_novo::text as situacao_relatorio_gestao_novo, + id_plano_acao::integer as id_plano_acao, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transferegov_emendas", "relatorios_gestao_novo_especial") }} + ) + +select * +from relatorio_gestao_novo_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/schema.yml b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/schema.yml new file mode 100644 index 00000000..f04608e2 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/schema.yml @@ -0,0 +1,1138 @@ +version: 2 + +models: + + # Emendas DBT + + ## Bronze + - name: programas + description: > + Tabela com informações sobre programas especiais do TransfereGov relacionados a emendas parlamentares. + Contém dados sobre a execução financeira dos programas, incluindo valores de necessidade financeira, + disponibilização, impedimentos e documentos hábeis gerados. + Os dados são extraídos da tabela programas_especiais do TransfereGov Emendas, com formatação adequada + de valores numéricos e datas. + meta: + tags: + - bronze + columns: + - name: id_programa + description: > + Identificador único do programa no TransfereGov. + - name: ano_programa + description: > + Ano de referência do programa. + - name: modalidade_programa + description: > + Modalidade do programa (ex: ESPECIAL). + - name: codigo_programa + description: > + Código identificador do programa no sistema. + - name: id_orgao_superior_programa + description: > + Identificador do órgão superior responsável pelo programa. + - name: sigla_orgao_programa + description: > + Sigla do órgão responsável pelo programa. + - name: nome_orgao_programa + description: > + Nome completo do órgão responsável pelo programa. + - name: id_unidade_gestora_programa + description: > + Identificador da unidade gestora responsável pelo programa. + - name: documentos_origem_programa + description: > + Documentos de origem associados ao programa. + - name: id_unidade_orcamentaria_responsavel_programa + description: > + Identificador da unidade orçamentária responsável pelo programa. + - name: data_inicio_ciencia_programa + description: > + Data de início de ciência do programa. + - name: data_fim_ciencia_programa + description: > + Data de fim de ciência do programa. + - name: valor_necessidade_financeira_programa + description: > + Valor da necessidade financeira do programa. + - name: valor_total_disponibilizado_programa + description: > + Valor total disponibilizado para o programa. + - name: valor_impedido_programa + description: > + Valor impedido do programa. + - name: valor_a_disponibilizar_programa + description: > + Valor ainda a ser disponibilizado para o programa. + - name: valor_documentos_habeis_gerados_programa + description: > + Valor dos documentos hábeis gerados para o programa. + - name: valor_obs_geradas_programa + description: > + Valor das ordens bancárias geradas para o programa. + - name: valor_disponibilidade_atual_programa + description: > + Valor da disponibilidade financeira atual do programa. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'id_programa' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'ano_programa' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'id_orgao_superior_programa' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'id_unidade_gestora_programa' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'id_unidade_orcamentaria_responsavel_programa' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'data_inicio_ciencia_programa' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'data_fim_ciencia_programa' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'valor_necessidade_financeira_programa' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'valor_total_disponibilizado_programa' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'valor_impedido_programa' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'valor_a_disponibilizar_programa' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'valor_documentos_habeis_gerados_programa' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'valor_obs_geradas_programa' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.programas' + nome_coluna: 'valor_disponibilidade_atual_programa' + tipo_esperado: 'numeric' + + - name: planos_acoes + description: > + Tabela com informações sobre os planos de ação especiais relacionados a emendas parlamentares do TransfereGov. + Contém dados sobre beneficiários, dados bancários, emendas parlamentares, classificação orçamentária, + impedimentos e valores de custeio e investimento. + Os dados são extraídos da tabela planos_acao_especiais do TransfereGov Emendas, com formatação adequada + de valores numéricos. + meta: + tags: + - bronze + columns: + - name: id_plano_acao + description: > + Identificador único do Plano de Ação (PA). + - name: codigo_plano_acao + description: > + Código do Programa concatenado com o ID do Plano de Ação. + - name: ano_plano_acao + description: > + Ano de criação do Plano de Ação. + - name: modalidade_plano_acao + description: > + Modalidade de Transferência do Plano de Ação. + - name: situacao_plano_acao + description: > + Situação do Plano de Ação. + - name: cnpj_beneficiario_plano_acao + description: > + CNPJ – Cadastro Nacional de Pessoa Jurídica do Beneficiário do Plano de Ação. + - name: nome_beneficiario_plano_acao + description: > + Nome do Beneficiário do Plano de Ação. + - name: uf_beneficiario_plano_acao + description: > + Sigla da Unidade de Federação do beneficiário. + - name: codigo_banco_plano_acao + description: > + Código do Banco do PA. + - name: codigo_situacao_dado_bancario_plano_acao + description: > + Código da Situação da Conta Corrente do PA. + - name: nome_banco_plano_acao + description: > + Nome do Banco do PA. + - name: numero_agencia_plano_acao + description: > + Número da Agência Bancária da Conta Corrente do PA. + - name: dv_agencia_plano_acao + description: > + Dígito Verificador da Agência Bancária da Conta Corrente do PA. + - name: numero_conta_plano_acao + description: > + Número da Conta Corrente do PA. + - name: dv_conta_plano_acao + description: > + Dígito Verificador da Conta Corrente do PA. + - name: nome_parlamentar_emenda_plano_acao + description: > + Nome do Parlamentar Autor da Emenda. + - name: ano_emenda_parlamentar_plano_acao + description: > + Ano da Emenda Parlamentar. + - name: codigo_parlamentar_emenda_plano_acao + description: > + Código do Parlamentar Autor da Emenda. + - name: sequencial_emenda_parlamentar_plano_acao + description: > + Sequencial da Emenda por Parlamentar no Ano. + - name: numero_emenda_parlamentar_plano_acao + description: > + Concatenação do Ano, Código e Sequencial do Parlamentar. + - name: codigo_emenda_parlamentar_formatado_plano_acao + description: > + Código Formatado da Emenda Parlamentar. + - name: codigo_descricao_areas_politicas_publicas_plano_acao + description: > + Concatenação dos Códigos e Descrições dos Tipos da Áreas das Políticas Públicas + com os Códigos e Descrições das Áreas das Políticas Públicas. + - name: descricao_programacao_orcamentaria_plano_acao + description: > + Concatenação das Programações Orçamentárias constantes da Lei Orçamentária do ente + beneficiado na qual o recurso será apropriado. + - name: motivo_impedimento_plano_acao + description: > + Motivo do Impedimento do Plano de Ação. + - name: valor_custeio_plano_acao + description: > + Valor Consolidado de Custeio das Emendas Parlamentares do Plano de Ação. + - name: valor_investimento_plano_acao + description: > + Valor Consolidado de Investimento das Emendas Parlamentares do Plano de Ação. + - name: id_programa + description: > + Identificador único do Programa associado ao Plano de Ação. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.planos_acoes' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_acoes' + nome_coluna: 'ano_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_acoes' + nome_coluna: 'codigo_situacao_dado_bancario_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_acoes' + nome_coluna: 'numero_agencia_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_acoes' + nome_coluna: 'numero_conta_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_acoes' + nome_coluna: 'sequencial_emenda_parlamentar_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_acoes' + nome_coluna: 'valor_custeio_plano_acao' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_acoes' + nome_coluna: 'valor_investimento_plano_acao' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_acoes' + nome_coluna: 'id_programa' + tipo_esperado: 'integer' + + - name: empenhos_especiais + description: > + Tabela com informações sobre empenhos (Notas de Empenho) relacionados a emendas parlamentares do TransfereGov. + Contém dados sobre a execução orçamentária dos empenhos, incluindo valores, beneficiários, + classificação orçamentária e situação. + Os dados são extraídos da tabela empenhos_especiais do TransfereGov Emendas, com formatação adequada + de valores numéricos e datas. + meta: + tags: + - bronze + columns: + - name: id_empenho + description: > + Identificador único da Nota de Empenho (NE). + - name: id_minuta_empenho + description: > + Número da Minuta gerado para Nota de Empenho, utiliza o Número Interno e Ano de Emissão. + - name: numero_empenho + description: > + Número da Nota de Empenho gerada e enviada pelo SIAFI (Sistema Integrado de Administração Financeira). + - name: situacao_empenho + description: > + Código da situação da Nota de Empenho. + - name: descricao_situacao_empenho + description: > + Descrição da situação da Nota de Empenho. + - name: tipo_documento_empenho + description: > + Código do tipo da Nota de Empenho. + - name: descricao_tipo_documento_empenho + description: > + Descrição do tipo da Nota de Empenho. + - name: status_processamento_empenho + description: > + Indica o status do processamento em lote da Nota de Empenho. + - name: ug_responsavel_empenho + description: > + Código da Unidade Gestora Responsável da Nota de Empenho. + - name: ug_emitente_empenho + description: > + Código da Unidade Gestora Emitente da Nota de Empenho. + - name: descricao_ug_emitente_empenho + description: > + Nome da Unidade Gestora Emitente da Nota de Empenho. + - name: fonte_recurso_empenho + description: > + Fonte de Recurso da Nota de Empenho no SIAFI (Sistema Integrado de Administração Financeira). + - name: plano_interno_empenho + description: > + Instrumento de planejamento e de acompanhamento da ação planejada, usado como forma de detalhamento desta, + de uso exclusivo de cada Ministério/órgão. Código com até 11 posições alfa-numéricas. + - name: ptres_empenho + description: > + Número do Programa de Trabalho Resumido. + - name: grupo_natureza_despesa_empenho + description: > + Primeiro dígito do Código da Natureza de Despesa no SIAFI. + - name: natureza_despesa_empenho + description: > + Código da Natureza de Despesa no SIAFI (Sistema Integrado de Administração Financeira). + - name: subitem_empenho + description: > + Valor do Sub-item da Natureza de Despesa no SIAFI (Sistema Integrado de Administração Financeira). + - name: categoria_despesa_empenho + description: > + Código da Categoria de Despesa associada à Nota de Empenho. + - name: modalidade_despesa_empenho + description: > + Código da Modalidade de Despesa. + - name: cnpj_beneficiario_empenho + description: > + CNPJ do beneficiário da Nota de Empenho. + - name: nome_beneficiario_empenho + description: > + Nome do beneficiário da Nota de Empenho. + - name: uf_beneficiario_empenho + description: > + Sigla da Unidade da Federação do beneficiário. + - name: numero_ro_empenho + description: > + Número da lista gerado e enviado pelo SIAFI (Sistema Integrado de Administração Financeira). + - name: data_emissao_empenho + description: > + Data de envio ao SIAFI (Sistema Integrado de Administração Financeira), convertida para formato de data padrão. + - name: prioridade_desbloqueio_empenho + description: > + Indicador de prioridade no desbloqueio de recursos. + - name: valor_empenho + description: > + Valor total da Nota de Empenho, formatado como numérico. + - name: id_plano_acao + description: > + Identificador único do Plano de Ação (PA) relacionado ao empenho. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.empenhos_emendas' + nome_coluna: 'id_empenho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.empenhos_emendas' + nome_coluna: 'situacao_empenho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.empenhos_emendas' + nome_coluna: 'tipo_documento_empenho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.empenhos_emendas' + nome_coluna: 'ug_responsavel_empenho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.empenhos_emendas' + nome_coluna: 'ug_emitente_empenho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.empenhos_emendas' + nome_coluna: 'ptres_empenho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.empenhos_emendas' + nome_coluna: 'modalidade_despesa_empenho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.empenhos_emendas' + nome_coluna: 'data_emissao_empenho' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.empenhos_emendas' + nome_coluna: 'prioridade_desbloqueio_empenho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.empenhos_emendas' + nome_coluna: 'valor_empenho' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.empenhos_emendas' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + + - name: ordens_bancarias + description: > + Tabela com informações sobre ordens de pagamento e ordens bancárias relacionadas a emendas parlamentares + do TransfereGov. Contém dados sobre a emissão, situação e assinaturas das ordens de pagamento e bancárias, + incluindo vinculação ao SIAFI e referência ao documento hábil de origem. + Os dados são extraídos da tabela ordem_pagamento_ordem_bancaria_especial do TransfereGov Emendas, + com formatação adequada de datas. + meta: + tags: + - bronze + columns: + - name: id_op_ob + description: > + Identificador único da Operação de Ordem Bancária. + - name: data_emissao_op + description: > + Data da emissão da Ordem de Pagamento. + - name: numero_ordem_pagamento + description: > + Número da Ordem de Pagamento, no formato AAAAOPNNNNNN (ex: "2020OP146800"). + - name: vinculacao_op + description: > + Código da vinculação da Ordem de Pagamento no SIAFI (Padrão: 405). + - name: situacao_op + description: > + Código da situação da Ordem de Pagamento/Bancária. + - name: descricao_situacao_op + description: > + Descrição da situação da Ordem de Pagamento/Bancária. + - name: data_situacao_op + description: > + Data da situação da Ordem de Pagamento/Bancária. + - name: data_emissao_ob + description: > + Data da emissão da Ordem Bancária no SIAFI. + - name: numero_ordem_bancaria + description: > + Número da Ordem Bancária, no formato AAAAOBNNNNNN (ex: "2020OB146800"). + - name: numero_ordem_lancamento + description: > + Número da Nota de Lançamento no sistema, no formato AAAANSNNNNNN (ex: "2020NS146800"). + - name: data_assinatura_ordenador_despesa_ob + description: > + Data de assinatura do Ordenador de Despesa. + - name: data_assinatura_gestor_financeiro_ob + description: > + Data de assinatura do Gestor Financeiro. + - name: id_dh + description: > + Identificador único do Documento Hábil (DH) vinculado à ordem bancária. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.ordens_bancarias' + nome_coluna: 'id_op_ob' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.ordens_bancarias' + nome_coluna: 'data_emissao_op' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.ordens_bancarias' + nome_coluna: 'vinculacao_op' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.ordens_bancarias' + nome_coluna: 'situacao_op' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.ordens_bancarias' + nome_coluna: 'data_situacao_op' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.ordens_bancarias' + nome_coluna: 'data_emissao_ob' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.ordens_bancarias' + nome_coluna: 'data_assinatura_ordenador_despesa_ob' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.ordens_bancarias' + nome_coluna: 'data_assinatura_gestor_financeiro_ob' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.ordens_bancarias' + nome_coluna: 'id_dh' + tipo_esperado: 'integer' + + - name: historico_pagamentos + description: > + Tabela com informações sobre o histórico de eventos das ordens de pagamento relacionadas a emendas + parlamentares do TransfereGov. Contém dados sobre os eventos (situações) ocorridos ao longo do ciclo + de vida de cada ordem de pagamento, incluindo data/hora da ocorrência e descrição do evento. + Os dados são extraídos da tabela historico_pagamento_especial do TransfereGov Emendas. + meta: + tags: + - bronze + columns: + - name: id_historico_op_ob + description: > + Identificador único do registro de histórico da ordem de pagamento/bancária. + - name: data_hora_historico_op + description: > + Data e hora da inserção ou atualização do registro, conforme hora do sistema. + - name: historico_situacao_op + description: > + Código do evento da Ordem de Pagamento. + - name: descricao_historico_situacao_op + description: > + Descrição do evento da Ordem de Pagamento. + - name: id_op_ob + description: > + Identificador único da Operação de Ordem Bancária associada ao histórico. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.historico_pagamentos' + nome_coluna: 'id_historico_op_ob' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.historico_pagamentos' + nome_coluna: 'data_hora_historico_op' + tipo_esperado: 'timestamp without time zone' + - verificacao_tipagem: + nome_tabela: 'emendas.historico_pagamentos' + nome_coluna: 'historico_situacao_op' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.historico_pagamentos' + nome_coluna: 'id_op_ob' + tipo_esperado: 'integer' + + - name: planos_trabalho_especial + description: > + Tabela com informações sobre os planos de trabalho especiais relacionados a emendas parlamentares + do TransfereGov. Contém dados sobre o planejamento de execução, incluindo situação, prazos, + indicadores de orçamento próprio, classificação orçamentária e justificativas de prorrogação. + Os dados são extraídos da tabela plano_trabalho_especial do TransfereGov Emendas. + meta: + tags: + - bronze + columns: + - name: id_plano_trabalho + description: > + Identificador único do Planejamento. + - name: situacao_plano_trabalho + description: > + Identificador da situação do Planejamento. + - name: ind_orcamento_proprio_plano_trabalho + description: > + Indicador de que os recursos do Plano de Ação foram indicados no orçamento próprio do Beneficiário. + - name: data_inicio_execucao_plano_trabalho + description: > + Data de início da execução do Plano de Trabalho Especial. + - name: data_fim_execucao_plano_trabalho + description: > + Data prevista para o fim da execução do Plano de Trabalho Especial com base no prazo de execução. + - name: prazo_execucao_meses_plano_trabalho + description: > + Prazo de execução em meses. + - name: id_plano_acao + description: > + Identificador do Plano de Ação da Proposta. + - name: classificacao_orcamentaria_pt + description: > + Texto com classificação orçamentária da despesa informado quando indicado no orçamento próprio. + - name: ind_justificativa_prorrogacao_atraso_pt + description: > + Indica atraso na liberação dos recursos. + - name: ind_justificativa_prorrogacao_paralizacao_pt + description: > + Indica paralização da execução do objeto. + - name: justificativa_prorrogacao_pt + description: > + Detalhamento da justificativa da prorrogação de prazo. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.planos_trabalho_especial' + nome_coluna: 'id_plano_trabalho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_trabalho_especial' + nome_coluna: 'data_inicio_execucao_plano_trabalho' + tipo_esperado: 'timestamp without time zone' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_trabalho_especial' + nome_coluna: 'data_fim_execucao_plano_trabalho' + tipo_esperado: 'timestamp without time zone' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_trabalho_especial' + nome_coluna: 'prazo_execucao_meses_plano_trabalho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_trabalho_especial' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + + - name: documentos_habeis + description: > + Tabela com informações sobre documentos hábeis relacionados a emendas parlamentares do TransfereGov. + Contém dados sobre a emissão, situação, unidades gestoras envolvidas, valores e referências + orçamentárias dos documentos hábeis vinculados aos empenhos. + Os dados são extraídos da tabela documentos_habeis_especiais do TransfereGov Emendas, + com formatação adequada de valores numéricos e datas. + meta: + tags: + - bronze + columns: + - name: id_dh + description: > + Identificador único do Documento Hábil (DH). + - name: id_empenho + description: > + Identificador único da Nota de Empenho (NE) vinculada ao documento hábil. + - name: numero_documento_habil + description: > + Número do DH no formato YYYYTTNNNNNN (ex: "2020TF000001"). + - name: situacao_dh + description: > + Código da situação do Documento Hábil. + - name: descricao_situacao_dh + description: > + Descrição da situação do Documento Hábil. + - name: tipo_documento_dh + description: > + Código do tipo do Documento Hábil. + - name: ug_emitente_dh + description: > + Código da Unidade Gestora Emitente do Documento Hábil. + - name: descricao_ug_emitente_dh + description: > + Nome da Unidade Gestora Emitente do Documento Hábil. + - name: data_vencimento_dh + description: > + Data de vencimento do Documento Hábil. + - name: data_emissao_dh + description: > + Data de emissão do Documento Hábil. + - name: ug_pagadora_dh + description: > + Código da Unidade de Gestão Pagadora do Documento Hábil. + - name: descricao_ug_pagadora_dh + description: > + Nome da Unidade de Gestão Pagadora do Documento Hábil. + - name: variacao_patrimonial_diminuta_dh + description: > + Variação Patrimonial Diminutiva. + - name: passivo_transferencia_constitucional_legal_dh + description: > + Passivo de Transferência Legal ou Constitucional. + - name: centro_custo_empenho + description: > + Código do Centro de Custo. + - name: codigo_siorg_empenho + description: > + Código SIORG do Centro de Custo. + - name: mes_referencia_empenho + description: > + Mês de referência do Centro de Custo. + - name: ano_referencia_empenho + description: > + Ano de referência do Centro de Custo. + - name: ug_beneficiada_dh + description: > + Código da Unidade Gestora Beneficiada do Documento Hábil. + - name: descricao_ug_beneficiada_dh + description: > + Nome da Unidade Gestora Beneficiada do Documento Hábil. + - name: valor_dh + description: > + Valor do Documento Hábil. Se a Disponibilidade Financeira for menor que o valor do Empenho, + mais de um Documento Hábil pode ser criado. + - name: valor_rateio_dh + description: > + Valor do Rateio. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) em que os dados foram ingeridos da fonte original para a camada raw. + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.documentos_habeis' + nome_coluna: 'id_dh' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.documentos_habeis' + nome_coluna: 'id_empenho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.documentos_habeis' + nome_coluna: 'situacao_dh' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.documentos_habeis' + nome_coluna: 'ug_emitente_dh' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.documentos_habeis' + nome_coluna: 'data_vencimento_dh' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.documentos_habeis' + nome_coluna: 'data_emissao_dh' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.documentos_habeis' + nome_coluna: 'ug_pagadora_dh' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.documentos_habeis' + nome_coluna: 'codigo_siorg_empenho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.documentos_habeis' + nome_coluna: 'ano_referencia_empenho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.documentos_habeis' + nome_coluna: 'ug_beneficiada_dh' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.documentos_habeis' + nome_coluna: 'valor_dh' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.documentos_habeis' + nome_coluna: 'valor_rateio_dh' + tipo_esperado: 'numeric' + + - name: relatorio_gestao + description: > + Tabela que armazena a situação e o parecer final do Relatório de Gestão + vinculado ao Plano de Ação das emendas especiais. + meta: + tags: + - bronze + columns: + - name: id_relatorio_gestao + description: "Identificador Único do Relatorio de Gestão." + - name: situacao_relatorio_gestao + description: "Situação do Relatório de Gestão" + - name: parecer_relatorio_gestao + description: "Parecer do Relatório de Gestão" + - name: id_plano_acao + description: "Identificador Único do Plano de Ação (PA)." + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.relatorio_gestao' + nome_coluna: 'id_relatorio_gestao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.relatorio_gestao' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + + - name: relatorio_gestao_novo + description: > + Tabela que armazena os registros do Novo Relatório de Gestão das Emendas Especiais. + Contém o detalhamento da execução financeira por Plano de Ação, incluindo valores + executados, pendentes e o status de tramitação do relatório. + meta: + tags: + - bronze + columns: + - name: id_relatorio_gestao_novo + description: "Identificador único do registro do novo relatório de gestão." + - name: data_e_hora_relatorio_gestao_novo + description: "Timestamp do momento do registro ou última atualização do relatório." + - name: tipo_relatorio_gestao_novo + description: "Classificação do tipo de relatório enviado pelo executor." + - name: valor_executado_relatorio_gestao_novo + description: "Montante financeiro total que já foi executado e declarado." + - name: valor_pendente_relatorio_gestao_novo + description: "Montante financeiro que ainda aguarda execução ou comprovação." + - name: situacao_relatorio_gestao_novo + description: "Status atual do relatório (ex: Em preenchimento, Enviado, Homologado)." + - name: id_plano_acao + description: "Chave estrangeira que vincula o relatório ao Plano de Ação correspondente." + - name: dt_ingest + description: "Data e hora da ingestão dos dados na camada bronze (UTC-3)." + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.relatorio_gestao_novo' + nome_coluna: 'id_relatorio_gestao_novo' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.relatorio_gestao_novo' + nome_coluna: 'data_e_hora_relatorio_gestao_novo' + tipo_esperado: 'timestamp without time zone' + - verificacao_tipagem: + nome_tabela: 'emendas.relatorio_gestao_novo' + nome_coluna: 'valor_executado_relatorio_gestao_novo' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.relatorio_gestao_novo' + nome_coluna: 'valor_pendente_relatorio_gestao_novo' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.relatorio_gestao_novo' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + + - name: executor + description: > + Tabela com informações detalhadas sobre os executores dos planos de ação das emendas. + Contém dados cadastrais do executor, detalhes do objeto da execução, valores financeiros de custeio + e investimento, além de informações bancárias para a gestão dos recursos. + Os dados são oriundos da API 'executor_especial' do Transferegov. + meta: + tags: + - bronze + columns: + - name: id_plano_acao + description: "Identificador único do Plano de Ação da Proposta." + - name: id_executor + description: "Identificador Único do Executor do Planejamento." + - name: cnpj_executor + description: "CNPJ do ente ou entidade executora." + - name: nome_executor + description: "Nome do Executor." + - name: objeto_executor + description: "Texto detalhado informando o Objeto da Execução." + - name: valor_custeio_executor + description: "Valor de Custeio do objeto a ser atendido pelo executor." + - name: valor_investimento_executor + description: "Valor de Investimento do objeto a ser atendido pelo executor." + - name: ind_recursos_gerenciados_conta_especifica_executor + description: "Indica se os recursos serão gerenciados por conta específica do executor." + - name: codigo_banco_executor + description: "Código da instituição bancária do Executor." + - name: nome_banco_executor + description: "Nome do Banco do Executor." + - name: numero_agencia_executor + description: "Número da Agência Bancária do Executor." + - name: dv_agencia_executor + description: "Dígito Verificador da Agência Bancária." + - name: nome_agencia_executor + description: "Nome da Agência Bancária do Executor." + - name: numero_conta_executor + description: "Número da Conta Bancária do Executor." + - name: dv_conta_executor + description: "Dígito Verificador da Conta Bancária." + - name: codigo_situacao_dado_bancario_executor + description: "Código da Situação da Conta Bancária" + - name: descricao_situacao_dado_bancario_executor + description: "Descrição textual da situação da conta bancária." + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) da ingestão dos dados para a camada bronze." + + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.executor_especial' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.executor_especial' + nome_coluna: 'id_executor' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.executor_especial' + nome_coluna: 'valor_custeio_executor' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.executor_especial' + nome_coluna: 'valor_investimento_executor' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.executor_especial' + nome_coluna: 'numero_agencia_executor' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.executor_especial' + nome_coluna: 'numero_conta_executor' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.executor_especial' + nome_coluna: 'codigo_situacao_dado_bancario_executor' + tipo_esperado: 'integer' + + - name: metas + description: > + Tabela que detalha as metas físicas e financeiras estabelecidas por cada executor. + Contém o planejamento de execução, discriminando valores de emendas, recursos próprios, + rendimentos e doações, além da quantidade física e o tempo estimado (meses) para a meta. + meta: + tags: + - bronze + columns: + - name: id_executor + description: "Identificador Único do Executor do Planejamento." + - name: id_meta + description: "Identificação Única da Meta do Executor." + - name: sequencial_meta + description: "Sequencial numérico usado para ordenação das metas." + - name: nome_meta + description: "Nome ou título resumido da meta." + - name: desc_meta + description: "Descrição detalhada do que será realizado na meta." + - name: un_medida_meta + description: "Unidade de medida da meta." + - name: qt_uniade_meta + description: "Valor de custeio de Emenda Especial." + - name: valor_custeio_emenda_especial_meta + description: "Valor de Investimento de Emenda Especial." + - name: valor_investimento_emenda_especial_meta + description: "Valor de Custeio de Recursos Proprios." + - name: valor_custeio_recursos_proprios_meta + description: "Valor de Investimento de Recursos Proprios." + - name: valor_investimento_recursos_proprios_meta + description: "Valor de investimento aportado como recursos próprios." + - name: valor_custeio_rendimento_meta + description: "Valor de Custeio de Rendimentos." + - name: valor_investimento_rendimento_meta + description: "Valor de investimento originado de rendimentos." + - name: valor_custeio_doacao_meta + description: "Valor de custeio proveniente de doações." + - name: valor_investimento_doacao_meta + description: "Valor de investimento proveniente de doações." + - name: qt_meses_meta + description: "Quantidade de meses prevista para a execução da meta." + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) da ingestão dos dados para a camada bronze." + + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.meta_especial' + nome_coluna: 'id_executor' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.meta_especial' + nome_coluna: 'id_meta' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.meta_especial' + nome_coluna: 'qt_uniade_meta' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.meta_especial' + nome_coluna: 'valor_custeio_emenda_especial_meta' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.meta_especial' + nome_coluna: 'valor_investimento_emenda_especial_meta' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.meta_especial' + nome_coluna: 'valor_custeio_recursos_proprios_meta' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.meta_especial' + nome_coluna: 'valor_investimento_recursos_proprios_meta' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.meta_especial' + nome_coluna: 'qt_meses_meta' + tipo_esperado: 'integer' + + - name: finalidades + description: > + Tabela que detalha as finalidades das emendas especiais, vinculando cada executor às áreas + de políticas públicas beneficiadas. Permite identificar em quais setores (Saúde, Educação, etc.) + o recurso está sendo aplicado conforme a classificação do Transferegov. + meta: + tags: + - bronze + columns: + - name: id_executor + description: "Identificador Único do Executor do Planejamento (Chave de ligação com a tabela de executores)." + - name: cd_area_politica_publica_tipo_pt + description: "Código numérico que identifica o tipo da área de política pública." + - name: area_politica_publica_tipo_pt + description: "Nome descritivo do tipo da área de política pública." + - name: cd_area_politica_publica_pt + description: "Código da área específica de política pública (subdivisão do tipo)." + - name: area_politica_publica_pt + description: "Nome descritivo da área específica de política pública." + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) da ingestão dos dados para a camada bronze." + + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.finalidade_especial' + nome_coluna: 'id_executor' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.finalidade_especial' + nome_coluna: 'cd_area_politica_publica_tipo_pt' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.finalidade_especial' + nome_coluna: 'cd_area_politica_publica_pt' + tipo_esperado: 'integer' + + - name: tg_emendas + description: > + Tabela com informações de execução orçamentária do SIAFI para empenhos do Tesouro + vinculados a emendas parlamentares. Reúne classificações orçamentárias, autor da emenda, + localização (UF e município) e valores agregados de despesas empenhadas, liquidadas e pagas. + Os dados são extraídos da tabela empenhos_tesouro_emendas_parlamentares do SIAFI, com + padronização de datas e valores monetários. + meta: + tags: + - bronze + columns: + - name: emissao_mes + description: "Mês/ano de emissão da nota de empenho (primeiro dia do mês)." + - name: emissao_dia + description: "Data de emissão da nota de empenho." + - name: programa_governo + description: "Código do programa de governo no orçamento." + - name: programa_governo_descricao + description: "Descrição do programa de governo." + - name: acao_governo + description: "Código da ação de governo associada à emenda." + - name: acao_governo_descricao + description: "Descrição da ação de governo associada à emenda." + - name: autor_emendas_orcamento + description: "Identificador do autor da emenda no orçamento." + - name: autor_emendas_orcamento_descricao + description: "Descrição completa do autor da emenda no orçamento, incluindo indicação da emenda." + - name: autor_emendas_orcamento_nome + description: "Nome do autor da emenda em formato Title Case, extraído do texto completo antes da barra." + - name: uf_pt + description: "UF de aplicação da programação orçamentária (PT)." + - name: uf_pt_descricao + description: "Descrição da UF da programação orçamentária." + - name: municipio_pt + description: "Município de aplicação da programação orçamentária." + - name: ne_ccor + description: "Código da NE/CCOR no SIAFI." + - name: ne_num_processo + description: "Número do processo administrativo associado à NE." + - name: ne_info_complementar + description: "Informações complementares cadastradas na NE." + - name: ne_ccor_descricao + description: "Descrição textual da NE/CCOR." + - name: doc_observacao + description: "Observações adicionais registradas no documento SIAFI." + - name: grupo_despesa + description: "Código do grupo de despesa orçamentária." + - name: grupo_despesa_descricao + description: "Descrição do grupo de despesa orçamentária." + - name: natureza_despesa + description: "Código da natureza de despesa." + - name: natureza_despesa_descricao + description: "Descrição da natureza de despesa." + - name: modalidade_aplicacao + description: "Código da modalidade de aplicação." + - name: modalidade_aplicacao_descricao + description: "Descrição da modalidade de aplicação." + - name: ne_ccor_favorecido + description: "Identificador do favorecido na NE/CCOR." + - name: ne_ccor_favorecido_descricao + description: "Nome/descrição do favorecido na NE/CCOR." + - name: ne_ccor_ano_emissao + description: "Ano de emissão da NE/CCOR." + - name: ptres + description: "PTRES associado à despesa da emenda." + - name: localizador_gasto + description: "Código do localizador de gasto associado à programação orçamentária." + - name: localizador_gasto_descricao + description: "Descrição do localizador de gasto." + - name: regiao_pt + description: "Região associada ao programa de trabalho (PT) da emenda." + - name: item_informacao + description: "Código do item de informação orçamentária." + - name: item_informacao_descricao + description: "Descrição do item de informação orçamentária." + - name: despesas_empenhadas + description: "Valor total das despesas empenhadas para a combinação de classificações." + - name: despesas_liquidadas + description: "Valor total das despesas liquidadas para a combinação de classificações." + - name: despesas_pagas + description: "Valor total das despesas pagas para a combinação de classificações." + - name: dt_ingest + description: "Data e hora (UTC-3 Brasília) da ingestão dos dados para a camada bronze." + + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.tg_emendas' + nome_coluna: 'emissao_mes' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.tg_emendas' + nome_coluna: 'emissao_dia' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.tg_emendas' + nome_coluna: 'programa_governo' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.tg_emendas' + nome_coluna: 'grupo_despesa' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.tg_emendas' + nome_coluna: 'modalidade_aplicacao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.tg_emendas' + nome_coluna: 'ne_ccor_ano_emissao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.tg_emendas' + nome_coluna: 'ptres' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.tg_emendas' + nome_coluna: 'despesas_empenhadas' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.tg_emendas' + nome_coluna: 'despesas_liquidadas' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.tg_emendas' + nome_coluna: 'despesas_pagas' + tipo_esperado: 'numeric' diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/tg_emendas.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/tg_emendas.sql new file mode 100644 index 00000000..8a36a013 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/bronze/tg_emendas.sql @@ -0,0 +1,56 @@ +{{ config(materialized="table") }} + +with + tg_emendas_raw as ( + select + {{ target.schema }}.parse_date(emissao_mes) as emissao_mes, + to_date(emissao_dia, 'DD/MM/YYYY') as emissao_dia, + programa_governo::integer as programa_governo, + programa_governo_descricao::text as programa_governo_descricao, + acao_governo::text as acao_governo, + acao_governo_descricao::text as acao_governo_descricao, + autor_emendas_orcamento::text as autor_emendas_orcamento, + autor_emendas_orcamento_descricao::text as autor_emendas_orcamento_descricao, + initcap( + trim( + regexp_replace( + split_part(autor_emendas_orcamento_descricao, '/', 1), + '\s+', + ' ', + 'g' + ) + ) + ) as autor_emendas_orcamento_nome, + localizador_gasto::text as localizador_gasto, + localizador_gasto_descricao::text as localizador_gasto_descricao, + regiao_pt::text as regiao_pt, + uf_pt::text as uf_pt, + uf_pt_descricao::text as uf_pt_descricao, + municipio_pt::text as municipio_pt, + ne_ccor::text as ne_ccor, + ne_num_processo::text as ne_num_processo, + ne_info_complementar::text as ne_info_complementar, + ne_ccor_descricao::text as ne_ccor_descricao, + doc_observacao::text as doc_observacao, + grupo_despesa::integer as grupo_despesa, + grupo_despesa_descricao::text as grupo_despesa_descricao, + natureza_despesa::text as natureza_despesa, + natureza_despesa_descricao::text as natureza_despesa_descricao, + modalidade_aplicacao::integer as modalidade_aplicacao, + modalidade_aplicacao_descricao::text as modalidade_aplicacao_descricao, + ne_ccor_favorecido::text as ne_ccor_favorecido, + ne_ccor_favorecido_descricao::text as ne_ccor_favorecido_descricao, + ne_ccor_ano_emissao::integer as ne_ccor_ano_emissao, + ptres::integer as ptres, + item_informacao::text as item_informacao, + item_informacao_descricao::text as item_informacao_descricao, + {{ parse_financial_value("despesas_empenhadas") }} as despesas_empenhadas, + {{ parse_financial_value("despesas_liquidadas") }} as despesas_liquidadas, + {{ parse_financial_value("despesas_pagas") }} as despesas_pagas, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("siafi", "ne_tesouro_emendas") }} + ) + +select * +from tg_emendas_raw + diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/silver/emendas_partidos.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/silver/emendas_partidos.sql new file mode 100644 index 00000000..b690d490 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/silver/emendas_partidos.sql @@ -0,0 +1,157 @@ +{{ config(materialized='table') }} + +WITH tg_emendas AS ( + SELECT * + FROM {{ ref('tg_emendas') }} +), + +parlamentares_hist AS ( + SELECT * + FROM {{ ref('parlamentares_historico') }} +), + +tg_emendas_tratado AS ( + SELECT + *, + {{ name_formater("autor_emendas_orcamento_nome") }} AS chave_join_nome, + ROW_NUMBER() OVER () as emenda_id + FROM tg_emendas +), + +cruzamento_bruto AS ( + SELECT + e.emissao_mes, + e.emissao_dia, + e.programa_governo AS codigo_programa, + e.programa_governo_descricao AS programa, + e.acao_governo AS codigo_acao_ajustada, + e.acao_governo_descricao AS acao_ajustada, + e.autor_emendas_orcamento_descricao, + e.localizador_gasto AS localizador_gasto, + e.localizador_gasto_descricao AS localizador_gasto_descricao, + e.regiao_pt AS regiao_pt, + e.uf_pt AS uf, + e.uf_pt_descricao AS uf_descricao, + e.municipio_pt AS municipio, + 'Brasil' AS pais, + e.ne_ccor, + e.ne_num_processo, + e.ne_info_complementar, + e.ne_ccor_descricao, + e.doc_observacao, + e.grupo_despesa AS codigo_gnd, + e.grupo_despesa_descricao AS gnd, + e.natureza_despesa, + e.natureza_despesa_descricao, + e.modalidade_aplicacao AS codigo_modalidade, + e.modalidade_aplicacao_descricao AS modalidade, + e.ne_ccor_favorecido, + e.ne_ccor_favorecido_descricao, + e.ne_ccor_ano_emissao, + e.ptres, + e.item_informacao, + e.item_informacao_descricao, + e.despesas_empenhadas, + e.despesas_liquidadas, + e.despesas_pagas, + e.autor_emendas_orcamento_nome, + + e.emenda_id, + + p.id_parlamentar as id_autor, + p.cargo_parlamentar as cargo_autor, + p.nome_parlamentar as autor, + p.sigla_partido as partido, + p.uf_parlamentar as uf_autor, + p.url_foto as url_foto_autor, + p.email as email_autor, + p.url_logo_partido as url_foto_partido, + + e.dt_ingest, + + -- Prioridade de cruzamento + CASE + WHEN e.emissao_dia >= p.data_filiacao::date + AND e.emissao_dia <= COALESCE(p.data_desfiliacao::date, CURRENT_DATE) + THEN 1 + -- Se achou nome, mas a data não bateu + WHEN p.id_parlamentar IS NOT NULL + THEN 2 + -- Nomes que nem existem + ELSE 3 + END as prioridade_match, + + -- Distância de fallback para quando não tivermos batido o range + LEAST( + ABS(EXTRACT(EPOCH FROM (e.emissao_dia::timestamptz - p.data_filiacao))), + ABS(EXTRACT(EPOCH FROM (e.emissao_dia::timestamptz - COALESCE(p.data_desfiliacao, CURRENT_TIMESTAMP)))) + ) AS distancia_tempo + + FROM tg_emendas_tratado e + LEFT JOIN parlamentares_hist p + ON e.chave_join_nome = p.chave_join_nome +), + +deduplicado AS ( + SELECT * + FROM ( + SELECT *, + ROW_NUMBER() OVER ( + PARTITION BY emenda_id + ORDER BY + prioridade_match ASC, + distancia_tempo ASC + ) as rn + FROM cruzamento_bruto + ) sub + WHERE rn = 1 +) + +SELECT + emissao_mes, + emissao_dia, + codigo_programa, + programa, + codigo_acao_ajustada, + acao_ajustada, + autor_emendas_orcamento_descricao, + autor_emendas_orcamento_nome, + localizador_gasto, + localizador_gasto_descricao, + regiao_pt, + uf, + uf_descricao, + municipio, + pais, + ne_ccor, + ne_num_processo, + ne_info_complementar, + ne_ccor_descricao, + doc_observacao, + codigo_gnd, + gnd, + natureza_despesa, + natureza_despesa_descricao, + codigo_modalidade, + modalidade, + ne_ccor_favorecido, + ne_ccor_favorecido_descricao, + ne_ccor_ano_emissao, + ptres, + item_informacao, + item_informacao_descricao, + despesas_empenhadas, + despesas_liquidadas, + despesas_pagas, + + id_autor, + cargo_autor, + autor, + partido, + uf_autor, + url_foto_autor, + email_autor, + url_foto_partido, + + dt_ingest +FROM deduplicado \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/silver/planos_partidos.sql b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/silver/planos_partidos.sql new file mode 100644 index 00000000..c08d2580 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/silver/planos_partidos.sql @@ -0,0 +1,96 @@ +{{ config(materialized='table') }} + +WITH bronze_planos_acoes AS ( + SELECT * FROM {{ ref('planos_acoes') }} +), + +bronze_deputados AS ( + SELECT * FROM {{ ref('deputados') }} +), + +bronze_senadores AS ( + SELECT * FROM {{ ref('senadores') }} +), + +parlamentares_unificados AS ( + SELECT + id AS id_parlamentar, + TRIM(UPPER(nome)) AS chave_join_nome, + nome AS nome_parlamentar, + 'Deputado' AS cargo_parlamentar, + siglapartido AS sigla_partido, + siglauf AS uf_parlamentar, + urlfoto AS url_foto, + email + FROM bronze_deputados + + UNION ALL + + SELECT + id AS id_parlamentar, + TRIM(UPPER(nome_parlamentar)) AS chave_join_nome, + nome_parlamentar AS nome_parlamentar, + 'Senador' AS cargo_parlamentar, + sigla_partido AS sigla_partido, + uf AS uf_parlamentar, + url_foto AS url_foto, + email + FROM bronze_senadores +), + +planos_acoes_tratado AS ( + SELECT + *, + TRIM(UPPER(nome_parlamentar_emenda_plano_acao)) AS chave_join_nome + FROM bronze_planos_acoes +), + +final AS ( + SELECT + -- Todas as features de Planos de Ação + pa.id_plano_acao, + pa.codigo_plano_acao, + pa.ano_plano_acao, + pa.modalidade_plano_acao, + pa.situacao_plano_acao, + pa.cnpj_beneficiario_plano_acao, + pa.nome_beneficiario_plano_acao, + pa.uf_beneficiario_plano_acao, + pa.codigo_banco_plano_acao, + pa.codigo_situacao_dado_bancario_plano_acao, + pa.nome_banco_plano_acao, + pa.numero_agencia_plano_acao, + pa.dv_agencia_plano_acao, + pa.numero_conta_plano_acao, + pa.dv_conta_plano_acao, + pa.nome_parlamentar_emenda_plano_acao, + pa.ano_emenda_parlamentar_plano_acao, + pa.codigo_parlamentar_emenda_plano_acao, + pa.sequencial_emenda_parlamentar_plano_acao, + pa.numero_emenda_parlamentar_plano_acao, + pa.codigo_emenda_parlamentar_formatado_plano_acao, + pa.codigo_descricao_areas_politicas_publicas_plano_acao, + pa.descricao_programacao_orcamentaria_plano_acao, + pa.motivo_impedimento_plano_acao, + pa.valor_custeio_plano_acao, + pa.valor_investimento_plano_acao, + pa.id_programa, + + -- Features unificadas dos Parlamentares + parl.id_parlamentar, + parl.cargo_parlamentar, + parl.nome_parlamentar, + parl.sigla_partido, + parl.uf_parlamentar, + parl.url_foto, + parl.email, + + -- Data de ingestão (mantendo a da tabela fato) + pa.dt_ingest + + FROM planos_acoes_tratado pa + LEFT JOIN parlamentares_unificados parl + ON pa.chave_join_nome = parl.chave_join_nome +) + +SELECT * FROM final \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/emendas_dbt/silver/schema.yml b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/silver/schema.yml new file mode 100644 index 00000000..ecf8a240 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/emendas_dbt/silver/schema.yml @@ -0,0 +1,321 @@ +version: 2 + +models: + + - name: planos_partidos + description: > + Tabela silver que integra as informações dos planos de ação especiais relacionados a emendas + parlamentares do TransfereGov com metadados de parlamentares (deputados e senadores). + A partir da tabela bronze planos_acoes e das bases de deputados e senadores, enriquece cada + plano de ação com dados de identificação do parlamentar, partido, UF, foto e e-mail. + meta: + tags: + - silver + columns: + # Colunas de planos de ação (herdadas da camada bronze) + - name: id_plano_acao + description: > + Identificador único do Plano de Ação (PA). + - name: codigo_plano_acao + description: > + Código do Programa concatenado com o ID do Plano de Ação. + - name: ano_plano_acao + description: > + Ano de criação do Plano de Ação. + - name: modalidade_plano_acao + description: > + Modalidade de Transferência do Plano de Ação. + - name: situacao_plano_acao + description: > + Situação do Plano de Ação. + - name: cnpj_beneficiario_plano_acao + description: > + CNPJ – Cadastro Nacional de Pessoa Jurídica do Beneficiário do Plano de Ação. + - name: nome_beneficiario_plano_acao + description: > + Nome do Beneficiário do Plano de Ação. + - name: uf_beneficiario_plano_acao + description: > + Sigla da Unidade de Federação do beneficiário. + - name: codigo_banco_plano_acao + description: > + Código do Banco do Plano de Ação. + - name: codigo_situacao_dado_bancario_plano_acao + description: > + Código da Situação da Conta Corrente do Plano de Ação. + - name: nome_banco_plano_acao + description: > + Nome do Banco do Plano de Ação. + - name: numero_agencia_plano_acao + description: > + Número da Agência Bancária da Conta Corrente do Plano de Ação. + - name: dv_agencia_plano_acao + description: > + Dígito Verificador da Agência Bancária da Conta Corrente do Plano de Ação. + - name: numero_conta_plano_acao + description: > + Número da Conta Corrente do Plano de Ação. + - name: dv_conta_plano_acao + description: > + Dígito Verificador da Conta Corrente do Plano de Ação. + - name: nome_parlamentar_emenda_plano_acao + description: > + Nome do Parlamentar Autor da Emenda vinculado ao Plano de Ação. + - name: ano_emenda_parlamentar_plano_acao + description: > + Ano da Emenda Parlamentar associada ao Plano de Ação. + - name: codigo_parlamentar_emenda_plano_acao + description: > + Código do Parlamentar Autor da Emenda. + - name: sequencial_emenda_parlamentar_plano_acao + description: > + Sequencial da Emenda por Parlamentar no Ano. + - name: numero_emenda_parlamentar_plano_acao + description: > + Concatenação do Ano, Código e Sequencial do Parlamentar, formando o identificador da emenda. + - name: codigo_emenda_parlamentar_formatado_plano_acao + description: > + Código formatado da Emenda Parlamentar associada ao Plano de Ação. + - name: codigo_descricao_areas_politicas_publicas_plano_acao + description: > + Concatenação dos códigos e descrições dos tipos das áreas de políticas públicas + com os códigos e descrições das áreas das políticas públicas. + - name: descricao_programacao_orcamentaria_plano_acao + description: > + Concatenação das programações orçamentárias constantes da Lei Orçamentária do ente + beneficiado na qual o recurso será apropriado. + - name: motivo_impedimento_plano_acao + description: > + Motivo do impedimento do Plano de Ação, quando existente. + - name: valor_custeio_plano_acao + description: > + Valor consolidado de custeio das emendas parlamentares do Plano de Ação. + - name: valor_investimento_plano_acao + description: > + Valor consolidado de investimento das emendas parlamentares do Plano de Ação. + - name: id_programa + description: > + Identificador único do Programa associado ao Plano de Ação. + # Colunas de metadados dos parlamentares + - name: id_parlamentar + description: > + Identificador único do parlamentar na base unificada de deputados e senadores. + - name: cargo_parlamentar + description: > + Cargo do parlamentar responsável pela emenda (por exemplo, Deputado ou Senador). + - name: nome_parlamentar + description: > + Nome do parlamentar conforme registrado na base de origem (Câmara dos Deputados ou Senado Federal). + - name: sigla_partido + description: > + Sigla do partido ao qual o parlamentar está filiado. + - name: uf_parlamentar + description: > + Sigla da Unidade Federativa (UF) que o parlamentar representa. + - name: url_foto + description: > + URL da foto oficial do parlamentar na base de origem. + - name: email + description: > + Endereço de e-mail institucional do parlamentar. + - name: dt_ingest + description: > + Data e hora (UTC-3 Brasília) mais recente de ingestão dos dados das tabelas fonte utilizadas neste modelo. + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.planos_partidos' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_partidos' + nome_coluna: 'ano_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_partidos' + nome_coluna: 'codigo_situacao_dado_bancario_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_partidos' + nome_coluna: 'numero_agencia_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_partidos' + nome_coluna: 'numero_conta_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_partidos' + nome_coluna: 'sequencial_emenda_parlamentar_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_partidos' + nome_coluna: 'valor_custeio_plano_acao' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_partidos' + nome_coluna: 'valor_investimento_plano_acao' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_partidos' + nome_coluna: 'id_programa' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_partidos' + nome_coluna: 'id_parlamentar' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.planos_partidos' + nome_coluna: 'dt_ingest' + tipo_esperado: 'timestamp with time zone' + + - name: emendas_partidos + description: > + Tabela silver que enriquece os dados de emendas parlamentares do SIAFI-Tesouro com informações + cadastrais completas e históricas dos respectivos parlamentares e seus logotipos de partido. + Para a integração, avalia o nome com fallback temporal em relação ao momento de apresentação da emenda, + garantindo precisão na identificação de partido e cenário institucional à época. + meta: + tags: + - silver + columns: + - name: emissao_mes + description: Mês de emissão referencial da emenda orçamentária. + - name: emissao_dia + description: Data pontual exata de emissão referencial. + - name: codigo_programa + description: Código orçamentário numérico do programa de governo. + - name: programa + description: Descritivo do escopo do programa de governo que abrange o recurso. + - name: codigo_acao_ajustada + description: Código indexado da ação ajustada governamental. + - name: acao_ajustada + description: Descrição da ação de governo orçamentária que recebe o numerário. + - name: autor_emendas_orcamento_descricao + description: Informação bruta contendo a descrição completa do autor original contido no SIAFI. + - name: autor_emendas_orcamento_nome + description: Parte explícita do nome do autor destrinchado limpo no bronze, alvo primário de ligação. + - name: uf + description: Unidade federativa favorecida no destino dos recursos contidos na emenda. + - name: uf_descricao + description: Nome legível em extenso descritivo da UF. + - name: municipio + description: Localidade alvo municipal receptora contida. + - name: pais + description: Região país consolidada constante ('Brasil'). + - name: ne_ccor + description: Identificador de emissão originária gerando a conta corrente da Nota de Empenho (NE CCOR). + - name: ne_num_processo + description: Protocolo SEI base ou número rigoroso do processo público gerador original atrelado. + - name: ne_info_complementar + description: Repasse de dados textuais avulsos e complementares no interior descritivo da emenda. + - name: ne_ccor_descricao + description: Descritivo literal da Conta Corrente. + - name: doc_observacao + description: Comentários e observações de cadastro adicionadas ao corpo do documento governamental transacionado. + - name: codigo_gnd + description: Numérico estrito do Grupo de Natureza de Despesa. + - name: gnd + description: Rotulação formal designando a subfamília do Grupo de Natureza de Despesa onde os gastos repousam. + - name: natureza_despesa + description: Numérico formal indicativo da sub-natureza atrelada à despesa gerada. + - name: natureza_despesa_descricao + description: Descritivo em linguagem verbal detalhando a natureza final onde foi fixada e chancelada o repasse monetário. + - name: codigo_modalidade + description: Subcódigo identificador numérico atrelado à categoria sistêmica Modalidade de Aplicação. + - name: modalidade + description: Texto explícito definindo o perfil estrutural da finalidade e modalidade estritamente avaliadas desta verba. + - name: ne_ccor_favorecido + description: CPF ou credencial CNPJ de cunho privado/institucional validada do ente favorecido terminal da execução. + - name: ne_ccor_favorecido_descricao + description: Razão social declarada da entidade que irá usufruir e receber do orçamento. + - name: ne_ccor_ano_emissao + description: Marcador de referência em representação anual do dia inicial do empenho do tesouro reportado. + - name: ptres + description: Programa de Trabalho Resumido - indicador consolidado contábil da União no sistema do SIAFI. + - name: localizador_gasto + description: Código do localizador de gasto associado à programação orçamentária. + - name: localizador_gasto_descricao + description: Descrição do localizador de gasto. + - name: regiao_pt + description: Região associada ao programa de trabalho (PT) da emenda. + - name: item_informacao + description: Hash ou item identificatório especial acoplado das métricas adicionais requeridas pelo processo contábil. + - name: item_informacao_descricao + description: Especificidade textual legível para auditoria relacionada fundamentalmente ao "item informação" do registro. + - name: despesas_empenhadas + description: Limite de fundos transacionados com base monetária ou saldo reservado à entidade recebedora na etapa de empenho. + - name: despesas_liquidadas + description: Montante e liquidez monetária contendo aprovação de entrega ou contraprestação referencial. + - name: despesas_pagas + description: Transferência monetária e emulação do recurso que se concretizou no processo de desembolso bancário explícito ao fornecedor. + - name: id_autor + description: Chave estrangeira extraída derivando-se da base histórica associatória dos parlamentares unificados. + - name: cargo_autor + description: Papel representacional (Senador, Deputado) assumido no ato em que emendou a peça pelo resgate de proximidade cronológica. + - name: autor + description: Nome purificado e resoluto encontrado na rede e camada silver para aquele político atuante. + - name: partido + description: Filiação ideológico-partidária oficial extraída da avaliação condicional temporal apurada ao tempo em que o documento subiu. + - name: uf_autor + description: Limite espacial contínuo estadual ou UF representativa que elegeu o ator governamental. + - name: url_foto_autor + description: Conexão estática direcionada vinculando o perfil institucional pictográfico extraído formalmente da web. + - name: email_autor + description: Espelho de conexão funcional do servidor federal que gerencia as ordens por mensagens do parlamento alvo. + - name: url_foto_partido + description: Logotipo e identidade diagramada iconográfica associada à sua chapa ou estandarte governamental daquele tempo exato. + - name: dt_ingest + description: Tracking text ou espelho de captura temporal da engrenagem do job (Airflow ETL) no formato padrão de UTC-3 timestamp timezone. + + tests: + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'emissao_mes' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'emissao_dia' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'codigo_programa' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'codigo_gnd' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'codigo_modalidade' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'ne_ccor_ano_emissao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'ptres' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'despesas_empenhadas' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'despesas_liquidadas' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'despesas_pagas' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'id_autor' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'autor' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'emendas.emendas_partidos' + nome_coluna: 'pais' + tipo_esperado: 'text' diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/empenhos_tesouro_ted.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/empenhos_tesouro_ted.sql new file mode 100644 index 00000000..f8605312 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/empenhos_tesouro_ted.sql @@ -0,0 +1,37 @@ +{{ config(materialized="table") }} + + +with + empenhos_teds_raw as ( + select + programa_governo::text as programa_governo, + programa_governo_descricao::text as programa_governo_descricao, + acao_governo::text as acao_governo, + acao_governo_descricao::text as acao_governo_descricao, + emissao_mes::text as emissao_mes, + emissao_dia::text as emissao_dia, + ne_ccor::text as ne_ccor, + regexp_replace(ne_num_processo, '[./-]', '', 'g') as ne_num_processo, + ne_info_complementar::text as ne_info_complementar, + ne_ccor_descricao::text as ne_ccor_descricao, + doc_observacao::text as doc_observacao, + natureza_despesa::text as natureza_despesa, + natureza_despesa_descricao::text as natureza_despesa_descricao, + upper(ne_ccor_favorecido::text) as ne_ccor_favorecido, + ne_ccor_favorecido_descricao::text as ne_ccor_favorecido_descricao, + ne_ccor_ano_emissao::integer as ne_ccor_ano_emissao, + ptres::text as ptres, + fonte_recursos_detalhada::text as fonte_recursos_detalhada, + fonte_recursos_detalhada_descricao::text as fonte_recursos_detalhada_descricao, + {{ parse_financial_value("despesas_empenhadas") }} as despesas_empenhadas, + {{ parse_financial_value("despesas_liquidadas") }} as despesas_liquidadas, + {{ parse_financial_value("despesas_pagas") }} as despesas_pagas, + {{ parse_financial_value("restos_a_pagar_inscritos") }} as restos_a_pagar_inscritos, + {{ parse_financial_value("restos_a_pagar_pagos") }} as restos_a_pagar_pagos, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("siafi", "ne_tesouro") }} + where ne_ccor_ano_emissao ~ '^[0-9]{4}$' + ) + +select * +from empenhos_teds_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/nc_tesouro_mir.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/nc_tesouro_mir.sql new file mode 100644 index 00000000..ae9f776c --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/nc_tesouro_mir.sql @@ -0,0 +1,96 @@ +{{ config(materialized='table')}} + +with + + notas_credito_pre as ( + select + programa_governo, + programa_governo_descricao, + acao_governo, + acao_governo_descricao, + + nc, + nc_transferencia, + nc_fonte_recursos, + nc_fonte_recursos_descricao, + ptres, + nc_evento_descricao, + nc_ug_responsavel, + nc_ug_responsavel_descricao, + nc_natureza_despesa, + nc_natureza_despesa_descricao, + nc_plano_interno, + nc_plano_interno_descricao1, + favorecido_doc, + favorecido_doc_descricao, + + favorecido_municipio, + favorecido_municipio_descricao, + + {{parse_financial_value("nc_valor_linha")}} as valor_celula, + {{parse_financial_value("movimento_liquido_moeda_origem")}} as movimento_liquido_moeda_origem, + + (dt_ingest || '-03:00')::timestamptz as dt_ingest, + + cast(null as varchar) as descricao, + nc_plano_interno_descricao2, + nc_evento, + cast(null as varchar) as nc_item_detalhamento, + cast(null as date) as emissao_dia, + cast(null as varchar) as emissao_mes, + cast(null as varchar) as emissao_ano, + cast(null as varchar) as ro, + cast(null as varchar) as dc, + cast(null as numeric) as total_lista, + cast(null as varchar) as esfera_orcamentaria_codigo, + cast(null as varchar) as esfera_orcamentaria_nome + from {{ source("siafi", "nc_tesouro_pre_2026") }} + ), + notas_credito_pos as ( + select + -- campos nulos: + cast(null as varchar) as programa_governo, + cast(null as varchar) as programa_governo_descricao, + cast(null as varchar) as acao_governo, + cast(null as varchar) as acao_governo_descricao, + + nc, + nc_transferencia, + fonte_codigo as nc_fonte_recursos, + fonte_nome as nc_fonte_recursos_descricao, + ptres, + tipo_nc as nc_evento_descricao, + emitente_codigo as nc_ug_responsavel, + emitente_nome as nc_ug_responsavel_descricao, + gnd_codigo as nc_natureza_despesa, + gnd_nome as nc_natureza_despesa_descricao, + pi_codigo as nc_plano_interno, + pi_nome as nc_plano_interno_descricao1, + favorecido_codigo as nc_favorecido_doc, + favorecido_nome as nc_favorecido_doc_descricao, + + cast(null as varchar) as favorecido_municipio, + cast(null as varchar) as favorecido_municipio_descricao, + + {{parse_financial_value("valor_celula")}} as nc_valor_linha, + {{parse_financial_value("total_lista")}} as movimento_liquido_moeda_origem, + (dt_ingest || '-03:00')::timestamptz as dt_ingest, + + descricao, + cast(null as varchar)as nc_plano_interno_descricao2, + cast(null as varchar)as nc_evento, + nc_item_detalhamento, + to_date(emissao_dia, 'DD/MM/YYYY') as emissao_dia, + emissao_mes, + emissao_ano, + ro, + dc, + {{parse_financial_value("total_lista")}} as total_lista, + esfera_orcamentaria_codigo, + esfera_orcamentaria_nome + from {{ source("siafi", "nc_tesouro_pos__2026") }} + ) + +select * from notas_credito_pre +union all +select * from notas_credito_pos diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/notas_de_credito.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/notas_de_credito.sql new file mode 100644 index 00000000..3b0f4601 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/notas_de_credito.sql @@ -0,0 +1,23 @@ +{{ config(materialized="table") }} + + +with + notas_de_credito_raw as ( + select + id_nota::integer as id_nota, + id_plano_acao::integer as id_plano_acao, + tx_minuta_nota::text as tx_minuta_nota, + tx_numero_nota::text as tx_numero_nota, + dt_emissao_nota::timestamp as dt_emissao_nota, + cd_gestao_emitente_nota::text as cd_gestao_emitente_nota, + cd_gestao_favorecida_nota::text as cd_gestao_favorecida_nota, + tx_situacao_nota::text as tx_situacao_nota, + cd_ug_emitente_nota::text as cd_ug_emitente_nota, + cd_ug_favorecida_nota::text as cd_ug_favorecida_nota, + tx_observacao_nota::text as tx_observacao_nota, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transfere_gov", "notas_de_credito") }} + ) + +select * +from notas_de_credito_raw diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/pf_ptres.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/pf_ptres.sql new file mode 100644 index 00000000..79466999 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/pf_ptres.sql @@ -0,0 +1,29 @@ +{{ config(materialized="table", alias="pf_ptres_mir") }} + + +with + pf_ptres_raw as ( + select + programa_governo::text as programa_governo, + programa_governo_descricao::text as programa_governo_descricao, + plano_orcamentario::text as plano_orcamentario, + plano_orcamentario_descricao_1::text as plano_orcamentario_descricao_1, + plano_orcamentario_descricao_2::text as plano_orcamentario_descricao_2, + plano_orcamentario_descricao_3::text as plano_orcamentario_descricao_3, + plano_orcamentario_descricao_4::text as plano_orcamentario_descricao_4, + plano_orcamentario_descricao_5::text as plano_orcamentario_descricao_5, + plano_orcamentario_descricao_6::text as plano_orcamentario_descricao_6, + acao_governo::text as acao_governo, + acao_governo_descricao::text as acao_governo_descricao, + ptres::text as ptres, + natureza_despesa::text as natureza_despesa, + natureza_despesa_descricao::text as natureza_despesa_descricao, + {{ parse_financial_value("dotacao_inicial") }} as dotacao_inicial, + {{ parse_financial_value("dotacao_suplementar") }} as dotacao_suplementar, + {{ parse_financial_value("dotacao_atualizada") }} as dotacao_atualizada, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("siafi", "programacao_acao_ptres") }} + ) + +select * +from pf_ptres_raw diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/pf_tesouro.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/pf_tesouro.sql new file mode 100644 index 00000000..e906d57d --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/pf_tesouro.sql @@ -0,0 +1,28 @@ +{{ config(materialized="table", alias="pf_tesouro_mir") }} + + +with + pf_tesouro_raw as ( + select + emissao_mes::text as emissao_mes, + to_date(emissao_dia, 'DD/MM/YYYY') as emissao_dia, + ug_emitente::text as ug_emitente, + ug_emitente_descricao::text as ug_emitente_descricao, + ug_favorecido::text as ug_favorecido, + ug_favorecido_descricao::text as ug_favorecido_descricao, + pf_evento::text as pf_evento, + pf_evento_descricao::text as pf_evento_descricao, + pf::text as pf, + pf_inscricao::text as pf_inscricao, + pf_acao::text as pf_acao, + pf_acao_descricao::text as pf_acao_descricao, + pf_fonte_recursos::text as pf_fonte_recursos, + pf_fonte_recursos_descricao::text as pf_fonte_recursos_descricao, + doc_observacao::text as doc_observacao, + {{ parse_financial_value("pf_valor_linha") }} as pf_valor_linha, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("siafi", "pf_tesouro") }} + ) + +select * +from pf_tesouro_raw diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/pf_transfere.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/pf_transfere.sql new file mode 100644 index 00000000..17fd5502 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/pf_transfere.sql @@ -0,0 +1,22 @@ +{{ config(materialized="table", alias="pf_transfere_mir") }} + + +with + pf_transfere_raw as ( + select + id_programacao::integer as id_programacao, + id_plano_acao::integer as id_plano_acao, + tp_pf_tipo_programacao::text as tp_pf_tipo_programacao, + tx_minuta_programacao::text as tx_minuta_programacao, + tx_numero_programacao::text as tx_numero_programacao, + tx_situacao_programacao::text as tx_situacao_programacao, + tx_observacao_programacao::text as tx_observacao_programacao, + ug_emitente_programacao::text as ug_emitente_programacao, + ug_favorecida_programacao::text as ug_favorecida_programacao, + dh_recebimento_programacao::timestamp as dh_recebimento_programacao, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transfere_gov", "programacao_financeira") }} + ) + +select * +from pf_transfere_raw diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/planos_acao_ted.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/planos_acao_ted.sql new file mode 100644 index 00000000..fda249e3 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/planos_acao_ted.sql @@ -0,0 +1,31 @@ +{{ config(materialized="table") }} + +with + planos_acao_raw as ( + select + id_plano_acao::integer as id_plano_acao, + id_programa::integer as id_programa, + sigla_unidade_descentralizada::text as sigla_unidade_descentralizada, + unidade_descentralizada::text as unidade_descentralizada, + sigla_unidade_responsavel_execucao::text as sigla_unidade_responsavel_execucao, + unidade_responsavel_execucao::text as unidade_responsavel_execucao, + nullif(vl_total_plano_acao, '')::numeric(15, 2) as vl_total_plano_acao, + nullif(dt_inicio_vigencia, '')::timestamp::date as dt_inicio_vigencia, + nullif(dt_fim_vigencia, '')::timestamp::date as dt_fim_vigencia, + tx_objeto_plano_acao::text as tx_objeto_plano_acao, + tx_justificativa_plano_acao::text as tx_justificativa_plano_acao, + nullif(in_forma_execucao_direta, '')::boolean as in_forma_execucao_direta, + nullif(in_forma_execucao_particulares, '')::boolean as in_forma_execucao_particulares, + nullif(in_forma_execucao_descentralizada, '')::boolean as in_forma_execucao_descentralizada, + tx_situacao_plano_acao::text as tx_situacao_plano_acao, + nullif(aa_ano_plano_acao, '')::integer as aa_ano_plano_acao, + nullif(vl_beneficiario_especifico, '')::numeric(15, 2) as vl_beneficiario_especifico, + nullif(vl_chamamento_publico, '')::numeric(15, 2) as vl_chamamento_publico, + sq_instrumento::text as sq_instrumento, + nullif(aa_instrumento, '')::integer as aa_instrumento, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transfere_gov", "planos_acao") }} + ) + +select * +from planos_acao_raw diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/programas_ted.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/programas_ted.sql new file mode 100644 index 00000000..ae9319b4 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/programas_ted.sql @@ -0,0 +1,39 @@ +{{ config(materialized="table") }} + +with + programas_raw as ( + select + id_programa::integer as id_programa, + tx_codigo_programa::text as tx_codigo_programa, + nullif(aa_ano_programa, '')::integer as aa_ano_programa, + tx_situacao_programa::text as tx_situacao_programa, + tx_nome_programa::text as tx_nome_programa, + sigla_unidade_descentralizadora::text as sigla_unidade_descentralizadora, + unidade_descentralizadora::text as unidade_descentralizadora, + sigla_unidade_responsavel_acompanhamento::text as sigla_unidade_responsavel_acompanhamento, + unidade_responsavel_acompanhamento::text as unidade_responsavel_acompanhamento, + tx_nome_institucional_programa::text as tx_nome_institucional_programa, + tx_objetivo_programa::text as tx_objetivo_programa, + tx_descricao_programa::text as tx_descricao_programa, + nullif(in_grupo_investimento_obra, '')::boolean as in_grupo_investimento_obra, + nullif(in_grupo_investimento_servico, '')::boolean as in_grupo_investimento_servico, + nullif(in_grupo_investimento_equipamento, '')::boolean as in_grupo_investimento_equipamento, + in_autoriza_subdescentralizacao_outro::text as in_autoriza_subdescentralizacao_outro, + in_autoriza_realizacao_despesas::text as in_autoriza_realizacao_despesas, + in_autoriza_execucao_creditos_descentralizada::text as in_autoriza_execucao_creditos_descentralizada, + nullif(in_beneficiario_especifico, '')::boolean as in_beneficiario_especifico, + nullif(dt_recebimento_plano_beneficiario_inicio, '')::timestamp::date + as dt_recebimento_plano_beneficiario_inicio, + nullif(dt_recebimento_plano_beneficiario_fim, '')::timestamp::date + as dt_recebimento_plano_beneficiario_fim, + nullif(in_chamamento_publico, '')::boolean as in_chamamento_publico, + nullif(dt_recebimento_plano_chamamento_inicio, '')::timestamp::date + as dt_recebimento_plano_chamamento_inicio, + nullif(dt_recebimento_plano_chamamento_fim, '')::timestamp::date + as dt_recebimento_plano_chamamento_fim, + (dt_ingest || '-03:00')::timestamptz as dt_ingest + from {{ source("transfere_gov", "programas") }} + ) + +select * +from programas_raw diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/schema.yaml b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/schema.yaml new file mode 100644 index 00000000..e4b70074 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/bronze/schema.yaml @@ -0,0 +1,561 @@ +version: 2 + +models: + - name: empenhos_tesouro_ted + description: > + Tabela da camada bronze contendo informações detalhadas sobre os empenhos do Tesouro Gerencial + vinculados a emendas parlamentares e TEDs. + A tabela realiza a limpeza do número do processo, padronização de nomes de favorecidos + e conversão de valores financeiros de string para numeric(15,2) via macro. + meta: + tags: + - bronze + columns: + - name: programa_governo + description: > + Código identificador do programa de governo associado ao empenho. + - name: programa_governo_descricao + description: > + Descrição textual do programa de governo. + - name: acao_governo + description: > + Código da ação orçamentária (Ação de Governo). + - name: acao_governo_descricao + description: > + Descrição da ação orçamentária. + - name: emissao_mes + description: > + Mês de emissão da nota de empenho. + - name: emissao_dia + description: > + Dia específico da emissão da nota de empenho. + - name: ne_ccor + description: > + Número completo da nota de empenho (Chave natural principal). + - name: ne_num_processo + description: > + Número do processo administrativo, limpo de caracteres especiais (pontos, barras, hifens) via regex. + - name: ne_info_complementar + description: > + Informações adicionais registradas na nota de empenho. + - name: ne_ccor_descricao + description: > + Descrição detalhada do item ou serviço empenhado. + - name: doc_observacao + description: > + Observações registradas no documento fiscal ou contábil. + - name: natureza_despesa + description: > + Código da natureza da despesa (ex: 339030). + - name: natureza_despesa_descricao + description: > + Descrição da categoria da natureza de despesa. + - name: ne_ccor_favorecido + description: > + CNPJ ou CPF do favorecido do empenho, convertido para caixa alta. + - name: ne_ccor_favorecido_descricao + description: > + Nome ou Razão Social do favorecido. + - name: ne_ccor_ano_emissao + description: > + Ano de emissão do empenho (YYYY), validado via regex para garantir 4 dígitos numéricos. + - name: ptres + description: > + Programa de Trabalho Resumido (PTRES). + - name: fonte_recursos_detalhada + description: > + Código detalhado da fonte de financiamento. + - name: fonte_recursos_detalhada_descricao + description: > + Descrição da fonte de recursos. + - name: despesas_empenhadas + description: > + Valor total das despesas empenhadas, convertido para numeric(15,2). + - name: despesas_liquidadas + description: > + Valor das despesas que já passaram pela liquidação, convertido para numeric(15,2). + - name: despesas_pagas + description: > + Valor efetivamente pago ao favorecido, convertido para numeric(15,2). + - name: restos_a_pagar_inscritos + description: > + Valor inscrito em Restos a Pagar (RP), convertido para numeric(15,2). + - name: restos_a_pagar_pagos + description: > + Valor de Restos a Pagar que já foram quitados, convertido para numeric(15,2). + - name: dt_ingest + description: > + Timestamp da ingestão dos dados na camada raw, ajustado para o fuso horário de Brasília (UTC-3). + + tests: + - verificacao_tipagem: + nome_tabela: 'mir.empenhos_tesouro_parlamentares' + nome_coluna: 'ne_ccor_ano_emissao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'mir.empenhos_tesouro_parlamentares' + nome_coluna: 'despesas_empenhadas' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'mir.empenhos_tesouro_parlamentares' + nome_coluna: 'restos_a_pagar_inscritos' + tipo_esperado: 'numeric' + + - name: nc_tesouro_mir + description: > + Modelo da camada bronze que consolida os dados brutos de Notas de Crédito do Tesouro Gerencial (SIAFI), + abrangendo os períodos anteriores e posteriores a 2026. + Realiza a unificação (UNION ALL) das bases históricas e atuais, preenchendo com valores nulos + (ou zero para campos financeiros) as colunas inexistentes em cada tabela original para garantir a integridade do schema. + Também aplica o casamento semântico entre colunas equivalentes de 2025 e 2026 + (ex.: evento/tipo_nc, UG responsável/emitente, natureza/GND, plano interno/descrição), + mantendo um layout único para consumo nas camadas seguintes. + meta: + tags: + - bronze + columns: + - name: programa_governo + description: "Somente pré-2026. Código identificador do programa de governo." + - name: programa_governo_descricao + description: "Somente pré-2026. Descrição detalhada do programa de governo." + - name: acao_governo + description: "Somente pré-2026. Código da ação orçamentária." + - name: acao_governo_descricao + description: "Somente pré-2026. Descrição da ação orçamentária." + - name: nc + description: "Comum (pré e pós-2026). Número identificador da Nota de Crédito (NC)." + - name: nc_transferencia + description: "Comum (pré e pós-2026). Número/identificador de transferência associado à NC." + - name: nc_fonte_recursos + description: "Comum (pré e pós-2026). Código da fonte de recursos (na pós-2026 vem de fonte_codigo)." + - name: nc_fonte_recursos_descricao + description: "Comum (pré e pós-2026). Descrição da fonte de recursos (na pós-2026 vem de fonte_nome)." + - name: ptres + description: "Comum (pré e pós-2026). Programa de Trabalho Resumido (PTRES)." + - name: nc_evento_descricao + description: "Comum (pré e pós-2026). NC - Evento Desc (na pós-2026 mapeado a partir de tipo_nc)." + - name: nc_ug_responsavel + description: "Comum (pré e pós-2026). NC - UG Responsável (na pós-2026 mapeado a partir de emitente_codigo)." + - name: nc_ug_responsavel_descricao + description: "Comum (pré e pós-2026). NC - UG Responsável Descrição (na pós-2026 mapeado a partir de emitente_nome)." + - name: nc_natureza_despesa + description: "Comum (pré e pós-2026). NC - Natureza Despesa (na pós-2026 mapeado a partir de gnd_codigo)." + - name: nc_natureza_despesa_descricao + description: "Comum (pré e pós-2026). NC - Natureza Despesa Descrição (na pós-2026 mapeado a partir de gnd_nome)." + - name: nc_plano_interno + description: "Comum (pré e pós-2026). NC - Plano Interno (na pós-2026 mapeado a partir de pi_codigo)." + - name: nc_plano_interno_descricao1 + description: "Comum (pré e pós-2026). NC - Plano Interno Descrição 1 (na pós-2026 mapeado a partir de pi_nome)." + - name: favorecido_doc + description: "Comum (pré e pós-2026). Favorecido Doc (na pós-2026 mapeado a partir de favorecido_codigo)." + - name: favorecido_doc_descricao + description: "Comum (pré e pós-2026). Favorecido Doc Desc (na pós-2026 mapeado a partir de favorecido_nome)." + - name: favorecido_municipio + description: "Somente pré-2026. Código do município do favorecido." + - name: favorecido_municipio_descricao + description: "Somente pré-2026. Nome do município do favorecido." + - name: valor_celula + description: "Comum (pré e pós-2026). Valor financeiro (na pré-2026 vem de nc_valor_linha; na pós-2026 vem de valor_celula)." + - name: movimento_liquido_moeda_origem + description: "Comum (pré e pós-2026). Movimento líquido (na pré-2026 vem de movimento_liquido_moeda_origem; na pós-2026 mapeado a partir de total_lista)." + - name: dt_ingest + description: "Comum (pré e pós-2026). Timestamp da ingestão (UTC-3)." + - name: descricao + description: "Somente pós-2026. Descrição geral da NC (campo descricao)." + - name: nc_plano_interno_descricao2 + description: "Somente pré-2026. NC - Plano Interno Descrição 2." + - name: nc_evento + description: "Somente pré-2026. NC - Evento (sem match na base pós-2026)." + - name: nc_item_detalhamento + description: "Somente pós-2026. NC Item - Detalhamento (S/N) (sem match na base pré-2026)." + - name: emissao_dia + description: "Somente pós-2026. Dia da emissão da NC." + - name: emissao_mes + description: "Somente pós-2026. Mês da emissão da NC." + - name: emissao_ano + description: "Somente pós-2026. Ano da emissão da NC." + - name: ro + description: "Somente pós-2026. Registro de Operação associado." + - name: dc + description: "Somente pós-2026. Indicador de Débito ou Crédito (D/C)." + - name: total_lista + description: "Somente pós-2026. Valor total da lista de itens." + - name: esfera_orcamentaria_codigo + description: "Somente pós-2026. Código da esfera orçamentária." + - name: esfera_orcamentaria_nome + description: "Somente pós-2026. Nome da esfera orçamentária." + + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.nc_tesouro_mir' + nome_coluna: 'ptres' + tipo_esperado: 'text' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.nc_tesouro_mir' + nome_coluna: 'valor_celula' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.nc_tesouro_mir' + nome_coluna: 'movimento_liquido_moeda_origem' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.nc_tesouro_mir' + nome_coluna: 'emissao_dia' + tipo_esperado: 'date' + + - name: pf_tesouro + description: > + Modelo da camada bronze para programacoes financeiras do Tesouro (PF), + com padronizacao basica de tipos, conversao do valor financeiro para numeric(15,2) + e ajuste de timestamp de ingestao para UTC-3. + meta: + tags: + - bronze + columns: + - name: emissao_mes + description: "Mes de emissao da programacao financeira." + - name: emissao_dia + description: "Data de emissao da programacao financeira." + - name: ug_emitente + description: "Codigo da UG emitente." + - name: ug_emitente_descricao + description: "Descricao da UG emitente." + - name: ug_favorecido + description: "Codigo da UG favorecida." + - name: ug_favorecido_descricao + description: "Descricao da UG favorecida." + - name: pf_evento + description: "Codigo do evento da PF." + - name: pf_evento_descricao + description: "Descricao do evento da PF." + - name: pf + description: "Identificador da programacao financeira." + - name: pf_inscricao + description: "Numero de inscricao associado a PF (ex.: TED)." + - name: pf_acao + description: "Codigo da acao registrada na PF." + - name: pf_acao_descricao + description: "Descricao da acao registrada na PF." + - name: pf_fonte_recursos + description: "Codigo da fonte de recursos da PF." + - name: pf_fonte_recursos_descricao + description: "Descricao da fonte de recursos da PF." + - name: doc_observacao + description: "Observacao textual do documento." + - name: pf_valor_linha + description: "Valor financeiro da linha, convertido para numeric(15,2)." + - name: dt_ingest + description: "Timestamp de ingestao ajustado para UTC-3." + + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_tesouro_mir' + nome_coluna: 'emissao_dia' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_tesouro_mir' + nome_coluna: 'pf_valor_linha' + tipo_esperado: 'numeric' + + - name: pf_ptres + description: > + Modelo da camada bronze para a programacao de acao por PTRES (SIAFI), + com padronizacao de tipos textuais e conversao dos campos de dotacao + para numeric(15,2), mantendo timestamp de ingestao em UTC-3. + meta: + tags: + - bronze + columns: + - name: programa_governo + description: "Codigo do programa de governo." + - name: programa_governo_descricao + description: "Descricao do programa de governo." + - name: plano_orcamentario + description: "Codigo do plano orcamentario." + - name: plano_orcamentario_descricao_1 + description: "Descricao 1 do plano orcamentario." + - name: plano_orcamentario_descricao_2 + description: "Descricao 2 do plano orcamentario." + - name: plano_orcamentario_descricao_3 + description: "Descricao 3 do plano orcamentario." + - name: plano_orcamentario_descricao_4 + description: "Descricao 4 do plano orcamentario." + - name: plano_orcamentario_descricao_5 + description: "Descricao 5 do plano orcamentario." + - name: plano_orcamentario_descricao_6 + description: "Descricao 6 do plano orcamentario." + - name: acao_governo + description: "Codigo da acao de governo." + - name: acao_governo_descricao + description: "Descricao da acao de governo." + - name: ptres + description: "Codigo PTRES associado a programacao." + - name: natureza_despesa + description: "Codigo da natureza de despesa." + - name: natureza_despesa_descricao + description: "Descricao da natureza de despesa." + - name: dotacao_inicial + description: "Valor da dotacao inicial convertido para numeric(15,2)." + - name: dotacao_suplementar + description: "Valor da dotacao suplementar convertido para numeric(15,2)." + - name: dotacao_atualizada + description: "Valor da dotacao atualizada convertido para numeric(15,2)." + - name: dt_ingest + description: "Timestamp de ingestao ajustado para UTC-3." + + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_ptres_mir' + nome_coluna: 'dotacao_inicial' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_ptres_mir' + nome_coluna: 'dotacao_atualizada' + tipo_esperado: 'numeric' + + - name: pf_transfere + description: > + Modelo da camada bronze para programacao financeira do Transferegov, + com padronizacao de tipos de identificadores e campos textuais, + alem do ajuste de dt_ingest para timestamp com fuso UTC-3. + meta: + tags: + - bronze + columns: + - name: id_programacao + description: "Identificador da programacao financeira." + - name: id_plano_acao + description: "Identificador do plano de acao no Transferegov." + - name: tp_pf_tipo_programacao + description: "Tipo da programacao financeira (ex.: T)." + - name: tx_minuta_programacao + description: "Identificador da minuta/programacao (formato MPF)." + - name: tx_numero_programacao + description: "Numero da programacao financeira." + - name: tx_situacao_programacao + description: "Situacao da programacao financeira." + - name: tx_observacao_programacao + description: "Descricao textual/observacao da programacao." + - name: ug_emitente_programacao + description: "Codigo da UG emitente da programacao." + - name: ug_favorecida_programacao + description: "Codigo da UG favorecida da programacao." + - name: dh_recebimento_programacao + description: "Timestamp de recebimento/cadastro da programacao no sistema de origem." + - name: dt_ingest + description: "Timestamp de ingestao ajustado para UTC-3." + + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_transfere_mir' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_transfere_mir' + nome_coluna: 'id_programacao' + tipo_esperado: 'integer' + + - name: notas_de_credito + description: > + Modelo da camada bronze para Notas de Crédito do TransfereGov (módulo TED), + com padronização de tipos e ajuste do timestamp de ingestão para UTC-3. + meta: + tags: + - bronze + columns: + - name: id_nota + description: "Identificador único da Nota de Crédito." + - name: id_plano_acao + description: "Identificador único do Plano de Ação associado." + - name: tx_minuta_nota + description: "Minuta/identificador textual da Nota de Crédito." + - name: tx_numero_nota + description: "Número da Nota de Crédito." + - name: dt_emissao_nota + description: "Data/hora de emissão da Nota de Crédito." + - name: cd_gestao_emitente_nota + description: "Código da gestão emitente da Nota de Crédito." + - name: cd_gestao_favorecida_nota + description: "Código da gestão favorecida da Nota de Crédito." + - name: tx_situacao_nota + description: "Situação da Nota de Crédito." + - name: cd_ug_emitente_nota + description: "Código da Unidade Gestora emitente da Nota de Crédito." + - name: cd_ug_favorecida_nota + description: "Código da Unidade Gestora favorecida da Nota de Crédito." + - name: tx_observacao_nota + description: "Observações/texto livre da Nota de Crédito." + - name: dt_ingest + description: "Timestamp de ingestão ajustado para UTC-3." + + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.notas_de_credito_mir' + nome_coluna: 'id_nota' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.notas_de_credito_mir' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.notas_de_credito_mir' + nome_coluna: 'dt_emissao_nota' + tipo_esperado: 'timestamp without time zone' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.notas_de_credito_mir' + nome_coluna: 'dt_ingest' + tipo_esperado: 'timestamp with time zone' + + - name: programas_ted + description: > + Modelo da camada bronze para Programas do TransfereGov (módulo TED), + com padronização de tipos e ajuste do timestamp de ingestão para UTC-3. + meta: + tags: + - bronze + columns: + - name: id_programa + description: "Identificador único do Programa." + - name: tx_codigo_programa + description: "Código do Programa." + - name: aa_ano_programa + description: "Ano do Programa." + - name: tx_situacao_programa + description: "Situação do Programa." + - name: tx_nome_programa + description: "Nome do Programa." + - name: sigla_unidade_descentralizadora + description: "Sigla da unidade descentralizadora." + - name: unidade_descentralizadora + description: "Unidade descentralizadora." + - name: sigla_unidade_responsavel_acompanhamento + description: "Sigla da unidade responsável pelo acompanhamento." + - name: unidade_responsavel_acompanhamento + description: "Unidade responsável pelo acompanhamento." + - name: tx_nome_institucional_programa + description: "Nome institucional do Programa." + - name: tx_objetivo_programa + description: "Objetivo do Programa." + - name: tx_descricao_programa + description: "Descrição do Programa." + - name: in_grupo_investimento_obra + description: "Indicador do grupo de investimento (obra)." + - name: in_grupo_investimento_servico + description: "Indicador do grupo de investimento (serviço)." + - name: in_grupo_investimento_equipamento + description: "Indicador do grupo de investimento (equipamento)." + - name: in_autoriza_subdescentralizacao_outro + description: "Indicador/flag de autorização de subdescentralização (outro)." + - name: in_autoriza_realizacao_despesas + description: "Indicador/flag de autorização para realização de despesas." + - name: in_autoriza_execucao_creditos_descentralizada + description: "Indicador/flag de autorização para execução de créditos descentralizada." + - name: in_beneficiario_especifico + description: "Indicador de beneficiário específico." + - name: dt_recebimento_plano_beneficiario_inicio + description: "Data de início do recebimento do plano de beneficiário." + - name: dt_recebimento_plano_beneficiario_fim + description: "Data final do recebimento do plano de beneficiário." + - name: in_chamamento_publico + description: "Indicador de chamamento público." + - name: dt_recebimento_plano_chamamento_inicio + description: "Data de início do recebimento do plano de chamamento." + - name: dt_recebimento_plano_chamamento_fim + description: "Data final do recebimento do plano de chamamento." + - name: dt_ingest + description: "Timestamp de ingestão ajustado para UTC-3." + + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.programas_mir' + nome_coluna: 'id_programa' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.programas_mir' + nome_coluna: 'aa_ano_programa' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.programas_mir' + nome_coluna: 'in_beneficiario_especifico' + tipo_esperado: 'boolean' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.programas_mir' + nome_coluna: 'dt_recebimento_plano_beneficiario_inicio' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.programas_mir' + nome_coluna: 'dt_ingest' + tipo_esperado: 'timestamp with time zone' + + - name: planos_acao_ted + description: > + Modelo da camada bronze para Planos de Ação do TransfereGov (módulo TED), + com padronização de tipos e ajuste do timestamp de ingestão para UTC-3. + meta: + tags: + - bronze + columns: + - name: id_plano_acao + description: "Identificador único do Plano de Ação." + - name: id_programa + description: "Identificador do Programa associado." + - name: sigla_unidade_descentralizada + description: "Sigla da unidade descentralizada." + - name: unidade_descentralizada + description: "Unidade descentralizada." + - name: sigla_unidade_responsavel_execucao + description: "Sigla da unidade responsável pela execução." + - name: unidade_responsavel_execucao + description: "Unidade responsável pela execução." + - name: vl_total_plano_acao + description: "Valor total do Plano de Ação." + - name: dt_inicio_vigencia + description: "Data de início de vigência do Plano de Ação." + - name: dt_fim_vigencia + description: "Data de fim de vigência do Plano de Ação." + - name: tx_objeto_plano_acao + description: "Objeto do Plano de Ação." + - name: tx_justificativa_plano_acao + description: "Justificativa do Plano de Ação." + - name: in_forma_execucao_direta + description: "Indicador da forma de execução direta." + - name: in_forma_execucao_particulares + description: "Indicador da forma de execução por particulares." + - name: in_forma_execucao_descentralizada + description: "Indicador da forma de execução descentralizada." + - name: tx_situacao_plano_acao + description: "Situação do Plano de Ação." + - name: aa_ano_plano_acao + description: "Ano do Plano de Ação." + - name: vl_beneficiario_especifico + description: "Valor do beneficiário específico." + - name: vl_chamamento_publico + description: "Valor do chamamento público." + - name: sq_instrumento + description: "Sequencial do instrumento." + - name: aa_instrumento + description: "Ano do instrumento." + - name: dt_ingest + description: "Timestamp de ingestão ajustado para UTC-3." + + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.planos_acao_mir' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.planos_acao_mir' + nome_coluna: 'id_programa' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.planos_acao_mir' + nome_coluna: 'vl_total_plano_acao' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.planos_acao_mir' + nome_coluna: 'dt_inicio_vigencia' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.planos_acao_mir' + nome_coluna: 'dt_ingest' + tipo_esperado: 'timestamp with time zone' diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/gold/schema.yml b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/gold/schema.yml new file mode 100644 index 00000000..e0fb8900 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/gold/schema.yml @@ -0,0 +1,116 @@ +version: 2 + +models: + + - name: ted_resumo_orcamentario + description: > + Tabela gold de consolidação orçamentária e financeira por Plano de Ação TED do MIR. + Agrega em uma única linha por (plano_acao, num_transf) os valores provenientes de + quatro fontes distintas: o valor total pactuado no plano (TransfereGov), os créditos + orçamentários descentralizados via Notas de Crédito (SIAFI), os empenhos registrados + nas Notas de Empenho (SIAFI) e os recursos financeiros liberados via Programações + Financeiras (SIAFI + TransfereGov). As quatro dimensões são combinadas via FULL JOIN + para garantir que nenhum registro seja perdido mesmo que exista em apenas uma fonte. + meta: + tags: + - gold + columns: + - name: plano_acao + description: > + Identificador único do Plano de Ação no TransfereGov. Chave primária junto com num_transf. + - name: num_transf + description: > + Número da transferência TED — o elo entre os sistemas SIAFI e TransfereGov. + É o campo que conecta NCs, NEs e PFs do SIAFI ao Plano de Ação correspondente. + - name: sigla_unidade_descentralizada + description: > + Sigla da unidade receptora dos recursos, conforme cadastrado no Plano de Ação do TransfereGov. + - name: valor_firmado + description: > + Valor total pactuado no Plano de Ação (vl_total_plano_acao), conforme o registro + mais recente no TransfereGov. Representa o compromisso financeiro acordado. + - name: orcamento_recebido + description: > + Soma dos valores de Notas de Crédito recebidas (eventos que não são 300301/300307), + representando o crédito orçamentário efetivamente descentralizado para o MIR. + - name: orcamento_devolvido + description: > + Soma dos valores de Notas de Crédito de eventos 300301 e 300307, + representando crédito orçamentário devolvido ao órgão concedente. + - name: empenhado + description: > + Soma dos valores positivos de despesas_empenhadas — empenhos formalizados, + excluindo anulações. + - name: empenho_anulado + description: > + Soma dos valores absolutos das despesas_empenhadas negativas — empenhos + cancelados ou revertidos após a emissão. + - name: despesas_pagas_exercicio + description: > + Total de despesas pagas no exercício corrente, proveniente do campo + despesas_pagas das Notas de Empenho. + - name: despesas_pagas_rap + description: > + Total de despesas pagas via Restos a Pagar (exercícios anteriores), + proveniente de restos_a_pagar_pagos das Notas de Empenho. + - name: restos_a_pagar + description: > + Valor total inscrito como Restos a Pagar — despesas empenhadas e não + pagas até o encerramento do exercício fiscal. + - name: despesas_liquidada + description: > + Soma das despesas liquidadas — despesas com bens ou serviços + efetivamente entregues e atestados. + - name: financeiro_recebido + description: > + Soma dos valores de Programações Financeiras com ação 'TRANSFERENCIA', + representando os recursos financeiros efetivamente recebidos pelo MIR. + - name: financeiro_devolvido + description: > + Soma dos valores de Programações Financeiras com ação 'DEVOLUCAO', + representando recursos financeiros devolvidos ao concedente. + - name: financeiro_cancelado + description: > + Soma dos valores de Programações Financeiras com ação 'CANCELAMENTO', + representando programações canceladas antes de seu processamento. + - name: dt_ingest + description: > + Timestamp mais recente de ingestão entre todas as fontes que compõem o registro + (planos_acao_ted, nc_plano_acao, empenhos_por_plano_acao, pf_unificado). + Indica quando os dados foram atualizados pela última vez (UTC-3). + - name: sigla_unidade_responsavel_acompanhamento + description: > + Sigla da unidade responsável pelo acompanhamento do programa, proveniente + de programas_ted (TransfereGov). + - name: tx_nome_institucional_programa + description: > + Nome institucional do programa ao qual o Plano de Ação está vinculado, + proveniente de programas_ted (TransfereGov). + - name: tx_objetivo_programa + description: > + Descrição do objetivo do programa, proveniente de programas_ted (TransfereGov). + - name: programa_governo + description: > + Código do programa de governo conforme classificação orçamentária SIAFI, + proveniente de nc_plano_acao. + - name: programa_governo_descricao + description: > + Descrição do programa de governo conforme classificação orçamentária SIAFI, + proveniente de nc_plano_acao. + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.ted_resumo_orcamentario' + nome_coluna: 'plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.ted_resumo_orcamentario' + nome_coluna: 'valor_firmado' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.ted_resumo_orcamentario' + nome_coluna: 'empenhado' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.ted_resumo_orcamentario' + nome_coluna: 'dt_ingest' + tipo_esperado: 'timestamp with time zone' diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/gold/ted_resumo_orcamentario.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/gold/ted_resumo_orcamentario.sql new file mode 100644 index 00000000..eeb0bf19 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/gold/ted_resumo_orcamentario.sql @@ -0,0 +1,151 @@ +{{ config(materialized="table") }} + +with + planos_acao_deduplicado as ( + select + id_plano_acao, + id_programa, + sq_instrumento as num_transf, + sigla_unidade_descentralizada, + vl_total_plano_acao, + dt_ingest as dt_ingest_plano_acao + from ( + select + pa.*, + row_number() over ( + partition by pa.id_plano_acao + order by pa.dt_ingest desc + ) as rn + from {{ ref("planos_acao_ted") }} pa + ) pa_filtrado + where rn = 1 + ), + + programas_tb as ( + select + pad.id_plano_acao, + prog.sigla_unidade_responsavel_acompanhamento, + prog.tx_nome_institucional_programa, + prog.tx_objetivo_programa + from planos_acao_deduplicado pad + left join {{ ref("programas_ted") }} prog using (id_programa) + ), + + valor_firmado_tb as ( + select + id_plano_acao as plano_acao, + num_transf, + vl_total_plano_acao as valor_firmado, + sigla_unidade_descentralizada, + dt_ingest_plano_acao as dt_ingest_vf + from planos_acao_deduplicado + ), + + valores_orcamentos_tb as ( + select + id_plano_acao as plano_acao, + nc_transferencia as num_transf, + sum( + case + when nc_evento in ('300301', '300307') then 0 + else valor_celula + end + ) as orcamento_recebido, + sum( + case + when nc_evento in ('300301', '300307') then valor_celula + else 0 + end + ) as orcamento_devolvido, + max(programa_governo) as programa_governo, + max(programa_governo_descricao) as programa_governo_descricao, + max(dt_ingest) as dt_ingest_vo + from {{ ref("nc_plano_acao") }} + where ptres not in ('-9') + group by id_plano_acao, nc_transferencia + ), + + valores_empenhados_tb as ( + select + plano_acao, + num_transf, + sum( + case when despesas_empenhadas > 0 then despesas_empenhadas else 0 end + ) as empenhado, + sum( + case when despesas_empenhadas < 0 then -despesas_empenhadas else 0 end + ) as empenho_anulado, + sum(despesas_pagas) as despesas_pagas_exercicio, + sum(restos_a_pagar_pagos) as despesas_pagas_rap, + sum(restos_a_pagar_inscritos) as restos_a_pagar, + sum(despesas_liquidadas) as despesas_liquidada, + max(dt_ingest) as dt_ingest_ve + from {{ ref("empenhos_por_plano_acao") }} + group by plano_acao, num_transf + ), + + valores_financeiro_tb as ( + select + id_plano_acao as plano_acao, + pf_inscricao as num_transf, + sum( + case + when substring(pf_acao_descricao, '(\w+) ') = 'TRANSFERENCIA' + then pf_valor_linha + else 0 + end + ) as financeiro_recebido, + sum( + case + when substring(pf_acao_descricao, '(\w+) ') = 'DEVOLUCAO' + then pf_valor_linha + else 0 + end + ) as financeiro_devolvido, + sum( + case + when substring(pf_acao_descricao, '(\w+) ') = 'CANCELAMENTO' + then pf_valor_linha + else 0 + end + ) as financeiro_cancelado, + max(dt_ingest) as dt_ingest_vfin + from {{ ref("pf_unificado") }} + group by id_plano_acao, pf_inscricao + ), + + join_parcial as ( + select + *, + greatest(vo.dt_ingest_vo, ve.dt_ingest_ve, vfin.dt_ingest_vfin) as dt_ingest_jp + from valores_orcamentos_tb vo + full join valores_empenhados_tb ve using (plano_acao, num_transf) + full join valores_financeiro_tb vfin using (plano_acao, num_transf) + ) + +select + plano_acao, + num_transf, + sigla_unidade_descentralizada, + valor_firmado, + orcamento_recebido, + orcamento_devolvido, + empenhado, + empenho_anulado, + despesas_pagas_exercicio, + despesas_pagas_rap, + restos_a_pagar, + despesas_liquidada, + financeiro_recebido, + financeiro_devolvido, + financeiro_cancelado, + greatest(vf.dt_ingest_vf, jp.dt_ingest_jp) as dt_ingest, + prog.sigla_unidade_responsavel_acompanhamento, + prog.tx_nome_institucional_programa, + prog.tx_objetivo_programa, + jp.programa_governo, + jp.programa_governo_descricao +from valor_firmado_tb vf +full join join_parcial jp using (plano_acao, num_transf) +left join programas_tb prog on plano_acao = prog.id_plano_acao +where (plano_acao is not null) or (num_transf is not null) diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/empenhos_por_plano_acao.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/empenhos_por_plano_acao.sql new file mode 100644 index 00000000..a982209d --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/empenhos_por_plano_acao.sql @@ -0,0 +1,562 @@ +with +empenhos_sem_vinculo_ted as( + select + *, + right(ne_ccor, 12) as ne, + left(ne_ccor,6) as orgao_id, + null as nc, + null as num_transf, + 'sem vinculo' as metodo + from {{ ref("empenhos_tesouro_ted") }} + where + ne_ccor_descricao ~* '\bTED[[:space:]:/().-]*(S/?[VN]|S/?VINCULO)' + or ne_ccor_descricao ~* 'SEM[[:space:]]+VINC[[:space:]]*(ULO|/TED)' +), +empenhos_filtrados as( + select + * + from {{ ref("empenhos_tesouro_ted") }} + where + ne_ccor_descricao !~* '\bTED[[:space:]:/().-]*(S/?[VN]|S/?VINCULO)' + and ne_ccor_descricao !~* 'SEM[[:space:]]+VINC[[:space:]]*(ULO|/TED)' + +), +empenhos_orgaos_metodo_1 as ( + select + *, + -- Uma série de extrações que servirão de identificadores + right(ne_ccor, 12) as ne, + left(ne_ccor,6) as orgao_id, + {{ target.schema }}.format_nc( + regexp_substr(ne_ccor_descricao, '([0-9]{4}NC[0-9]+)') + ) as nc, + replace( + (regexp_match( + ne_ccor_descricao, + '(FERENCIA|TED|CRICAO|TRANSF.|TRANF.|TRANF. |TRANSFERENCIA |TRANSFERENCIA:)(\s|^|-|)([0-9]{6}|1\w{5}|[0-9]{3}\.[0-9]{3})(\s|$|\.|,|-|\/)', + 'i' + ))[3], + '.', + '' + ) as num_transf, + 'metodo 1' as metodo + from empenhos_filtrados +), + +empenhos_restantes_metodo_1 as( +select * from empenhos_orgaos_metodo_1 where num_transf is null AND nc is null +), + +empenhos_orgaos_metodo_2 as ( + select + programa_governo, + programa_governo_descricao, + acao_governo, + acao_governo_descricao, + emissao_mes, + emissao_dia, + ne_ccor, + ne_num_processo, + ne_info_complementar, + ne_ccor_descricao, + doc_observacao, + natureza_despesa, + natureza_despesa_descricao, + ne_ccor_favorecido, + ne_ccor_favorecido_descricao, + ne_ccor_ano_emissao, + ptres, + fonte_recursos_detalhada, + fonte_recursos_detalhada_descricao, + despesas_empenhadas, + despesas_liquidadas, + despesas_pagas, + restos_a_pagar_inscritos, + restos_a_pagar_pagos, + dt_ingest, + ne, + orgao_id, + nc, + replace( + (regexp_match( + ne_ccor_descricao, + '.*(?:NOTA DE (TRANSFERENCIA|TRANFERENCIA|CREDITO))[:.[:space:]-]*((?=[A-Za-z0-9]*[0-9])[A-Za-z0-9]{6,})', + 'i' + ))[2], + '.', + '' + ) as num_transf, + 'metodo 2' as metodo + from empenhos_restantes_metodo_1 p +), + +empenhos_restantes_metodo_2 as( +select * from empenhos_orgaos_metodo_2 where num_transf is null AND nc is null +), + +empenhos_orgaos_metodo_3 as ( + select + programa_governo, + programa_governo_descricao, + acao_governo, + acao_governo_descricao, + emissao_mes, + emissao_dia, + ne_ccor, + ne_num_processo, + ne_info_complementar, + ne_ccor_descricao, + doc_observacao, + natureza_despesa, + natureza_despesa_descricao, + ne_ccor_favorecido, + ne_ccor_favorecido_descricao, + ne_ccor_ano_emissao, + ptres, + fonte_recursos_detalhada, + fonte_recursos_detalhada_descricao, + despesas_empenhadas, + despesas_liquidadas, + despesas_pagas, + restos_a_pagar_inscritos, + restos_a_pagar_pagos, + dt_ingest, + ne, + orgao_id, + nc, + replace( + (regexp_match( + ne_ccor_descricao, + '.*(?:(?:TED(?:[[:space:]]*[-.N∞øº°∅()]*))[[:space:]]*|(?:SIAFI[[:space:]]+N∫))[[:space:].-]*(?= 3 + AND ano_normalizado IS NOT NULL + GROUP BY orgao_id, numero_base +), + +empenhos_orgaos_metodo_7 as ( +SELECT + a.*, + g.ano_oficial, + CASE + WHEN length(a.numero_base) <= 2 + AND a.ano_normalizado is null + THEN NULL + + -- se o registro não tem ano, e o numero_base é "longo", e há um ano oficial no grupo -> preencher + WHEN a.ano_normalizado IS NULL + AND length(a.numero_base) >= 3 + AND g.ano_oficial IS NOT NULL + THEN a.numero_base || '/' || g.ano_oficial + + -- se o registro já tem ano_normalizado -> manter esse ano (normalizado) + WHEN a.ano_normalizado IS NOT NULL + THEN a.numero_base || '/' || a.ano_normalizado + + -- caso contrário (nenhum ano encontrado) -> deixar só o numero_base + ELSE a.numero_base + END AS numero_ted_normalizado +FROM norm_metodo_7 a +LEFT JOIN agrupado_metodo_7 g + ON a.orgao_id = g.orgao_id +AND a.numero_base = g.numero_base +), + +empenhos_restantes_metodo_7 as( + select * from empenhos_orgaos_metodo_7 + WHERE numero_ted_normalizado is null +), + +base_empenhos_orgaos_metodo_8 as ( +select + -- seleciona todas as colunas do órgãos 1, exceto nc e num_transf + emissao_mes,emissao_dia,ne_ccor,ne_num_processo,ne_info_complementar,ne_ccor_descricao,doc_observacao,natureza_despesa,natureza_despesa_descricao,ne_ccor_favorecido,ne_ccor_favorecido_descricao,ne_ccor_ano_emissao,ptres,fonte_recursos_detalhada,fonte_recursos_detalhada_descricao,despesas_empenhadas,despesas_liquidadas,despesas_pagas,restos_a_pagar_inscritos,restos_a_pagar_pagos,dt_ingest, ne,orgao_id,nc,num_transf,metodo, + trim(both ' -' from regexp_replace((regexp_match( + doc_observacao, + 'TED [[:space:].:NR∫º°-]*(?:([A-Za-zÀ-ÿ/][A-Za-zÀ-ÿ0-9/ \\-]*)[[:space:]\\-]+)?([0-9]{1,5}(?:[./ \\-][0-9]{2,4})?)', + 'i' + ))[1], '\s+', ' ', 'g')) AS complemento_ted, + replace((regexp_match( + doc_observacao, + 'TED [[:space:].:NR∫º°-]*(?:([A-Za-zÀ-ÿ/][A-Za-zÀ-ÿ0-9/ \\-]*)[[:space:]\\-]+)?([0-9]{1,5}(?:[./ \\-][0-9]{2,4})?)', + 'i' + ))[2], '.', '') AS num_ted, + 'metodo 2' as metodo_ted +from empenhos_restantes_metodo_7), + +base_metodo_8 AS ( + SELECT + *, + (regexp_match(num_ted, '^([0-9]{1,5})(?:[/.\- ]([0-9]{2,4}))?$'))[1] AS numero_base, + (regexp_match(num_ted, '^([0-9]{1,5})(?:[/.\- ]([0-9]{2,4}))?$'))[2] AS ano_raw + FROM base_empenhos_orgaos_metodo_8 +), +norm_metodo_8 AS ( + SELECT + *, + CASE + WHEN ano_raw IS NULL THEN NULL + WHEN length(ano_raw) = 2 THEN + CASE WHEN ano_raw::int <= 30 + THEN '20' || ano_raw -- 24 → 2024 + ELSE '19' || ano_raw -- 95 → 1995 + END + ELSE ano_raw + END AS ano_normalizado + FROM base_metodo_8 +), +agrupado_metodo_8 AS ( + -- calculamos o ano oficial APENAS para numero_base "longos" + SELECT + orgao_id, + numero_base, + MAX(ano_normalizado) AS ano_oficial + FROM norm_metodo_7 + WHERE length(numero_base) >= 3 + AND ano_normalizado IS NOT NULL + GROUP BY orgao_id, numero_base +), +empenhos_orgaos_metodo_8 as( + SELECT + a.*, + g.ano_oficial, + CASE + WHEN length(a.numero_base) <= 2 + AND a.ano_normalizado is null + THEN NULL + + -- se o registro não tem ano, e o numero_base é "longo", e há um ano oficial no grupo -> preencher + WHEN a.ano_normalizado IS NULL + AND length(a.numero_base) >= 3 + AND g.ano_oficial IS NOT NULL + THEN a.numero_base || '/' || g.ano_oficial + + -- se o registro já tem ano_normalizado -> manter esse ano (normalizado) + WHEN a.ano_normalizado IS NOT NULL + THEN a.numero_base || '/' || a.ano_normalizado + + -- caso contrário (nenhum ano encontrado) -> deixar só o numero_base + ELSE a.numero_base + END AS numero_ted_normalizado + FROM norm_metodo_8 a + LEFT JOIN agrupado_metodo_8 g + ON a.orgao_id = g.orgao_id + AND a.numero_base = g.numero_base + ), +empenhos_restantes_metodo_8 as( + select * from empenhos_orgaos_metodo_8 + WHERE numero_ted_normalizado is null +), + +union_metodo_7_8 as( +select * from empenhos_orgaos_metodo_7 WHERE numero_ted_normalizado is not null +UNION ALL +select * from empenhos_orgaos_metodo_8 WHERE numero_ted_normalizado is not null +UNION ALL +select * from empenhos_restantes_metodo_8), + +ids_agregados_num_ted_normalizado as( +select + orgao_id, + numero_ted_normalizado, + MAX(nc) AS nc, + MAX(num_transf) AS num_transf +from union_metodo_7_8 +GROUP BY orgao_id,numero_ted_normalizado +), + +empenhos_orgaos_metodo_9 AS ( +SELECT + ert.emissao_mes,ert.emissao_dia,ert.ne_ccor,ert.ne_num_processo,ert.ne_info_complementar,ert.ne_ccor_descricao,ert.doc_observacao,ert.natureza_despesa,ert.natureza_despesa_descricao,ert.ne_ccor_favorecido,ert.ne_ccor_favorecido_descricao,ert.ne_ccor_ano_emissao,ert.ptres,ert.fonte_recursos_detalhada,ert.fonte_recursos_detalhada_descricao,ert.despesas_empenhadas,ert.despesas_liquidadas,ert.despesas_pagas,ert.restos_a_pagar_inscritos,ert.restos_a_pagar_pagos,ert.dt_ingest, ert.ne,ert.orgao_id, + COALESCE(ert.nc, r.nc) AS nc, + COALESCE(ert.num_transf, r.num_transf) AS num_transf, + -- método calculado dinamicamente + CASE + WHEN (ert.nc IS NULL AND r.nc IS NOT NULL) + OR (ert.num_transf IS NULL AND r.num_transf IS NOT NULL) + THEN 'metodo 9' + ELSE ert.metodo + END AS metodo, + complemento_ted, num_ted, metodo_ted, numero_base, ano_raw, ano_normalizado, ano_oficial, + numero_ted_normalizado + FROM union_metodo_7_8 ert + LEFT JOIN ids_agregados_num_ted_normalizado r USING (orgao_id,numero_ted_normalizado) +), +empenhos_restantes_metodo_9 as ( + select * from empenhos_orgaos_metodo_9 where (nc != '') or (num_transf is not null) +), +planos_de_acao as ( + select * from {{ ref("num_transf_n_plano_acao") }} where plano_acao is not null +), +result_table as ( + select distinct er.*, pa.plano_acao::integer as plano_acao, pa.num_transf as num_transf_pa + from empenhos_restantes_metodo_9 er + left join planos_de_acao pa + on er.num_transf=CAST(pa.num_transf AS TEXT) +) -- + +select * +from result_table diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/nc_plano_acao.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/nc_plano_acao.sql new file mode 100644 index 00000000..891d695e --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/nc_plano_acao.sql @@ -0,0 +1,25 @@ +{{ config(materialized="table") }} + +with + raw_data as ( + select * + from {{ ref("nc_unificado") }} + where nc_transferencia != '-8' + ), + + planos_de_acao as ( + select distinct * + from {{ ref("num_transf_n_plano_acao") }} + where plano_acao is not null + ), + + result_table as ( + select + rd.*, + pda.plano_acao::integer as id_plano_acao + from raw_data rd + left join planos_de_acao pda on rd.nc_transferencia = pda.num_transf + ) + +select * +from result_table diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/nc_unificado.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/nc_unificado.sql new file mode 100644 index 00000000..7201f274 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/nc_unificado.sql @@ -0,0 +1,67 @@ +{{ config(materialized="table") }} + +with + nc_tesouro as ( + select + * + from {{ ref("nc_tesouro_mir") }} + ), + + programa_por_ptres as ( + select + ptres, + programa_governo, + programa_governo_descricao, + acao_governo, + plano_orcamentario_descricao_6 + from ( + select + ptres, + programa_governo, + programa_governo_descricao, + acao_governo, + plano_orcamentario_descricao_6, + row_number() over (partition by trim(ptres) order by ptres) as rn + from {{ ref("pf_ptres") }} + ) subquery + where rn = 1 + ) + +select + coalesce(t.programa_governo, pp.programa_governo) as programa_governo, + coalesce(t.programa_governo_descricao, pp.programa_governo_descricao) as programa_governo_descricao, + coalesce(t.acao_governo, pp.acao_governo) as acao_governo, + coalesce(t.acao_governo_descricao, pp.plano_orcamentario_descricao_6) as acao_governo_descricao, + t.nc, + t.nc_transferencia, + t.nc_fonte_recursos, + t.nc_fonte_recursos_descricao, + t.ptres, + t.nc_evento_descricao, + t.nc_ug_responsavel, + t.nc_ug_responsavel_descricao, + t.nc_natureza_despesa, + t.nc_natureza_despesa_descricao, + t.nc_plano_interno, + t.nc_plano_interno_descricao1, + t.favorecido_doc, + t.favorecido_doc_descricao, + t.favorecido_municipio, + t.favorecido_municipio_descricao, + t.valor_celula, + t.movimento_liquido_moeda_origem, + t.dt_ingest, + t.descricao, + t.nc_plano_interno_descricao2, + t.nc_evento, + t.nc_item_detalhamento, + t.emissao_dia, + t.emissao_mes, + t.emissao_ano, + t.ro, + t.dc, + t.total_lista, + t.esfera_orcamentaria_codigo, + t.esfera_orcamentaria_nome +from nc_tesouro t +left join programa_por_ptres pp on trim(pp.ptres) = trim(t.ptres) \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/pf_unificado.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/pf_unificado.sql new file mode 100644 index 00000000..e13e6e14 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/pf_unificado.sql @@ -0,0 +1,73 @@ +{{ config(materialized="table") }} + +with + pf_tesouro as ( + select + emissao_mes, + emissao_dia, + ug_emitente, + ug_emitente_descricao, + ug_favorecido, + ug_favorecido_descricao, + pf_evento, + pf_evento_descricao, + pf, + pf_inscricao, + pf_acao, + pf_acao_descricao, + pf_fonte_recursos, + pf_fonte_recursos_descricao, + doc_observacao, + pf_valor_linha, + dt_ingest as dt_ingest_tesouro, + upper(trim(right(pf, 12))) as pf_chave + from {{ ref("pf_tesouro") }} + ), + pf_transfere as ( + select + id_programacao, + id_plano_acao, + tp_pf_tipo_programacao, + tx_minuta_programacao, + tx_numero_programacao, + tx_situacao_programacao, + tx_observacao_programacao, + ug_emitente_programacao, + ug_favorecida_programacao, + dh_recebimento_programacao, + dt_ingest as dt_ingest_transfere, + upper(trim(tx_numero_programacao)) as pf_chave + from {{ ref("pf_transfere") }} + ) + +select + t.emissao_mes, + t.emissao_dia, + t.ug_emitente, + t.ug_emitente_descricao, + t.ug_favorecido, + t.ug_favorecido_descricao, + t.pf_evento, + t.pf_evento_descricao, + t.pf, + t.pf_inscricao, + t.pf_acao, + t.pf_acao_descricao, + t.pf_fonte_recursos, + t.pf_fonte_recursos_descricao, + t.doc_observacao, + t.pf_valor_linha, + p.id_programacao, + p.id_plano_acao, + p.tp_pf_tipo_programacao, + p.tx_minuta_programacao, + p.tx_numero_programacao, + p.tx_situacao_programacao, + p.tx_observacao_programacao, + p.ug_emitente_programacao, + p.ug_favorecida_programacao, + p.dh_recebimento_programacao, + t.pf_chave, + greatest(t.dt_ingest_tesouro, p.dt_ingest_transfere) as dt_ingest +from pf_tesouro t +inner join pf_transfere p using (pf_chave) diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/pf_unificado_planos_acao.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/pf_unificado_planos_acao.sql new file mode 100644 index 00000000..8f68c70e --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/pf_unificado_planos_acao.sql @@ -0,0 +1,96 @@ +{{ config(materialized="table") }} + +with + pf_unificado as ( + select * + from {{ ref("pf_unificado") }} + ), + planos_acao_deduplicado as ( + select + id_plano_acao, + id_programa, + sigla_unidade_descentralizada, + unidade_descentralizada, + sigla_unidade_responsavel_execucao, + unidade_responsavel_execucao, + vl_total_plano_acao, + dt_inicio_vigencia, + dt_fim_vigencia, + tx_objeto_plano_acao, + tx_justificativa_plano_acao, + in_forma_execucao_direta, + in_forma_execucao_particulares, + in_forma_execucao_descentralizada, + tx_situacao_plano_acao, + aa_ano_plano_acao, + vl_beneficiario_especifico, + vl_chamamento_publico, + sq_instrumento, + aa_instrumento, + dt_ingest as dt_ingest_plano_acao + from ( + select + pa.*, + row_number() over ( + partition by pa.id_plano_acao + order by pa.dt_ingest desc + ) as rn + from {{ ref("planos_acao_ted") }} pa + ) pa_filtrado + where rn = 1 + ) + +select + pf.emissao_mes, + pf.emissao_dia, + pf.ug_emitente, + pf.ug_emitente_descricao, + pf.ug_favorecido, + pf.ug_favorecido_descricao, + pf.pf_evento, + pf.pf_evento_descricao, + pf.pf, + pf.pf_inscricao, + pf.pf_acao, + pf.pf_acao_descricao, + pf.pf_fonte_recursos, + pf.pf_fonte_recursos_descricao, + pf.doc_observacao, + pf.pf_valor_linha, + pf.id_programacao, + pf.id_plano_acao, + pf.tp_pf_tipo_programacao, + pf.tx_minuta_programacao, + pf.tx_numero_programacao, + pf.tx_situacao_programacao, + pf.tx_observacao_programacao, + pf.ug_emitente_programacao, + pf.ug_favorecida_programacao, + pf.dh_recebimento_programacao, + pf.pf_chave, + pa.id_programa, + pa.sigla_unidade_descentralizada, + pa.unidade_descentralizada, + pa.sigla_unidade_responsavel_execucao, + pa.unidade_responsavel_execucao, + pa.vl_total_plano_acao, + pa.dt_inicio_vigencia, + pa.dt_fim_vigencia, + pa.tx_objeto_plano_acao, + pa.tx_justificativa_plano_acao, + pa.in_forma_execucao_direta, + pa.in_forma_execucao_particulares, + pa.in_forma_execucao_descentralizada, + pa.tx_situacao_plano_acao, + pa.aa_ano_plano_acao, + pa.vl_beneficiario_especifico, + pa.vl_chamamento_publico, + pa.sq_instrumento, + pa.aa_instrumento, + coalesce( + greatest(pf.dt_ingest, pa.dt_ingest_plano_acao), + pf.dt_ingest, + pa.dt_ingest_plano_acao + ) as dt_ingest +from pf_unificado pf +left join planos_acao_deduplicado pa using (id_plano_acao) diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/schema.yml b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/schema.yml new file mode 100644 index 00000000..2cf5b406 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/silver/schema.yml @@ -0,0 +1,442 @@ +version: 2 + +models: + - name: pf_unificado + description: > + Tabela silver que unifica as programacoes financeiras do Tesouro Gerencial + (pf_tesouro) e do Transferegov (pf_transfere), realizando o cruzamento pela + chave normalizada da PF. A chave e formada pelos ultimos 12 caracteres do campo + pf no Tesouro e comparada com tx_numero_programacao no Transferegov. + meta: + tags: + - silver + columns: + - name: emissao_mes + description: "Mes de emissao da programacao financeira no Tesouro." + - name: emissao_dia + description: "Data de emissao da programacao financeira no Tesouro." + - name: ug_emitente + description: "Codigo da UG emitente no Tesouro Gerencial." + - name: ug_emitente_descricao + description: "Descricao da UG emitente no Tesouro Gerencial." + - name: ug_favorecido + description: "Codigo da UG favorecida no Tesouro Gerencial." + - name: ug_favorecido_descricao + description: "Descricao da UG favorecida no Tesouro Gerencial." + - name: pf_evento + description: "Codigo do evento da programacao financeira no Tesouro." + - name: pf_evento_descricao + description: "Descricao do evento da programacao financeira no Tesouro." + - name: pf + description: "Identificador completo da PF no Tesouro Gerencial." + - name: pf_inscricao + description: "Inscricao/numero associado a PF no Tesouro." + - name: pf_acao + description: "Codigo da acao vinculada a PF no Tesouro." + - name: pf_acao_descricao + description: "Descricao da acao vinculada a PF no Tesouro." + - name: pf_fonte_recursos + description: "Codigo da fonte de recursos da PF no Tesouro." + - name: pf_fonte_recursos_descricao + description: "Descricao da fonte de recursos da PF no Tesouro." + - name: doc_observacao + description: "Observacao textual do documento no Tesouro." + - name: pf_valor_linha + description: "Valor financeiro da linha da PF no Tesouro." + - name: id_programacao + description: "Identificador da programacao financeira no Transferegov." + - name: id_plano_acao + description: "Identificador do plano de acao no Transferegov." + - name: tp_pf_tipo_programacao + description: "Tipo da programacao financeira no Transferegov." + - name: tx_minuta_programacao + description: "Numero/texto da minuta de programacao no Transferegov." + - name: tx_numero_programacao + description: "Numero da programacao financeira no Transferegov." + - name: tx_situacao_programacao + description: "Situacao da programacao financeira no Transferegov." + - name: tx_observacao_programacao + description: "Observacao/objeto da programacao no Transferegov." + - name: ug_emitente_programacao + description: "Codigo da UG emitente da programacao no Transferegov." + - name: ug_favorecida_programacao + description: "Codigo da UG favorecida da programacao no Transferegov." + - name: dh_recebimento_programacao + description: "Timestamp de recebimento/cadastro da programacao no Transferegov." + - name: pf_chave + description: "Chave normalizada usada no join entre Tesouro e Transferegov." + - name: dt_ingest + description: "Maior timestamp de ingestao entre as fontes (UTC-3)." + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_unificado' + nome_coluna: 'emissao_dia' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_unificado' + nome_coluna: 'pf_valor_linha' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_unificado' + nome_coluna: 'id_programacao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_unificado' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + + - name: pf_unificado_planos_acao + description: > + Tabela silver que enriquece a base pf_unificado com os dados de planos de acao + do Transferegov, por meio do id_plano_acao. O cruzamento preserva todas as + linhas de PF e deduplica planos de acao por id_plano_acao, mantendo o registro + mais recente por dt_ingest. + meta: + tags: + - silver + columns: + - name: id_programacao + description: "Identificador da programacao financeira no Transferegov." + - name: id_plano_acao + description: "Identificador do plano de acao usado como chave de cruzamento." + - name: id_programa + description: "Identificador do programa associado ao plano de acao." + - name: pf + description: "Identificador completo da programacao financeira no Tesouro." + - name: tx_numero_programacao + description: "Numero da programacao financeira no Transferegov." + - name: tx_situacao_plano_acao + description: "Situacao do plano de acao no Transferegov." + - name: vl_total_plano_acao + description: "Valor total informado para o plano de acao." + - name: dt_ingest + description: "Maior timestamp de ingestao entre pf_unificado e planos_acao_ted." + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_unificado_planos_acao' + nome_coluna: 'id_programacao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_unificado_planos_acao' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_unificado_planos_acao' + nome_coluna: 'id_programa' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.pf_unificado_planos_acao' + nome_coluna: 'vl_total_plano_acao' + tipo_esperado: 'numeric' + + - name: nc_unificado + description: > + Tabela silver que parte da nc_tesouro_mir e enriquece os campos de programa e ação + com mapeamento por PTRES (programa_por_ptres/pf_ptres). + O modelo mantém o layout principal de NC do SIAFI e aplica COALESCE para preencher + programa_governo, programa_governo_descricao, acao_governo e acao_governo_descricao + quando estiverem ausentes na base original. + meta: + tags: + - silver + columns: + - name: programa_governo + description: "Código do programa de governo (SIAFI com fallback do mapeamento por PTRES)." + - name: programa_governo_descricao + description: "Descrição do programa de governo (SIAFI com fallback do mapeamento por PTRES)." + - name: acao_governo + description: "Código da ação de governo (SIAFI com fallback do mapeamento por PTRES)." + - name: acao_governo_descricao + description: "Descrição da ação de governo (SIAFI com fallback do mapeamento por PTRES)." + - name: nc + description: "Número identificador da Nota de Crédito (NC)." + - name: nc_transferencia + description: "Número/identificador de transferência da NC." + - name: nc_fonte_recursos + description: "Código da fonte de recursos." + - name: nc_fonte_recursos_descricao + description: "Descrição da fonte de recursos." + - name: ptres + description: "Programa de Trabalho Resumido (PTRES) no SIAFI." + - name: nc_evento_descricao + description: "Descrição do evento (mapeada para Tipo NC na harmonização de layouts)." + - name: nc_ug_responsavel + description: "Código da UG responsável." + - name: nc_ug_responsavel_descricao + description: "Nome da UG responsável." + - name: nc_natureza_despesa + description: "Código da natureza de despesa." + - name: nc_natureza_despesa_descricao + description: "Descrição da natureza de despesa." + - name: nc_plano_interno + description: "Código do plano interno." + - name: nc_plano_interno_descricao1 + description: "Descrição 1 do plano interno." + - name: favorecido_doc + description: "Documento (CPF/CNPJ) do favorecido." + - name: favorecido_doc_descricao + description: "Nome/razão social do favorecido." + - name: favorecido_municipio + description: "Código do município do favorecido." + - name: favorecido_municipio_descricao + description: "Descrição do município do favorecido." + - name: valor_celula + description: "Valor financeiro da célula/linha da NC no SIAFI." + - name: movimento_liquido_moeda_origem + description: "Movimento líquido na moeda de origem." + - name: descricao + description: "Descrição geral da NC (quando disponível)." + - name: nc_plano_interno_descricao2 + description: "Descrição 2 do plano interno." + - name: nc_evento + description: "Código do evento contábil (quando disponível)." + - name: nc_item_detalhamento + description: "Detalhamento da NC (S/N), quando disponível." + - name: emissao_dia + description: "Data de emissão da Nota de Crédito (SIAFI, quando disponível)." + - name: emissao_mes + description: "Mês de emissão da Nota de Crédito (SIAFI, quando disponível)." + - name: emissao_ano + description: "Ano de emissão da Nota de Crédito (SIAFI, quando disponível)." + - name: ro + description: "Registro de operação associado." + - name: dc + description: "Indicador débito/crédito." + - name: total_lista + description: "Valor total da lista." + - name: esfera_orcamentaria_codigo + description: "Código da esfera orçamentária." + - name: esfera_orcamentaria_nome + description: "Nome da esfera orçamentária." + - name: dt_ingest + description: "Timestamp de ingestão do SIAFI (UTC-3)." + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.nc_unificado' + nome_coluna: 'valor_celula' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.nc_unificado' + nome_coluna: 'emissao_dia' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.nc_unificado' + nome_coluna: 'dt_ingest' + tipo_esperado: 'timestamp with time zone' + + - name: nc_plano_acao + description: > + Tabela silver que parte de nc_unificado e enriquece com o identificador do Plano de Ação + do TransfereGov (id_plano_acao) via a ponte num_transf_n_plano_acao (nc_transferencia -> plano_acao). + Filtra registros com nc_transferencia = '-8' (NCs sem vínculo de transferência válido). + Retorna todas as colunas de nc_unificado acrescidas de id_plano_acao, que pode ser nulo + quando a NC do SIAFI não possui correspondência no TransfereGov. + meta: + tags: + - silver + columns: + - name: id_plano_acao + description: > + Identificador do Plano de Ação no TransfereGov (integer), obtido via ponte + num_transf_n_plano_acao pelo nc_transferencia. Nulo quando a NC não possui + correspondência no TransfereGov. + - name: programa_governo + description: "Código do programa de governo (SIAFI, somente dados pré-2026; enriquecido por PTRES via nc_unificado)." + - name: programa_governo_descricao + description: "Descrição do programa de governo (SIAFI, somente dados pré-2026; enriquecido por PTRES via nc_unificado)." + - name: acao_governo + description: "Código da ação de governo (SIAFI, somente dados pré-2026; enriquecido por PTRES via nc_unificado)." + - name: acao_governo_descricao + description: "Descrição da ação de governo (SIAFI, somente dados pré-2026; enriquecido por PTRES via nc_unificado)." + - name: nc + description: "Número completo da Nota de Crédito no SIAFI. Primeiros 6 dígitos = UG emitente; últimos 12 = identificador da NC." + - name: nc_transferencia + description: "Número da transferência TED associado à NC — chave de ligação com a ponte num_transf_n_plano_acao." + - name: nc_fonte_recursos + description: "Código da fonte de recursos da NC (pré-2026: nc_fonte_recursos; pós-2026: fonte_codigo)." + - name: nc_fonte_recursos_descricao + description: "Descrição da fonte de recursos (pré-2026: nc_fonte_recursos_descricao; pós-2026: fonte_nome)." + - name: ptres + description: "Programa de Trabalho Resumido (PTRES) vinculado à NC no SIAFI." + - name: nc_evento_descricao + description: "Descrição do evento contábil (pré-2026: nc_evento_descricao; pós-2026: tipo_nc)." + - name: nc_ug_responsavel + description: "Código da UG responsável pela NC (pré-2026: nc_ug_responsavel; pós-2026: emitente_codigo)." + - name: nc_ug_responsavel_descricao + description: "Nome da UG responsável (pré-2026: nc_ug_responsavel_descricao; pós-2026: emitente_nome)." + - name: nc_natureza_despesa + description: "Código da natureza de despesa (pré-2026: nc_natureza_despesa; pós-2026: gnd_codigo)." + - name: nc_natureza_despesa_descricao + description: "Descrição da natureza de despesa (pré-2026: nc_natureza_despesa_descricao; pós-2026: gnd_nome)." + - name: nc_plano_interno + description: "Código do plano interno da NC (pré-2026: nc_plano_interno; pós-2026: pi_codigo)." + - name: nc_plano_interno_descricao1 + description: "Descrição principal do plano interno (pré-2026: nc_plano_interno_descricao1; pós-2026: pi_nome)." + - name: nc_plano_interno_descricao2 + description: "Descrição secundária do plano interno. Somente dados pré-2026; nulo para pós-2026." + - name: favorecido_doc + description: "CPF/CNPJ do favorecido (pré-2026: favorecido_doc; pós-2026: favorecido_codigo)." + - name: favorecido_doc_descricao + description: "Nome ou razão social do favorecido (pré-2026: favorecido_doc_descricao; pós-2026: favorecido_nome)." + - name: favorecido_municipio + description: "Código do município do favorecido. Somente dados pré-2026; nulo para pós-2026." + - name: favorecido_municipio_descricao + description: "Nome do município do favorecido. Somente dados pré-2026; nulo para pós-2026." + - name: valor_celula + description: "Valor financeiro da linha da NC (numeric). Pré-2026: nc_valor_linha; pós-2026: valor_celula. Sem correção de sinal." + - name: movimento_liquido_moeda_origem + description: "Movimento líquido na moeda de origem. Pré-2026: campo homônimo; pós-2026: total_lista." + - name: descricao + description: "Descrição geral da NC. Somente dados pós-2026; nulo para pré-2026." + - name: nc_evento + description: "Código do evento contábil da NC. Somente dados pré-2026; nulo para pós-2026. Usado no gold para classificar orcamento_recebido/devolvido." + - name: nc_item_detalhamento + description: "Indicador S/N de detalhamento do item da NC. Somente dados pós-2026; nulo para pré-2026." + - name: emissao_dia + description: "Data de emissão da NC (date). Somente dados pós-2026; nulo para pré-2026." + - name: emissao_mes + description: "Mês de emissão da NC (text). Somente dados pós-2026; nulo para pré-2026." + - name: emissao_ano + description: "Ano de emissão da NC (text). Somente dados pós-2026; nulo para pré-2026." + - name: ro + description: "Registro de Operação associado à NC. Somente dados pós-2026; nulo para pré-2026." + - name: dc + description: "Indicador Débito/Crédito da NC. Somente dados pós-2026; nulo para pré-2026." + - name: total_lista + description: "Valor total da lista de itens da NC (numeric). Somente dados pós-2026; nulo para pré-2026." + - name: esfera_orcamentaria_codigo + description: "Código da esfera orçamentária. Somente dados pós-2026; nulo para pré-2026." + - name: esfera_orcamentaria_nome + description: "Nome da esfera orçamentária. Somente dados pós-2026; nulo para pré-2026." + - name: dt_ingest + description: "Timestamp de ingestão do SIAFI (timestamptz, UTC-3)." + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.nc_plano_acao' + nome_coluna: 'id_plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.nc_plano_acao' + nome_coluna: 'valor_celula' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.nc_plano_acao' + nome_coluna: 'emissao_dia' + tipo_esperado: 'date' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.nc_plano_acao' + nome_coluna: 'dt_ingest' + tipo_esperado: 'timestamp with time zone' + + - name: empenhos_por_plano_acao + description: > + Tabela silver que vincula as Notas de Empenho do SIAFI aos Planos de Ação do TransfereGov. + O vínculo é desafiador porque a NE não possui um campo estruturado para o número de + transferência — ele está embutido no campo livre ne_ccor_descricao ou em campos auxiliares. + O modelo aplica 9 métodos sequenciais de extração via regex, processando cada NE apenas + uma vez pelo primeiro método que obtiver resultado. Ao final, cruza o num_transf encontrado + com a view num_transf_n_plano_acao para obter o plano_acao correspondente. + meta: + tags: + - silver + columns: + - name: emissao_mes + description: "Mês de emissão da NE no SIAFI" + - name: emissao_dia + description: "Data de emissão da NE no SIAFI" + - name: ne_ccor + description: "Chave completa da Nota de Empenho no SIAFI" + - name: ne_num_processo + description: "Número do processo administrativo associado à NE, limpo de pontos, barras e hifens." + - name: ne_info_complementar + description: "Campo complementar da NE. Usado pelo método 5 de extração: quando contém exatamente 6 dígitos ou padrão 1XXXXX, é tratado como num_transf." + - name: ne_ccor_descricao + description: "Descrição livre da NE — campo principal de onde os métodos 1 a 5 e 7 extraem o num_transf via regex." + - name: doc_observacao + description: "Campo de observação do documento associado à NE. Usado pelo método 8 de extração via padrão 'TED NNN/AAAA'." + - name: natureza_despesa + description: "Código da natureza de despesa da NE (ex: 339030)." + - name: natureza_despesa_descricao + description: "Descrição da natureza de despesa da NE." + - name: ne_ccor_favorecido + description: "CNPJ ou CPF do favorecido do empenho, padronizado em maiúsculas." + - name: ne_ccor_favorecido_descricao + description: "Nome ou razão social do favorecido da NE." + - name: ne_ccor_ano_emissao + description: "Ano de emissão da NE (integer, YYYY), validado via regex para garantir 4 dígitos numéricos." + - name: ptres + description: "Programa de Trabalho Resumido (PTRES) vinculado à NE no SIAFI." + - name: fonte_recursos_detalhada + description: "Código detalhado da fonte de recursos da NE." + - name: fonte_recursos_detalhada_descricao + description: "Descrição da fonte de recursos. Usado pelo método 4 de extração do num_transf via regex no padrão 'TED: NNNNNN'." + - name: despesas_empenhadas + description: "Valor empenhado da NE (numeric 15,2). Positivo = novo empenho; negativo = anulação de empenho." + - name: despesas_liquidadas + description: "Valor liquidado (numeric 15,2) — despesas com bens ou serviços confirmados como entregues." + - name: despesas_pagas + description: "Valor efetivamente pago no exercício corrente (numeric 15,2)." + - name: restos_a_pagar_inscritos + description: "Valor inscrito em Restos a Pagar (numeric 15,2) — empenhado e não pago até o encerramento do exercício." + - name: restos_a_pagar_pagos + description: "Valor de Restos a Pagar efetivamente quitado (numeric 15,2)." + - name: dt_ingest + description: "Timestamp de ingestão da NE no SIAFI (timestamptz, UTC-3)." + - name: ne + description: "Identificador curto da Nota de Empenho — últimos 12 caracteres de ne_ccor." + - name: orgao_id + description: "Código do órgão emitente da NE — primeiros 6 caracteres de ne_ccor." + - name: nc + description: "Número da Nota de Crédito associada à NE, extraído de ne_ccor_descricao via regex e formatado pela UDF format_nc." + - name: num_transf + description: "Número da transferência TED extraído da NE pelos métodos 1 a 9. Chave de ligação com a ponte num_transf_n_plano_acao." + - name: metodo + description: > + Identifica qual método resolveu o vínculo da NE. Valores possíveis: + 'sem vinculo' (NEs com TED explícito sem número), 'metodo 1' a 'metodo 9', + 'ted ou nc invalido' e 'vinculo nao encontrado' (sem vínculo identificado). + - name: complemento_ted + description: "Complemento textual do TED extraído antes do número (ex: nome do programa), produzido pelos métodos 7 e 8." + - name: num_ted + description: "Número bruto do TED extraído do campo de texto pelos métodos 7 e 8, antes da normalização de ano." + - name: metodo_ted + description: "Indica a origem do numero_ted_normalizado: 'metodo 1' (ne_ccor_descricao) ou 'metodo 2' (doc_observacao)." + - name: numero_base + description: "Parte numérica do TED sem o ano, extraída de num_ted para o processo de normalização." + - name: ano_raw + description: "Ano bruto extraído de num_ted antes da normalização (pode ser 2 ou 4 dígitos)." + - name: ano_normalizado + description: "Ano com 4 dígitos após normalização: anos de 2 dígitos <= 30 viram '20XX'; > 30 viram '19XX'." + - name: ano_oficial + description: "Ano mais recente observado para o mesmo (orgao_id, numero_base), usado para preencher NEs sem ano explícito." + - name: numero_ted_normalizado + description: "Número do TED no formato canônico 'numero_base/ano_oficial' (ex: '42/2024'). Nulo quando nenhum ano pôde ser determinado com numero_base curto (<= 2 dígitos)." + - name: plano_acao + description: "Identificador do Plano de Ação no TransfereGov (integer), obtido via ponte num_transf_n_plano_acao a partir do num_transf. Nulo para NEs sem correspondência." + - name: num_transf_pa + description: "num_transf conforme registrado na ponte num_transf_n_plano_acao — usado para conferência cruzada com o num_transf extraído da NE." + tests: + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.empenhos_por_plano_acao' + nome_coluna: 'despesas_empenhadas' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.empenhos_por_plano_acao' + nome_coluna: 'despesas_liquidadas' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.empenhos_por_plano_acao' + nome_coluna: 'restos_a_pagar_inscritos' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.empenhos_por_plano_acao' + nome_coluna: 'ne_ccor_ano_emissao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.empenhos_por_plano_acao' + nome_coluna: 'plano_acao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siafi_dbt.empenhos_por_plano_acao' + nome_coluna: 'dt_ingest' + tipo_esperado: 'timestamp with time zone' diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/views/num_transf_n_plano_acao.sql b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/views/num_transf_n_plano_acao.sql new file mode 100644 index 00000000..ef212aff --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/views/num_transf_n_plano_acao.sql @@ -0,0 +1,66 @@ +{{ config(materialized="view") }} + +with + + nc_transfere_gov as ( + select distinct id_plano_acao, tx_numero_nota as nc, ndc.cd_ug_emitente_nota as ug + from {{ source("transfere_gov", "notas_de_credito") }} ndc + where ndc.tx_numero_nota is not null + ), + + nc_siafi as ( + select distinct + left(nc, 6) as ug, right(nc, 12) as nc, nt.nc_transferencia as num_transf + from {{ref("nc_tesouro_mir")}} nt + where nc_transferencia != '-8' + ), + + joined as ( + select distinct num_transf, id_plano_acao as plano_acao + from nc_siafi + left join nc_transfere_gov using (nc, ug) + ), + + ranked as ( + select + *, + row_number() over ( + partition by num_transf + order by case when plano_acao is not null then 1 else 2 end + ) as rn + from joined + ), + + via_nc as ( + select num_transf, plano_acao + from ranked + where rn = 1 + ), + + via_sq_instrumento as ( + select + sq_instrumento as num_transf, + id_plano_acao::text as plano_acao + from {{ ref("planos_acao_ted") }} + where sq_instrumento is not null + ), + + unificado as ( + select * from via_nc + union + select * from via_sq_instrumento + ), + + final as ( + select + *, + row_number() over ( + partition by num_transf + order by case when plano_acao is not null then 1 else 2 end + ) as rn + from unificado + ) + +select num_transf, plano_acao +from final +where rn = 1 diff --git a/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/views/schema.yml b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/views/schema.yml new file mode 100644 index 00000000..c661aa0a --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/empenhos_ted_dbt/views/schema.yml @@ -0,0 +1,37 @@ +version: 2 + +models: + + - name: num_transf_n_plano_acao + description: > + View de mapeamento entre o número de transferência do SIAFI (num_transf) + e o identificador do Plano de Ação do TransfereGov (plano_acao / id_plano_acao). + É a ponte central que conecta os dois sistemas. + + O mapeamento é construído por dois caminhos complementares, depois unificados via UNION: + + 1. Via NC: cruza nc_tesouro_mir (SIAFI) com transfere_gov.notas_de_credito pela chave + composta (nc, ug_emitente). Quando a mesma Nota de Crédito aparece nos dois sistemas, + o id_plano_acao do TransfereGov é associado ao nc_transferencia do SIAFI. + + 2. Via sq_instrumento: lê planos_acao_ted diretamente, onde o campo sq_instrumento + já contém o num_transf. Captura planos que ainda não têm NC emitida ou cujas NCs + não cruzam pelo caminho 1. + + Quando o mesmo num_transf aparece nos dois caminhos com plano_acao distintos, + um row_number prioriza o registro com plano_acao não-nulo. + meta: + tags: + - view + columns: + - name: num_transf + description: > + Número da transferência TED conforme registrado no SIAFI (nc_transferencia) + ou no TransfereGov (sq_instrumento). Chave de entrada usada por NCs, NEs e PFs + para referenciar uma transferência específica. + - name: plano_acao + description: > + Identificador do Plano de Ação no TransfereGov (id_plano_acao). Pode ser nulo + quando o num_transf do SIAFI não possui correspondência em nenhum dos dois caminhos + de cruzamento. Tipo text na view — consumidores devem fazer cast para integer + conforme necessário. diff --git a/airflow_lappis/dags/dbt/mir/models/metadata/models_metadata.sql b/airflow_lappis/dags/dbt/mir/models/metadata/models_metadata.sql new file mode 100644 index 00000000..f1981994 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/metadata/models_metadata.sql @@ -0,0 +1,67 @@ +{{ + config( + materialized='incremental', + unique_key=['schema_name', 'table_name'], + on_schema_change='sync_all_columns' + ) +}} + +{# + Tabela de Metadados dos Modelos dbt + =================================== + + Esta tabela armazena metadados de todos os modelos executados no dbt. + + Campos principais: + - schema_name: Schema do modelo + - table_name: Nome da tabela/modelo + - dt_transform: Data da última transformação (quando o modelo foi executado) + - run_id: ID único da execução do dbt + + A tabela é atualizada de forma incremental, mantendo apenas o registro + mais recente para cada combinação de schema + table_name. +#} + +WITH dbt_models AS ( + {# + Usando a função graph do dbt para iterar sobre todos os modelos do projeto. + Isso garante que capturamos metadados de todos os modelos definidos. + #} + {% set models_data = [] %} + + {% for node in graph.nodes.values() %} + {% if node.resource_type == 'model' %} + {% do models_data.append({ + 'schema_name': node.schema, + 'table_name': node.name, + 'database_name': node.database, + 'materialization': node.config.materialized, + 'description': node.description | default('') | replace("'", "''") + }) %} + {% endif %} + {% endfor %} + + {% for model in models_data %} + SELECT + '{{ model.schema_name }}' AS schema_name, + '{{ model.table_name }}' AS table_name, + '{{ model.database_name }}' AS database_name, + '{{ model.materialization }}' AS materialization, + '{{ model.description[:500] }}' AS description, + ('{{ run_started_at }}'::TIMESTAMP AT TIME ZONE 'UTC' AT TIME ZONE 'America/Sao_Paulo') AS dt_transform, + '{{ invocation_id }}' AS run_id + {% if not loop.last %} + UNION ALL + {% endif %} + {% endfor %} +) + +SELECT + schema_name, + table_name, + database_name, + materialization, + description, + dt_transform, + run_id +FROM dbt_models diff --git a/airflow_lappis/dags/dbt/mir/models/metadata/schema.yml b/airflow_lappis/dags/dbt/mir/models/metadata/schema.yml new file mode 100644 index 00000000..e4fce0ab --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/metadata/schema.yml @@ -0,0 +1,46 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dbt-labs/dbt-jsonschema/main/schemas/latest/dbt_yml_files-latest.json + +version: 2 + +models: + - name: models_metadata + description: > + Tabela central de metadados que armazena informações sobre todos os modelos dbt executados. + Cada linha representa um modelo único, identificado pela combinação de schema e table_name. + A tabela é atualizada de forma incremental, mantendo histórico das execuções. + meta: + tags: + - metadata + - governance + columns: + - name: schema_name + description: Nome do schema onde o modelo está localizado. + tests: + - not_null + + - name: table_name + description: Nome da tabela/modelo. + tests: + - not_null + + - name: database_name + description: Nome do banco de dados onde o modelo está materializado. + + - name: materialization + description: Tipo de materialização do modelo (table, view, incremental, etc). + + - name: description + description: Descrição do modelo extraída do schema.yml. + + - name: dt_transform + description: > + Data e hora em que o modelo foi transformado/executado pela última vez. + Corresponde ao momento em que a execução do dbt foi iniciada (run_started_at). + Timezone: America/Sao_Paulo (UTC-3). + tests: + - not_null + + - name: run_id + description: > + Identificador único da execução do dbt (invocation_id). + Permite rastrear qual execução gerou a transformação. diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/convenio.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/convenio.sql new file mode 100644 index 00000000..4fb820bf --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/convenio.sql @@ -0,0 +1,50 @@ +{{ config(materialized="table") }} + +with + convenio_raw as ( + select + nullif(nr_convenio, '')::integer as nr_convenio, + nullif(id_proposta, '')::integer as id_proposta, + nullif(dia, '')::integer as dia, + nullif(mes, '')::integer as mes, + nullif(ano, '')::integer as ano, + to_date(nullif(dia_assin_conv, ''), 'DD/MM/YYYY') as dia_assin_conv, + sit_convenio::text as sit_convenio, + subsituacao_conv::text as subsituacao_conv, + situacao_publicacao::text as situacao_publicacao, + instrumento_ativo::text as instrumento_ativo, + ind_opera_obtv::text as ind_opera_obtv, + nr_processo::text as nr_processo, + nullif(ug_emitente, '')::integer as ug_emitente, + to_date(nullif(dia_publ_conv, ''), 'DD/MM/YYYY') as dia_publ_conv, + to_date(nullif(dia_inic_vigenc_conv, ''), 'DD/MM/YYYY') as dia_inic_vigenc_conv, + to_date(nullif(dia_fim_vigenc_conv, ''), 'DD/MM/YYYY') as dia_fim_vigenc_conv, + to_date(nullif(dia_fim_vigenc_original_conv, ''), 'DD/MM/YYYY') as dia_fim_vigenc_original_conv, + nullif(dias_prest_contas, '')::integer as dias_prest_contas, + to_date(nullif(dia_limite_prest_contas, ''), 'DD/MM/YYYY') as dia_limite_prest_contas, + to_date(nullif(data_suspensiva, ''), 'DD/MM/YYYY') as data_suspensiva, + to_date(nullif(data_retirada_suspensiva, ''), 'DD/MM/YYYY') as data_retirada_suspensiva, + nullif(dias_clausula_suspensiva, '')::integer as dias_clausula_suspensiva, + situacao_contratacao::text as situacao_contratacao, + ind_assinado::text as ind_assinado, + motivo_suspensao::text as motivo_suspensao, + ind_foto::text as ind_foto, + nullif(qtde_convenios, '')::integer as qtde_convenios, + nullif(qtd_ta, '')::integer as qtd_ta, + nullif(qtd_prorroga, '')::integer as qtd_prorroga, + replace(nullif(vl_global_conv, ''), ',', '.')::numeric(15, 2) as vl_global_conv, + replace(nullif(vl_repasse_conv, ''), ',', '.')::numeric(15, 2) as vl_repasse_conv, + replace(nullif(vl_contrapartida_conv, ''), ',', '.')::numeric(15, 2) as vl_contrapartida_conv, + replace(nullif(vl_empenhado_conv, ''), ',', '.')::numeric(15, 2) as vl_empenhado_conv, + replace(nullif(vl_desembolsado_conv, ''), ',', '.')::numeric(15, 2) as vl_desembolsado_conv, + replace(nullif(vl_saldo_reman_tesouro, ''), ',', '.')::numeric(15, 2) as vl_saldo_reman_tesouro, + replace(nullif(vl_saldo_reman_convenente, ''), ',', '.')::numeric(15, 2) as vl_saldo_reman_convenente, + replace(nullif(vl_rendimento_aplicacao, ''), ',', '.')::numeric(15, 2) as vl_rendimento_aplicacao, + replace(nullif(vl_ingresso_contrapartida, ''), ',', '.')::numeric(15, 2) as vl_ingresso_contrapartida, + replace(nullif(vl_saldo_conta, ''), ',', '.')::numeric(15, 2) as vl_saldo_conta, + replace(nullif(valor_global_original_conv, ''), ',', '.')::numeric(15, 2) as valor_global_original_conv + from {{ source("siconv", "convenio") }} + ) + +select * +from convenio_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/cronograma_desembolso.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/cronograma_desembolso.sql new file mode 100644 index 00000000..c6ab4554 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/cronograma_desembolso.sql @@ -0,0 +1,17 @@ +{{ config(materialized="table") }} + +with + cronograma_desembolso_raw as ( + select + nullif(id_proposta, '')::integer as id_proposta, + nullif(nr_convenio, '')::integer as nr_convenio, + nullif(nr_parcela_crono_desembolso, '')::integer as nr_parcela_crono_desembolso, + nullif(mes_crono_desembolso, '')::integer as mes_crono_desembolso, + nullif(ano_crono_desembolso, '')::integer as ano_crono_desembolso, + tipo_resp_crono_desembolso::text as tipo_resp_crono_desembolso, + replace(nullif(valor_parcela_crono_desembolso, ''), ',', '.')::numeric(15, 2) as valor_parcela_crono_desembolso + from {{ source("siconv", "cronograma_desembolso") }} + ) + +select * +from cronograma_desembolso_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/desbloqueio.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/desbloqueio.sql new file mode 100644 index 00000000..937ed2bd --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/desbloqueio.sql @@ -0,0 +1,18 @@ +{{ config(materialized="table") }} + +with + desbloqueio_raw as ( + select + nr_convenio::integer as nr_convenio, + nr_ob::text as nr_ob, + to_date(nullif(data_cadastro, ''), 'DD/MM/YYYY') as data_cadastro, + to_date(nullif(data_envio, ''), 'DD/MM/YYYY') as data_envio, + tipo_recurso_desbloqueio::text as tipo_recurso_desbloqueio, + replace(nullif(vl_total_desbloqueio, ''), ',', '.')::numeric(15, 2) as vl_total_desbloqueio, + replace(nullif(vl_desbloqueado, ''), ',', '.')::numeric(15, 2) as vl_desbloqueado, + replace(nullif(vl_bloqueado, ''), ',', '.')::numeric(15, 2) as vl_bloqueado + from {{ source("siconv", "desbloqueio") }} + ) + +select * +from desbloqueio_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/desembolso.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/desembolso.sql new file mode 100644 index 00000000..58068e78 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/desembolso.sql @@ -0,0 +1,21 @@ +{{ config(materialized="table") }} + +with + desembolso_raw as ( + select + nullif(id_desembolso, '')::integer as id_desembolso, + nullif(nr_convenio, '')::integer as nr_convenio, + to_date(nullif(dt_ult_desembolso, ''), 'DD/MM/YYYY') as dt_ult_desembolso, + nullif(qtd_dias_sem_desembolso, '')::integer as qtd_dias_sem_desembolso, + to_date(nullif(data_desembolso, ''), 'DD/MM/YYYY') as data_desembolso, + nullif(ano_desembolso, '')::integer as ano_desembolso, + nullif(mes_desembolso, '')::integer as mes_desembolso, + nr_siafi::text as nr_siafi, + nullif(ug_emitente_dh, '')::integer as ug_emitente_dh, + observacao_dh::text as observacao_dh, + replace(nullif(vl_desembolsado, ''), ',', '.')::numeric(15, 2) as vl_desembolsado + from {{ source("siconv", "desembolso") }} + ) + +select * +from desembolso_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/empenho.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/empenho.sql new file mode 100644 index 00000000..4006f84a --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/empenho.sql @@ -0,0 +1,28 @@ +{{ config(materialized="table") }} + +with + empenho_raw as ( + select + nullif(id_empenho, '')::integer as id_empenho, + nullif(nr_convenio, '')::integer as nr_convenio, + nr_empenho::text as nr_empenho, + tipo_nota::text as tipo_nota, + desc_tipo_nota::text as desc_tipo_nota, + to_date(nullif(data_emissao, ''), 'DD/MM/YYYY') as data_emissao, + cod_situacao_empenho::text as cod_situacao_empenho, + desc_situacao_empenho::text as desc_situacao_empenho, + nullif(ug_emitente, '')::integer as ug_emitente, + nullif(ug_responsavel, '')::integer as ug_responsavel, + fonte_recurso::text as fonte_recurso, + natureza_despesa::text as natureza_despesa, + plano_interno::text as plano_interno, + ptres::text as ptres, + replace(nullif(valor_empenho, ''), ',', '.')::numeric(15, 2) as valor_empenho, + resultado_primario::text as resultado_primario, + observacao_empenho::text as observacao_empenho, + descricao_emenda_siafi::text as descricao_emenda_siafi + from {{ source("siconv", "empenho") }} + ) + +select * +from empenho_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/historico_situacao.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/historico_situacao.sql new file mode 100644 index 00000000..4a6f9a1f --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/historico_situacao.sql @@ -0,0 +1,16 @@ +{{ config(materialized="table") }} + +with + historico_situacao_raw as ( + select + nullif(id_proposta, '')::integer as id_proposta, + nullif(nr_convenio, '')::integer as nr_convenio, + to_date(nullif(dia_historico_sit, ''), 'DD/MM/YYYY') as dia_historico_sit, + historico_sit::text as historico_sit, + nullif(dias_historico_sit, '')::integer as dias_historico_sit, + nullif(cod_historico_sit, '')::integer as cod_historico_sit + from {{ source("siconv", "historico_situacao") }} + ) + +select * +from historico_situacao_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/ingresso_contrapartida.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/ingresso_contrapartida.sql new file mode 100644 index 00000000..a0eb77db --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/ingresso_contrapartida.sql @@ -0,0 +1,13 @@ +{{ config(materialized="table") }} + +with + ingresso_contrapartida_raw as ( + select + nr_convenio::integer as nr_convenio, + to_date(nullif(dt_ingresso_contrapartida, ''), 'DD/MM/YYYY') as dt_ingresso_contrapartida, + replace(nullif(vl_ingresso_contrapartida, ''), ',', '.')::numeric(15, 2) as vl_ingresso_contrapartida + from {{ source("siconv", "ingresso_contrapartida") }} + ) + +select * +from ingresso_contrapartida_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/licitacao.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/licitacao.sql new file mode 100644 index 00000000..95e53044 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/licitacao.sql @@ -0,0 +1,28 @@ +{{ config(materialized="table") }} + +with + licitacao_raw as ( + select + nullif(id_licitacao, '')::integer as id_licitacao, + nullif(nr_convenio, '')::integer as nr_convenio, + nr_licitacao::text as nr_licitacao, + modalidade_licitacao::text as modalidade_licitacao, + tp_processo_compra::text as tp_processo_compra, + tipo_licitacao::text as tipo_licitacao, + nr_processo_licitacao::text as nr_processo_licitacao, + to_date(nullif(data_publicacao_licitacao, ''), 'DD/MM/YYYY') as data_publicacao_licitacao, + to_date(nullif(data_abertura_licitacao, ''), 'DD/MM/YYYY') as data_abertura_licitacao, + to_date(nullif(data_encerramento_licitacao, ''), 'DD/MM/YYYY') as data_encerramento_licitacao, + to_date(nullif(data_homologacao_licitacao, ''), 'DD/MM/YYYY') as data_homologacao_licitacao, + status_licitacao::text as status_licitacao, + situacao_aceite_processo_execu::text as situacao_aceite_processo_execu, + sistema_origem::text as sistema_origem, + situacao_sistema::text as situacao_sistema, + replace(nullif(valor_licitacao, ''), ',', '.')::numeric(20, 2) as valor_licitacao, + to_date(nullif(data_analise_aceite, ''), 'DD/MM/YYYY') as data_analise_aceite, + to_date(nullif(data_envio_analise, ''), 'DD/MM/YYYY') as data_envio_analise + from {{ source("siconv", "licitacao") }} + ) + +select * +from licitacao_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/meta_crono_fisico.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/meta_crono_fisico.sql new file mode 100644 index 00000000..80b68611 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/meta_crono_fisico.sql @@ -0,0 +1,27 @@ +{{ config(materialized="table") }} + +with + meta_crono_fisico_raw as ( + select + nullif(id_meta, '')::integer as id_meta, + nullif(id_proposta, '')::integer as id_proposta, + nullif(nr_convenio, '')::integer as nr_convenio, + cod_programa::text as cod_programa, + nome_programa::text as nome_programa, + nullif(nr_meta, '')::integer as nr_meta, + tipo_meta::text as tipo_meta, + desc_meta::text as desc_meta, + to_date(nullif(data_inicio_meta, ''), 'DD/MM/YYYY') as data_inicio_meta, + to_date(nullif(data_fim_meta, ''), 'DD/MM/YYYY') as data_fim_meta, + uf_meta::text as uf_meta, + municipio_meta::text as municipio_meta, + endereco_meta::text as endereco_meta, + cep_meta::text as cep_meta, + replace(nullif(qtd_meta, ''), ',', '.')::numeric(15, 2) as qtd_meta, + und_fornecimento_meta::text as und_fornecimento_meta, + replace(nullif(vl_meta, ''), ',', '.')::numeric(15, 2) as vl_meta + from {{ source("siconv", "meta_crono_fisico") }} + ) + +select * +from meta_crono_fisico_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/pagamento.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/pagamento.sql new file mode 100644 index 00000000..6d1bc858 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/pagamento.sql @@ -0,0 +1,29 @@ +{{ config(materialized="table") }} + +with + pagamento_raw as ( + select + nullif(nr_mov_fin, '')::integer as nr_mov_fin, + nullif(nr_convenio, '')::integer as nr_convenio, + identif_fornecedor::text as identif_fornecedor, + nome_fornecedor::text as nome_fornecedor, + tp_mov_financeira::text as tp_mov_financeira, + case + when nullif(data_pag, '') is null then null + when data_pag ~ '^\d{2}/\d{2}/\d{4}$' then to_date(data_pag, 'DD/MM/YYYY') + else to_date(data_pag, 'YYYY-MM-DD') + end as data_pag, + nr_dl::text as nr_dl, + desc_dl::text as desc_dl, + replace(nullif(vl_pago, ''), ',', '.')::numeric(15, 2) as vl_pago, + nullif(id_dl, '')::integer as id_dl, + case + when nullif(data_emissao_dl, '') is null then null + when data_emissao_dl ~ '^\d{2}/\d{2}/\d{4}$' then to_date(data_emissao_dl, 'DD/MM/YYYY') + else to_date(data_emissao_dl, 'YYYY-MM-DD') + end as data_emissao_dl + from {{ source("siconv", "pagamento") }} + ) + +select * +from pagamento_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/pagamento_tributo.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/pagamento_tributo.sql new file mode 100644 index 00000000..9ed96673 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/pagamento_tributo.sql @@ -0,0 +1,13 @@ +{{ config(materialized="table") }} + +with + pagamento_tributo_raw as ( + select + nr_convenio::integer as nr_convenio, + to_date(nullif(data_tributo, ''), 'DD/MM/YYYY') as data_tributo, + replace(nullif(vl_pag_tributos, ''), ',', '.')::numeric(15, 2) as vl_pag_tributos + from {{ source("siconv", "pagamento_tributo") }} + ) + +select * +from pagamento_tributo_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/proposta.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/proposta.sql new file mode 100644 index 00000000..36ada3b4 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/proposta.sql @@ -0,0 +1,46 @@ +{{ config(materialized="table") }} + +with + proposta_raw as ( + select + id_proposta::integer as id_proposta, + uf_proponente::text as uf_proponente, + munic_proponente::text as munic_proponente, + cod_munic_ibge::integer as cod_munic_ibge, + cod_orgao_sup::integer as cod_orgao_sup, + desc_orgao_sup::text as desc_orgao_sup, + natureza_juridica::text as natureza_juridica, + nr_proposta::text as nr_proposta, + dia_prop::integer as dia_prop, + mes_prop::integer as mes_prop, + ano_prop::integer as ano_prop, + to_date(nullif(dia_proposta, ''), 'DD/MM/YYYY') as dia_proposta, + cod_orgao::integer as cod_orgao, + desc_orgao::text as desc_orgao, + modalidade::text as modalidade, + identif_proponente::text as identif_proponente, + nm_proponente::text as nm_proponente, + cep_proponente::text as cep_proponente, + endereco_proponente::text as endereco_proponente, + bairro_proponente::text as bairro_proponente, + nm_banco::text as nm_banco, + situacao_conta::text as situacao_conta, + situacao_projeto_basico::text as situacao_projeto_basico, + sit_proposta::text as sit_proposta, + to_date(nullif(dia_inic_vigencia_proposta, ''), 'DD/MM/YYYY') as dia_inic_vigencia_proposta, + to_date(nullif(dia_fim_vigencia_proposta, ''), 'DD/MM/YYYY') as dia_fim_vigencia_proposta, + objeto_proposta::text as objeto_proposta, + item_investimento::text as item_investimento, + enviada_mandataria::text as enviada_mandataria, + nome_subtipo_proposta::text as nome_subtipo_proposta, + descricao_subtipo_proposta::text as descricao_subtipo_proposta, + replace(nullif(vl_global_prop, ''), ',', '.')::numeric(15, 2) as vl_global_prop, + replace(nullif(vl_repasse_prop, ''), ',', '.')::numeric(15, 2) as vl_repasse_prop, + replace(nullif(vl_contrapartida_prop, ''), ',', '.')::numeric(15, 2) as vl_contrapartida_prop, + cd_agencia::text as cd_agencia, + cd_conta::text as cd_conta + from {{ source("siconv", "proposta") }} + ) + +select * +from proposta_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/prorroga_oficio.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/prorroga_oficio.sql new file mode 100644 index 00000000..d1ea2563 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/prorroga_oficio.sql @@ -0,0 +1,17 @@ +{{ config(materialized="table") }} + +with + prorroga_oficio_raw as ( + select + nullif(nr_convenio, '')::integer as nr_convenio, + nr_prorroga::text as nr_prorroga, + to_date(nullif(dt_inicio_prorroga, ''), 'DD/MM/YYYY') as dt_inicio_prorroga, + to_date(nullif(dt_fim_prorroga, ''), 'DD/MM/YYYY') as dt_fim_prorroga, + nullif(dias_prorroga, '')::integer as dias_prorroga, + to_date(nullif(dt_assinatura_prorroga, ''), 'DD/MM/YYYY') as dt_assinatura_prorroga, + sit_prorroga::text as sit_prorroga + from {{ source("siconv", "prorroga_oficio") }} + ) + +select * +from prorroga_oficio_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/schema.yaml b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/schema.yaml new file mode 100644 index 00000000..fda5b622 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/schema.yaml @@ -0,0 +1,742 @@ +version: 2 + +models: + - name: proposta + description: > + Tabela da camada bronze contendo informações sobre propostas de convênios do SICONV, + com conversão de tipos, padronização de datas e valores financeiros. + meta: + tags: + - bronze + columns: + - name: id_proposta + description: "Identificador único da proposta." + - name: uf_proponente + description: "Unidade Federativa do proponente." + - name: munic_proponente + description: "Município do proponente." + - name: cod_munic_ibge + description: "Código IBGE do município." + - name: cod_orgao_sup + description: "Código do órgão superior." + - name: desc_orgao_sup + description: "Descrição do órgão superior." + - name: natureza_juridica + description: "Natureza jurídica do proponente." + - name: nr_proposta + description: "Número da proposta." + - name: dia_prop + description: "Dia da proposta." + - name: mes_prop + description: "Mês da proposta." + - name: ano_prop + description: "Ano da proposta." + - name: dia_proposta + description: "Data da proposta." + - name: cod_orgao + description: "Código do órgão." + - name: desc_orgao + description: "Descrição do órgão." + - name: modalidade + description: "Modalidade da proposta." + - name: identif_proponente + description: "Identificação do proponente (CNPJ/CPF)." + - name: nm_proponente + description: "Nome do proponente." + - name: cep_proponente + description: "CEP do proponente." + - name: endereco_proponente + description: "Endereço do proponente." + - name: bairro_proponente + description: "Bairro do proponente." + - name: nm_banco + description: "Nome do banco." + - name: situacao_conta + description: "Situação da conta bancária." + - name: situacao_projeto_basico + description: "Situação do projeto básico." + - name: sit_proposta + description: "Situação da proposta." + - name: dia_inic_vigencia_proposta + description: "Data de início da vigência da proposta." + - name: dia_fim_vigencia_proposta + description: "Data de fim da vigência da proposta." + - name: objeto_proposta + description: "Objeto da proposta." + - name: item_investimento + description: "Item de investimento." + - name: enviada_mandataria + description: "Indica se foi enviada à mandatária." + - name: nome_subtipo_proposta + description: "Nome do subtipo da proposta." + - name: descricao_subtipo_proposta + description: "Descrição do subtipo da proposta." + - name: vl_global_prop + description: "Valor global da proposta." + - name: vl_repasse_prop + description: "Valor de repasse da proposta." + - name: vl_contrapartida_prop + description: "Valor de contrapartida da proposta." + - name: cd_agencia + description: "Código da agência bancária." + - name: cd_conta + description: "Código da conta bancária." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.proposta' + nome_coluna: 'id_proposta' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.proposta' + nome_coluna: 'vl_global_prop' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.proposta' + nome_coluna: 'dia_proposta' + tipo_esperado: 'date' + + - name: convenio + description: > + Tabela da camada bronze contendo informações sobre convênios do SICONV, + com conversão de tipos, padronização de datas e valores financeiros. + meta: + tags: + - bronze + columns: + - name: nr_convenio + description: "Número do convênio." + - name: id_proposta + description: "Identificador da proposta associada." + - name: dia + description: "Dia de assinatura do convênio." + - name: mes + description: "Mês de assinatura do convênio." + - name: ano + description: "Ano de assinatura do convênio." + - name: dia_assin_conv + description: "Data de assinatura do convênio." + - name: sit_convenio + description: "Situação do convênio." + - name: subsituacao_conv + description: "Subsituação do convênio." + - name: situacao_publicacao + description: "Situação de publicação do convênio." + - name: instrumento_ativo + description: "Indica se o instrumento está ativo." + - name: ind_opera_obtv + description: "Indicador de operação OBTV." + - name: nr_processo + description: "Número do processo." + - name: ug_emitente + description: "Código da UG emitente." + - name: dia_publ_conv + description: "Data de publicação do convênio." + - name: dia_inic_vigenc_conv + description: "Data de início da vigência do convênio." + - name: dia_fim_vigenc_conv + description: "Data de fim da vigência do convênio." + - name: dia_fim_vigenc_original_conv + description: "Data de fim da vigência original do convênio." + - name: dias_prest_contas + description: "Prazo em dias para prestação de contas." + - name: dia_limite_prest_contas + description: "Data limite para prestação de contas." + - name: data_suspensiva + description: "Data da cláusula suspensiva." + - name: data_retirada_suspensiva + description: "Data de retirada da cláusula suspensiva." + - name: dias_clausula_suspensiva + description: "Dias de cláusula suspensiva." + - name: situacao_contratacao + description: "Situação da contratação." + - name: ind_assinado + description: "Indica se o convênio foi assinado." + - name: motivo_suspensao + description: "Motivo da suspensão do convênio." + - name: ind_foto + description: "Indica se há foto associada." + - name: qtde_convenios + description: "Quantidade de convênios." + - name: qtd_ta + description: "Quantidade de termos aditivos." + - name: qtd_prorroga + description: "Quantidade de prorrogações." + - name: vl_global_conv + description: "Valor global do convênio." + - name: vl_repasse_conv + description: "Valor de repasse do convênio." + - name: vl_contrapartida_conv + description: "Valor de contrapartida do convênio." + - name: vl_empenhado_conv + description: "Valor empenhado do convênio." + - name: vl_desembolsado_conv + description: "Valor desembolsado do convênio." + - name: vl_saldo_reman_tesouro + description: "Valor do saldo remanescente do tesouro." + - name: vl_saldo_reman_convenente + description: "Valor do saldo remanescente do convenente." + - name: vl_rendimento_aplicacao + description: "Valor do rendimento de aplicação." + - name: vl_ingresso_contrapartida + description: "Valor de ingresso de contrapartida." + - name: vl_saldo_conta + description: "Valor do saldo em conta." + - name: valor_global_original_conv + description: "Valor global original do convênio." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.convenio' + nome_coluna: 'nr_convenio' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.convenio' + nome_coluna: 'vl_global_conv' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.convenio' + nome_coluna: 'dia_assin_conv' + tipo_esperado: 'date' + + - name: desembolso + description: > + Tabela da camada bronze contendo informações sobre desembolsos de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: id_desembolso + description: "Identificador único do desembolso." + - name: nr_convenio + description: "Número do convênio associado." + - name: dt_ult_desembolso + description: "Data do último desembolso." + - name: qtd_dias_sem_desembolso + description: "Quantidade de dias sem desembolso." + - name: data_desembolso + description: "Data do desembolso." + - name: ano_desembolso + description: "Ano do desembolso." + - name: mes_desembolso + description: "Mês do desembolso." + - name: nr_siafi + description: "Número SIAFI do desembolso." + - name: ug_emitente_dh + description: "Código da UG emitente do documento hábil." + - name: observacao_dh + description: "Observação do documento hábil." + - name: vl_desembolsado + description: "Valor desembolsado." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.desembolso' + nome_coluna: 'id_desembolso' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.desembolso' + nome_coluna: 'vl_desembolsado' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.desembolso' + nome_coluna: 'data_desembolso' + tipo_esperado: 'date' + + - name: desbloqueio + description: > + Tabela da camada bronze contendo informações sobre desbloqueios de recursos de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: nr_convenio + description: "Número do convênio associado." + - name: nr_ob + description: "Número da ordem bancária." + - name: data_cadastro + description: "Data de cadastro do desbloqueio." + - name: data_envio + description: "Data de envio do desbloqueio." + - name: tipo_recurso_desbloqueio + description: "Tipo de recurso desbloqueado." + - name: vl_total_desbloqueio + description: "Valor total do desbloqueio." + - name: vl_desbloqueado + description: "Valor desbloqueado." + - name: vl_bloqueado + description: "Valor bloqueado." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.desbloqueio' + nome_coluna: 'nr_convenio' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.desbloqueio' + nome_coluna: 'vl_total_desbloqueio' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.desbloqueio' + nome_coluna: 'data_cadastro' + tipo_esperado: 'date' + + - name: solicitacao_alteracao + description: > + Tabela da camada bronze contendo informações sobre solicitações de alteração de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: id_solicitacao + description: "Identificador único da solicitação." + - name: nr_convenio + description: "Número do convênio associado." + - name: nr_solicitacao + description: "Número da solicitação." + - name: situacao_solicitacao + description: "Situação da solicitação." + - name: objeto_solicitacao + description: "Objeto da solicitação." + - name: data_solicitacao + description: "Data da solicitação." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.solicitacao_alteracao' + nome_coluna: 'id_solicitacao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.solicitacao_alteracao' + nome_coluna: 'data_solicitacao' + tipo_esperado: 'date' + + - name: termo_aditivo + description: > + Tabela da camada bronze contendo informações sobre termos aditivos de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: nr_convenio + description: "Número do convênio associado." + - name: id_solicitacao + description: "Identificador da solicitação associada." + - name: numero_ta + description: "Número do termo aditivo." + - name: tipo_ta + description: "Tipo do termo aditivo." + - name: vl_global_ta + description: "Valor global do termo aditivo." + - name: vl_repasse_ta + description: "Valor de repasse do termo aditivo." + - name: vl_contrapartida_ta + description: "Valor de contrapartida do termo aditivo." + - name: dt_assinatura_ta + description: "Data de assinatura do termo aditivo." + - name: dt_inicio_ta + description: "Data de início do termo aditivo." + - name: dt_fim_ta + description: "Data de fim do termo aditivo." + - name: justificativa_ta + description: "Justificativa do termo aditivo." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.termo_aditivo' + nome_coluna: 'nr_convenio' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.termo_aditivo' + nome_coluna: 'vl_global_ta' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.termo_aditivo' + nome_coluna: 'dt_assinatura_ta' + tipo_esperado: 'date' + + - name: solicitacao_rendimento_aplicacao + description: > + Tabela da camada bronze contendo informações sobre solicitações de rendimento de aplicação do SICONV. + meta: + tags: + - bronze + columns: + - name: id_solicitacao_rend_aplicacao + description: "Identificador único da solicitação de rendimento." + - name: nr_convenio + description: "Número do convênio associado." + - name: nr_solicitacao_rend_aplicacao + description: "Número da solicitação de rendimento." + - name: status_solicitacao_rend_aplicacao + description: "Status da solicitação de rendimento." + - name: data_solicitacao_rend_aplicacao + description: "Data da solicitação de rendimento." + - name: valor_solicitacao_rend_aplicacao + description: "Valor solicitado de rendimento." + - name: valor_aprovado_solicitacao_rend_aplicacao + description: "Valor aprovado de rendimento." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.solicitacao_rendimento_aplicacao' + nome_coluna: 'id_solicitacao_rend_aplicacao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.solicitacao_rendimento_aplicacao' + nome_coluna: 'valor_solicitacao_rend_aplicacao' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.solicitacao_rendimento_aplicacao' + nome_coluna: 'data_solicitacao_rend_aplicacao' + tipo_esperado: 'date' + + - name: prorroga_oficio + description: > + Tabela da camada bronze contendo informações sobre prorrogações de ofício de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: nr_convenio + description: "Número do convênio associado." + - name: nr_prorroga + description: "Número da prorrogação." + - name: dt_inicio_prorroga + description: "Data de início da prorrogação." + - name: dt_fim_prorroga + description: "Data de fim da prorrogação." + - name: dias_prorroga + description: "Quantidade de dias de prorrogação." + - name: dt_assinatura_prorroga + description: "Data de assinatura da prorrogação." + - name: sit_prorroga + description: "Situação da prorrogação." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.prorroga_oficio' + nome_coluna: 'nr_convenio' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.prorroga_oficio' + nome_coluna: 'dt_inicio_prorroga' + tipo_esperado: 'date' + + - name: pagamento_tributo + description: > + Tabela da camada bronze contendo informações sobre pagamentos de tributos de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: nr_convenio + description: "Número do convênio associado." + - name: data_tributo + description: "Data do pagamento do tributo." + - name: vl_pag_tributos + description: "Valor pago de tributos." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.pagamento_tributo' + nome_coluna: 'nr_convenio' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.pagamento_tributo' + nome_coluna: 'vl_pag_tributos' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.pagamento_tributo' + nome_coluna: 'data_tributo' + tipo_esperado: 'date' + + - name: pagamento + description: > + Tabela da camada bronze contendo informações sobre pagamentos de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: nr_mov_fin + description: "Número do movimento financeiro." + - name: nr_convenio + description: "Número do convênio associado." + - name: identif_fornecedor + description: "Identificação do fornecedor (CNPJ/CPF)." + - name: nome_fornecedor + description: "Nome do fornecedor." + - name: tp_mov_financeira + description: "Tipo do movimento financeiro." + - name: data_pag + description: "Data do pagamento." + - name: nr_dl + description: "Número do documento de liquidação." + - name: desc_dl + description: "Descrição do documento de liquidação." + - name: vl_pago + description: "Valor pago." + - name: id_dl + description: "Identificador do documento de liquidação." + - name: data_emissao_dl + description: "Data de emissão do documento de liquidação." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.pagamento' + nome_coluna: 'nr_mov_fin' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.pagamento' + nome_coluna: 'vl_pago' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.pagamento' + nome_coluna: 'data_pag' + tipo_esperado: 'date' + + - name: licitacao + description: > + Tabela da camada bronze contendo informações sobre licitações de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: id_licitacao + description: "Identificador único da licitação." + - name: nr_convenio + description: "Número do convênio associado." + - name: nr_licitacao + description: "Número da licitação." + - name: modalidade_licitacao + description: "Modalidade da licitação." + - name: tp_processo_compra + description: "Tipo do processo de compra." + - name: tipo_licitacao + description: "Tipo da licitação." + - name: nr_processo_licitacao + description: "Número do processo de licitação." + - name: data_publicacao_licitacao + description: "Data de publicação da licitação." + - name: data_abertura_licitacao + description: "Data de abertura da licitação." + - name: data_encerramento_licitacao + description: "Data de encerramento da licitação." + - name: data_homologacao_licitacao + description: "Data de homologação da licitação." + - name: status_licitacao + description: "Status da licitação." + - name: situacao_aceite_processo_execu + description: "Situação de aceite do processo de execução." + - name: sistema_origem + description: "Sistema de origem da licitação." + - name: situacao_sistema + description: "Situação no sistema." + - name: valor_licitacao + description: "Valor da licitação." + - name: data_analise_aceite + description: "Data de análise de aceite." + - name: data_envio_analise + description: "Data de envio para análise." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.licitacao' + nome_coluna: 'id_licitacao' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.licitacao' + nome_coluna: 'valor_licitacao' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.licitacao' + nome_coluna: 'data_publicacao_licitacao' + tipo_esperado: 'date' + + - name: ingresso_contrapartida + description: > + Tabela da camada bronze contendo informações sobre ingressos de contrapartida de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: nr_convenio + description: "Número do convênio associado." + - name: dt_ingresso_contrapartida + description: "Data de ingresso da contrapartida." + - name: vl_ingresso_contrapartida + description: "Valor de ingresso da contrapartida." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.ingresso_contrapartida' + nome_coluna: 'nr_convenio' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.ingresso_contrapartida' + nome_coluna: 'vl_ingresso_contrapartida' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.ingresso_contrapartida' + nome_coluna: 'dt_ingresso_contrapartida' + tipo_esperado: 'date' + + - name: empenho + description: > + Tabela da camada bronze contendo informações sobre empenhos de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: id_empenho + description: "Identificador único do empenho." + - name: nr_convenio + description: "Número do convênio associado." + - name: nr_empenho + description: "Número do empenho." + - name: tipo_nota + description: "Tipo da nota de empenho." + - name: desc_tipo_nota + description: "Descrição do tipo da nota." + - name: data_emissao + description: "Data de emissão do empenho." + - name: cod_situacao_empenho + description: "Código da situação do empenho." + - name: desc_situacao_empenho + description: "Descrição da situação do empenho." + - name: ug_emitente + description: "Código da UG emitente." + - name: ug_responsavel + description: "Código da UG responsável." + - name: fonte_recurso + description: "Fonte de recurso do empenho." + - name: natureza_despesa + description: "Natureza da despesa." + - name: plano_interno + description: "Plano interno do empenho." + - name: ptres + description: "Programa de Trabalho Resumido." + - name: valor_empenho + description: "Valor do empenho." + - name: resultado_primario + description: "Resultado primário do empenho." + - name: observacao_empenho + description: "Observação do empenho." + - name: descricao_emenda_siafi + description: "Descrição da emenda SIAFI." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.empenho' + nome_coluna: 'id_empenho' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.empenho' + nome_coluna: 'valor_empenho' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.empenho' + nome_coluna: 'data_emissao' + tipo_esperado: 'date' + + - name: historico_situacao + description: > + Tabela da camada bronze contendo o histórico de situações de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: id_proposta + description: "Identificador da proposta." + - name: nr_convenio + description: "Número do convênio associado." + - name: dia_historico_sit + description: "Data do histórico de situação." + - name: historico_sit + description: "Descrição do histórico de situação." + - name: dias_historico_sit + description: "Quantidade de dias no histórico." + - name: cod_historico_sit + description: "Código do histórico de situação." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.historico_situacao' + nome_coluna: 'nr_convenio' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.historico_situacao' + nome_coluna: 'dia_historico_sit' + tipo_esperado: 'date' + + - name: cronograma_desembolso + description: > + Tabela da camada bronze contendo informações sobre cronogramas de desembolso de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: id_proposta + description: "Identificador da proposta." + - name: nr_convenio + description: "Número do convênio associado." + - name: nr_parcela_crono_desembolso + description: "Número da parcela do cronograma de desembolso." + - name: mes_crono_desembolso + description: "Mês do cronograma de desembolso." + - name: ano_crono_desembolso + description: "Ano do cronograma de desembolso." + - name: tipo_resp_crono_desembolso + description: "Tipo de responsável pelo cronograma de desembolso." + - name: valor_parcela_crono_desembolso + description: "Valor da parcela do cronograma de desembolso." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.cronograma_desembolso' + nome_coluna: 'nr_convenio' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.cronograma_desembolso' + nome_coluna: 'valor_parcela_crono_desembolso' + tipo_esperado: 'numeric' + + - name: meta_crono_fisico + description: > + Tabela da camada bronze contendo informações sobre metas do cronograma físico de convênios do SICONV. + meta: + tags: + - bronze + columns: + - name: id_meta + description: "Identificador único da meta." + - name: id_proposta + description: "Identificador da proposta." + - name: nr_convenio + description: "Número do convênio associado." + - name: cod_programa + description: "Código do programa." + - name: nome_programa + description: "Nome do programa." + - name: nr_meta + description: "Número da meta." + - name: tipo_meta + description: "Tipo da meta." + - name: desc_meta + description: "Descrição da meta." + - name: data_inicio_meta + description: "Data de início da meta." + - name: data_fim_meta + description: "Data de fim da meta." + - name: uf_meta + description: "UF da meta." + - name: municipio_meta + description: "Município da meta." + - name: endereco_meta + description: "Endereço da meta." + - name: cep_meta + description: "CEP da meta." + - name: qtd_meta + description: "Quantidade da meta." + - name: und_fornecimento_meta + description: "Unidade de fornecimento da meta." + - name: vl_meta + description: "Valor da meta." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.meta_crono_fisico' + nome_coluna: 'id_meta' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.meta_crono_fisico' + nome_coluna: 'vl_meta' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.meta_crono_fisico' + nome_coluna: 'data_inicio_meta' + tipo_esperado: 'date' \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/solicitacao_alteracao.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/solicitacao_alteracao.sql new file mode 100644 index 00000000..e7d3736a --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/solicitacao_alteracao.sql @@ -0,0 +1,20 @@ +{{ config(materialized="table") }} + +with + solicitacao_alteracao_raw as ( + select + nullif(id_solicitacao, '')::integer as id_solicitacao, + nullif(nr_convenio, '')::integer as nr_convenio, + nr_solicitacao::text as nr_solicitacao, + situacao_solicitacao::text as situacao_solicitacao, + objeto_solicitacao::text as objeto_solicitacao, + case + when nullif(data_solicitacao, '') is null then null + when data_solicitacao ~ '^\d{2}/\d{2}/\d{4}$' then to_date(data_solicitacao, 'DD/MM/YYYY') + else to_date(data_solicitacao, 'YYYY-MM-DD') + end as data_solicitacao + from {{ source("siconv", "solicitacao_alteracao") }} + ) + +select * +from solicitacao_alteracao_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/solicitacao_rendimento_aplicacao.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/solicitacao_rendimento_aplicacao.sql new file mode 100644 index 00000000..21e14d77 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/solicitacao_rendimento_aplicacao.sql @@ -0,0 +1,21 @@ +{{ config(materialized="table") }} + +with + solicitacao_rendimento_aplicacao_raw as ( + select + nullif(id_solicitacao_rend_aplicacao, '')::integer as id_solicitacao_rend_aplicacao, + nullif(nr_convenio, '')::integer as nr_convenio, + nr_solicitacao_rend_aplicacao::text as nr_solicitacao_rend_aplicacao, + status_solicitacao_rend_aplicacao::text as status_solicitacao_rend_aplicacao, + case + when nullif(data_solicitacao_rend_aplicacao, '') is null then null + when data_solicitacao_rend_aplicacao ~ '^\d{2}/\d{2}/\d{4}$' then to_date(data_solicitacao_rend_aplicacao, 'DD/MM/YYYY') + else to_date(data_solicitacao_rend_aplicacao, 'YYYY-MM-DD') + end as data_solicitacao_rend_aplicacao, + replace(nullif(valor_solicitacao_rend_aplicacao, ''), ',', '.')::numeric(15, 2) as valor_solicitacao_rend_aplicacao, + replace(nullif(valor_aprovado_solicitacao_rend_aplicacao, ''), ',', '.')::numeric(15, 2) as valor_aprovado_solicitacao_rend_aplicacao + from {{ source("siconv", "solicitacao_rendimento_aplicacao") }} + ) + +select * +from solicitacao_rendimento_aplicacao_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/termo_aditivo.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/termo_aditivo.sql new file mode 100644 index 00000000..be595030 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/bronze/termo_aditivo.sql @@ -0,0 +1,21 @@ +{{ config(materialized="table") }} + +with + termo_aditivo_raw as ( + select + nullif(nr_convenio, '')::integer as nr_convenio, + nullif(id_solicitacao, '')::integer as id_solicitacao, + numero_ta::text as numero_ta, + tipo_ta::text as tipo_ta, + replace(nullif(vl_global_ta, ''), ',', '.')::numeric(15, 2) as vl_global_ta, + replace(nullif(vl_repasse_ta, ''), ',', '.')::numeric(15, 2) as vl_repasse_ta, + replace(nullif(vl_contrapartida_ta, ''), ',', '.')::numeric(15, 2) as vl_contrapartida_ta, + to_date(nullif(dt_assinatura_ta, ''), 'DD/MM/YYYY') as dt_assinatura_ta, + to_date(nullif(dt_inicio_ta, ''), 'DD/MM/YYYY') as dt_inicio_ta, + to_date(nullif(dt_fim_ta, ''), 'DD/MM/YYYY') as dt_fim_ta, + justificativa_ta::text as justificativa_ta + from {{ source("siconv", "termo_aditivo") }} + ) + +select * +from termo_aditivo_raw \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/silver/emendas_convenio.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/silver/emendas_convenio.sql new file mode 100644 index 00000000..880bc5ee --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/silver/emendas_convenio.sql @@ -0,0 +1,125 @@ +{{ config(materialized="table") }} + +with + emendas as ( + select * + from {{ ref("numero_transferencia") }} + ), + convenio as ( + select * + from {{ ref("convenio") }} + ), + planos_acao as ( + select * + from {{ ref("planos_acao_ted") }} + ) + +select + e.emissao_mes, + e.emissao_dia, + e.codigo_programa, + e.programa, + e.codigo_acao_ajustada, + e.acao_ajustada, + e.autor_emendas_orcamento_descricao, + e.autor_emendas_orcamento_nome, + e.localizador_gasto, + e.localizador_gasto_descricao, + e.regiao_pt, + e.uf, + e.uf_descricao, + e.municipio, + e.pais, + e.ne_ccor, + e.ne_num_processo, + e.ne_info_complementar, + e.ne_ccor_descricao, + e.doc_observacao, + e.codigo_gnd, + e.gnd, + e.natureza_despesa, + e.natureza_despesa_descricao, + e.codigo_modalidade, + e.modalidade, + e.ne_ccor_favorecido, + e.ne_ccor_favorecido_descricao, + e.ne_ccor_ano_emissao, + e.ptres, + e.item_informacao, + e.item_informacao_descricao, + e.despesas_empenhadas, + e.despesas_liquidadas, + e.despesas_pagas, + e.id_autor, + e.cargo_autor, + e.autor, + e.partido, + e.uf_autor, + e.url_foto_autor, + e.email_autor, + e.url_foto_partido, + e.numero_transferencia, + e.dt_ingest, + c.nr_convenio, + c.id_proposta, + c.dia, + c.mes, + c.ano, + c.dia_assin_conv, + c.sit_convenio, + c.subsituacao_conv, + c.situacao_publicacao, + c.instrumento_ativo, + c.ind_opera_obtv, + c.nr_processo, + c.ug_emitente, + c.dia_publ_conv, + c.dia_inic_vigenc_conv, + c.dia_fim_vigenc_conv, + c.dia_fim_vigenc_original_conv, + c.dias_prest_contas, + c.dia_limite_prest_contas, + c.data_suspensiva, + c.data_retirada_suspensiva, + c.dias_clausula_suspensiva, + c.situacao_contratacao, + c.ind_assinado, + c.motivo_suspensao, + c.ind_foto, + c.qtde_convenios, + c.qtd_ta, + c.qtd_prorroga, + c.vl_global_conv, + c.vl_repasse_conv, + c.vl_contrapartida_conv, + c.vl_empenhado_conv, + c.vl_desembolsado_conv, + c.vl_saldo_reman_tesouro, + c.vl_saldo_reman_convenente, + c.vl_rendimento_aplicacao, + c.vl_ingresso_contrapartida, + c.vl_saldo_conta, + c.valor_global_original_conv, + pa.id_plano_acao, + pa.id_programa, + pa.sigla_unidade_descentralizada, + pa.unidade_descentralizada, + pa.sigla_unidade_responsavel_execucao, + pa.unidade_responsavel_execucao, + pa.vl_total_plano_acao, + pa.dt_inicio_vigencia, + pa.dt_fim_vigencia, + pa.tx_objeto_plano_acao, + pa.tx_justificativa_plano_acao, + pa.in_forma_execucao_direta, + pa.in_forma_execucao_particulares, + pa.in_forma_execucao_descentralizada, + pa.tx_situacao_plano_acao, + pa.aa_ano_plano_acao, + pa.vl_beneficiario_especifico, + pa.vl_chamamento_publico, + pa.sq_instrumento, + pa.aa_instrumento +from emendas e +left join convenio c on e.numero_transferencia = c.nr_convenio +left join planos_acao pa on e.numero_transferencia::text = pa.sq_instrumento \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/silver/numero_transferencia.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/silver/numero_transferencia.sql new file mode 100644 index 00000000..5eb87cb6 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/silver/numero_transferencia.sql @@ -0,0 +1,51 @@ +{{ config(materialized="table") }} + +with + emendas as ( + select *, + case + when ne_info_complementar ~ '^\d+$' + then ne_info_complementar::integer + when ne_ccor_descricao ~* 'CONVENIO|FOMENTO|FOMENO' + then nullif( + regexp_replace( + ne_ccor_descricao, + '.*(?:CONVENIO|FOMENTO|FOMENO)\s*(?:N[°º]?)?\s*(\d{6}).*', + '\1' + ), + ne_ccor_descricao + )::integer + when ne_ccor_descricao ~* 'TED\s*\d{6}' + then nullif( + regexp_replace( + ne_ccor_descricao, + '.*TED\s*(\d{6}).*', + '\1' + ), + ne_ccor_descricao + )::integer + when doc_observacao ~* 'CONVENIO|FOMENTO|FOMENO' + then nullif( + regexp_replace( + doc_observacao, + '.*(?:CONVENIO|FOMENTO|FOMENO)\s*(?:N[°º]?)?\s*(\d{6}).*', + '\1' + ), + doc_observacao + )::integer + when doc_observacao ~* 'TED\s*\d{6}' + then nullif( + regexp_replace( + doc_observacao, + '.*TED\s*(\d{6}).*', + '\1' + ), + doc_observacao + )::integer + else null + end as numero_transferencia + from {{ ref("emendas_partidos") }} + ) + +select * +from emendas \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/silver/proposta_convenio.sql b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/silver/proposta_convenio.sql new file mode 100644 index 00000000..e2b83480 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/silver/proposta_convenio.sql @@ -0,0 +1,89 @@ +{{ config(materialized="table") }} + +with + convenio as ( + select * + from {{ ref("convenio") }} + ), + proposta as ( + select * + from {{ ref("proposta") }} + ) + +select + -- Colunas do convênio + c.nr_convenio, + c.dia as dia_conv, + c.mes as mes_conv, + c.ano as ano_conv, + c.dia_assin_conv, + c.sit_convenio, + c.subsituacao_conv, + c.situacao_publicacao, + c.instrumento_ativo, + c.ind_opera_obtv, + c.nr_processo, + c.ug_emitente, + c.dia_publ_conv, + c.dia_inic_vigenc_conv, + c.dia_fim_vigenc_conv, + c.dia_fim_vigenc_original_conv, + c.dias_prest_contas, + c.dia_limite_prest_contas, + c.data_suspensiva, + c.data_retirada_suspensiva, + c.dias_clausula_suspensiva, + c.situacao_contratacao, + c.ind_assinado, + c.motivo_suspensao, + c.ind_foto, + c.qtde_convenios, + c.qtd_ta, + c.qtd_prorroga, + c.vl_global_conv, + c.vl_repasse_conv, + c.vl_contrapartida_conv, + c.vl_empenhado_conv, + c.vl_desembolsado_conv, + c.vl_saldo_reman_tesouro, + c.vl_saldo_reman_convenente, + c.vl_rendimento_aplicacao, + c.vl_ingresso_contrapartida, + c.vl_saldo_conta, + c.valor_global_original_conv, + + -- Colunas da proposta + p.id_proposta, + p.uf_proponente, + p.munic_proponente, + p.cod_munic_ibge, + p.cod_orgao_sup, + p.desc_orgao_sup, + p.natureza_juridica, + p.nr_proposta, + p.dia_proposta, + p.cod_orgao, + p.desc_orgao, + p.modalidade, + p.identif_proponente, + p.nm_proponente, + p.cep_proponente, + p.endereco_proponente, + p.bairro_proponente, + p.nm_banco, + p.situacao_conta, + p.situacao_projeto_basico, + p.sit_proposta, + p.dia_inic_vigencia_proposta, + p.dia_fim_vigencia_proposta, + p.objeto_proposta, + p.item_investimento, + p.enviada_mandataria, + p.nome_subtipo_proposta, + p.descricao_subtipo_proposta, + p.vl_global_prop, + p.vl_repasse_prop, + p.vl_contrapartida_prop +from convenio c +inner join proposta p on c.id_proposta = p.id_proposta +where p.modalidade in ('CONVENIO', 'TERMO DE FOMENTO') diff --git a/airflow_lappis/dags/dbt/mir/models/siconv_dbt/silver/schema.yaml b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/silver/schema.yaml new file mode 100644 index 00000000..406f16c5 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/siconv_dbt/silver/schema.yaml @@ -0,0 +1,344 @@ +version: 2 + +models: + - name: numero_transferencia + description: > + Tabela da camada silver que extrai e padroniza o número de transferência + das emendas parlamentares. A extração é feita sequencialmente a partir de + múltiplas colunas usando as seguintes regras: + 1. Se `ne_info_complementar` contém apenas dígitos, usa diretamente. + 2. Se `ne_ccor_descricao` contém CONVENIO, FOMENTO ou FOMENO, extrai 6 dígitos via regex. + 3. Se `ne_ccor_descricao` contém TED seguido de 6 dígitos, extrai via regex. + 4. Se `doc_observacao` contém CONVENIO, FOMENTO ou FOMENO, extrai 6 dígitos via regex. + 5. Se `doc_observacao` contém TED seguido de 6 dígitos, extrai via regex. + Caso nenhuma regra se aplique, `numero_transferencia` fica como NULL. + meta: + tags: + - silver + columns: + - name: emissao_mes + description: "Mês de emissão da nota de empenho." + - name: emissao_dia + description: "Data de emissão da nota de empenho." + - name: codigo_programa + description: "Código do programa de governo." + - name: programa + description: "Descrição do programa de governo." + - name: codigo_acao_ajustada + description: "Código da ação orçamentária ajustada." + - name: acao_ajustada + description: "Descrição da ação orçamentária ajustada." + - name: autor_emendas_orcamento_descricao + description: "Descrição do autor da emenda orçamentária." + - name: autor_emendas_orcamento_nome + description: "Nome do autor da emenda orçamentária." + - name: uf + description: "Sigla da UF do autor." + - name: uf_descricao + description: "Nome da UF do autor." + - name: municipio + description: "Município associado à emenda." + - name: pais + description: "País associado à emenda." + - name: ne_ccor + description: "Número completo da nota de empenho." + - name: ne_num_processo + description: "Número do processo administrativo." + - name: ne_info_complementar + description: "Informações complementares da nota de empenho." + - name: ne_ccor_descricao + description: "Descrição da nota de empenho." + - name: doc_observacao + description: "Observações do documento." + - name: codigo_gnd + description: "Código do grupo de natureza de despesa." + - name: gnd + description: "Descrição do grupo de natureza de despesa." + - name: natureza_despesa + description: "Código da natureza de despesa." + - name: natureza_despesa_descricao + description: "Descrição da natureza de despesa." + - name: codigo_modalidade + description: "Código da modalidade de aplicação." + - name: modalidade + description: "Descrição da modalidade de aplicação." + - name: ne_ccor_favorecido + description: "CNPJ ou CPF do favorecido." + - name: ne_ccor_favorecido_descricao + description: "Nome do favorecido." + - name: ne_ccor_ano_emissao + description: "Ano de emissão da nota de empenho." + - name: ptres + description: "Programa de Trabalho Resumido." + - name: item_informacao + description: "Código do item de informação." + - name: item_informacao_descricao + description: "Descrição do item de informação." + - name: despesas_empenhadas + description: "Valor total das despesas empenhadas." + - name: despesas_liquidadas + description: "Valor das despesas liquidadas." + - name: despesas_pagas + description: "Valor das despesas pagas." + - name: id_autor + description: "Identificador do autor da emenda." + - name: cargo_autor + description: "Cargo do autor da emenda." + - name: autor + description: "Nome do autor da emenda." + - name: partido + description: "Partido do autor da emenda." + - name: uf_autor + description: "UF do autor da emenda." + - name: url_foto_autor + description: "URL da foto do autor." + - name: email_autor + description: "Email do autor." + - name: url_foto_partido + description: "URL da foto do partido." + - name: numero_transferencia + description: > + Número de transferência extraído das colunas ne_info_complementar, + ne_ccor_descricao e doc_observacao via regex. Pode ser número de + convênio ou TED. NULL quando não foi possível extrair. + - name: dt_ingest + description: "Timestamp de ingestão dos dados." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.numero_transferencia' + nome_coluna: 'numero_transferencia' + tipo_esperado: 'integer' + + - name: emendas_convenio + description: > + Tabela da camada silver que cruza emendas parlamentares com convênios do SICONV + e planos de ação do TransfereGov. Utiliza o `numero_transferencia` extraído + na tabela `numero_transferencia` para realizar o join com a tabela de convênios + pelo `nr_convenio`, e com `planos_acao` pelo `sq_instrumento` para os casos + de TEDs que não possuem convênio associado. + Do total de 191 registros, 174 (91%) têm convênio vinculado. Os 17 restantes + são TEDs, contratos ou instrumentos sem correspondência no SICONV. + meta: + tags: + - silver + columns: + - name: numero_transferencia + description: "Número de transferência extraído das emendas — convênio ou TED." + - name: nr_convenio + description: "Número do convênio do SICONV. NULL para TEDs e contratos." + - name: sit_convenio + description: "Situação atual do convênio." + - name: dia_assin_conv + description: "Data de assinatura do convênio." + - name: dia_inic_vigenc_conv + description: "Data de início da vigência do convênio." + - name: dia_fim_vigenc_conv + description: "Data de fim da vigência do convênio." + - name: vl_global_conv + description: "Valor global do convênio." + - name: vl_repasse_conv + description: "Valor de repasse do convênio." + - name: vl_contrapartida_conv + description: "Valor de contrapartida do convênio." + - name: vl_empenhado_conv + description: "Valor empenhado do convênio." + - name: vl_desembolsado_conv + description: "Valor desembolsado do convênio." + - name: sq_instrumento + description: "Número do instrumento no TransfereGov. Preenchido para TEDs." + - name: id_plano_acao + description: "Identificador do plano de ação no TransfereGov." + - name: vl_total_plano_acao + description: "Valor total do plano de ação." + - name: tx_situacao_plano_acao + description: "Situação do plano de ação." + - name: localizador_gasto + description: "Código do localizador de gasto associado à programação orçamentária." + - name: localizador_gasto_descricao + description: "Descrição do localizador de gasto." + - name: regiao_pt + description: "Região associada ao programa de trabalho (PT) da emenda." + - name: dt_ingest + description: "Timestamp de ingestão dos dados." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.emendas_convenio' + nome_coluna: 'nr_convenio' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.emendas_convenio' + nome_coluna: 'numero_transferencia' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.emendas_convenio' + nome_coluna: 'vl_global_conv' + tipo_esperado: 'numeric' + + - name: proposta_convenio + description: > + Tabela da camada silver que cruza convênios com propostas do SICONV, + filtrando apenas as modalidades CONVENIO e TERMO DE FOMENTO. + Utiliza o `id_proposta` como chave de junção entre as tabelas de convênio + e proposta da camada bronze. Somente convênios que possuem proposta + associada nas modalidades filtradas são incluídos (INNER JOIN). + meta: + tags: + - silver + columns: + - name: nr_convenio + description: "Número do convênio." + - name: dia_conv + description: "Dia de assinatura do convênio." + - name: mes_conv + description: "Mês de assinatura do convênio." + - name: ano_conv + description: "Ano de assinatura do convênio." + - name: dia_assin_conv + description: "Data de assinatura do convênio." + - name: sit_convenio + description: "Situação do convênio." + - name: subsituacao_conv + description: "Subsituação do convênio." + - name: situacao_publicacao + description: "Situação de publicação do convênio." + - name: instrumento_ativo + description: "Indica se o instrumento está ativo." + - name: ind_opera_obtv + description: "Indicador de operação OBTV." + - name: nr_processo + description: "Número do processo." + - name: ug_emitente + description: "Código da UG emitente." + - name: dia_publ_conv + description: "Data de publicação do convênio." + - name: dia_inic_vigenc_conv + description: "Data de início da vigência do convênio." + - name: dia_fim_vigenc_conv + description: "Data de fim da vigência do convênio." + - name: dia_fim_vigenc_original_conv + description: "Data de fim da vigência original do convênio." + - name: dias_prest_contas + description: "Prazo em dias para prestação de contas." + - name: dia_limite_prest_contas + description: "Data limite para prestação de contas." + - name: data_suspensiva + description: "Data da cláusula suspensiva." + - name: data_retirada_suspensiva + description: "Data de retirada da cláusula suspensiva." + - name: dias_clausula_suspensiva + description: "Dias de cláusula suspensiva." + - name: situacao_contratacao + description: "Situação da contratação." + - name: ind_assinado + description: "Indica se o convênio foi assinado." + - name: motivo_suspensao + description: "Motivo da suspensão do convênio." + - name: ind_foto + description: "Indica se há foto associada." + - name: qtde_convenios + description: "Quantidade de convênios." + - name: qtd_ta + description: "Quantidade de termos aditivos." + - name: qtd_prorroga + description: "Quantidade de prorrogações." + - name: vl_global_conv + description: "Valor global do convênio." + - name: vl_repasse_conv + description: "Valor de repasse do convênio." + - name: vl_contrapartida_conv + description: "Valor de contrapartida do convênio." + - name: vl_empenhado_conv + description: "Valor empenhado do convênio." + - name: vl_desembolsado_conv + description: "Valor desembolsado do convênio." + - name: vl_saldo_reman_tesouro + description: "Valor do saldo remanescente do tesouro." + - name: vl_saldo_reman_convenente + description: "Valor do saldo remanescente do convenente." + - name: vl_rendimento_aplicacao + description: "Valor do rendimento de aplicação." + - name: vl_ingresso_contrapartida + description: "Valor de ingresso de contrapartida." + - name: vl_saldo_conta + description: "Valor do saldo em conta." + - name: valor_global_original_conv + description: "Valor global original do convênio." + - name: id_proposta + description: "Identificador único da proposta." + - name: uf_proponente + description: "Unidade Federativa do proponente." + - name: munic_proponente + description: "Município do proponente." + - name: cod_munic_ibge + description: "Código IBGE do município." + - name: cod_orgao_sup + description: "Código do órgão superior." + - name: desc_orgao_sup + description: "Descrição do órgão superior." + - name: natureza_juridica + description: "Natureza jurídica do proponente." + - name: nr_proposta + description: "Número da proposta." + - name: dia_proposta + description: "Data da proposta." + - name: cod_orgao + description: "Código do órgão." + - name: desc_orgao + description: "Descrição do órgão." + - name: modalidade + description: "Modalidade da proposta (CONVENIO ou TERMO DE FOMENTO)." + - name: identif_proponente + description: "Identificação do proponente (CNPJ/CPF)." + - name: nm_proponente + description: "Nome do proponente." + - name: cep_proponente + description: "CEP do proponente." + - name: endereco_proponente + description: "Endereço do proponente." + - name: bairro_proponente + description: "Bairro do proponente." + - name: nm_banco + description: "Nome do banco." + - name: situacao_conta + description: "Situação da conta bancária." + - name: situacao_projeto_basico + description: "Situação do projeto básico." + - name: sit_proposta + description: "Situação da proposta." + - name: dia_inic_vigencia_proposta + description: "Data de início da vigência da proposta." + - name: dia_fim_vigencia_proposta + description: "Data de fim da vigência da proposta." + - name: objeto_proposta + description: "Objeto da proposta." + - name: item_investimento + description: "Item de investimento." + - name: enviada_mandataria + description: "Indica se foi enviada à mandatária." + - name: nome_subtipo_proposta + description: "Nome do subtipo da proposta." + - name: descricao_subtipo_proposta + description: "Descrição do subtipo da proposta." + - name: vl_global_prop + description: "Valor global da proposta." + - name: vl_repasse_prop + description: "Valor de repasse da proposta." + - name: vl_contrapartida_prop + description: "Valor de contrapartida da proposta." + tests: + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.proposta_convenio' + nome_coluna: 'nr_convenio' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.proposta_convenio' + nome_coluna: 'id_proposta' + tipo_esperado: 'integer' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.proposta_convenio' + nome_coluna: 'vl_global_conv' + tipo_esperado: 'numeric' + - verificacao_tipagem: + nome_tabela: 'siconv_dbt.proposta_convenio' + nome_coluna: 'vl_global_prop' + tipo_esperado: 'numeric' \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/models/sources.yml b/airflow_lappis/dags/dbt/mir/models/sources.yml new file mode 100644 index 00000000..02918481 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/models/sources.yml @@ -0,0 +1,74 @@ +version: 2 + +sources: + - name: transferegov_emendas + schema: transferegov_emendas + tables: + - name: programas_especiais + - name: planos_acao_especiais + - name: empenhos_especiais + - name: ordens_bancarias_especiais + - name: historico_pagamentos_especiais + - name: plano_trabalho_especial + - name: documentos_habeis_especiais + - name: relatorio_gestao_especial + - name: relatorios_gestao_novo_especial + - name: executor_especial + - name: metas_especiais + - name: finalidades_especiais + + - name: camara_deputados + schema: camara_deputados + tables: + - name: deputados + - name: deputados_historico + + - name: senado_federal + schema: senado_federal + tables: + - name: senadores + - name: legislaturas + - name: senadores_historico + + - name: dados_abertos + schema: dados_abertos + tables: + - name: logo_partidos + + - name: transfere_gov + schema: transfere_gov + tables: + - name: programas + - name: planos_acao + - name: programacao_financeira + - name: notas_de_credito + + - name: siafi + schema: siafi + tables: + - name: ne_tesouro + - name: ne_tesouro_emendas + - name: nc_tesouro_pos__2026 + - name: nc_tesouro_pre_2026 + - name: pf_tesouro + - name: programacao_acao_ptres + + - name: siconv + schema: siconv + tables: + - name: proposta + - name: convenio + - name: desembolso + - name: desbloqueio + - name: solicitacao_alteracao + - name: termo_aditivo + - name: solicitacao_rendimento_aplicacao + - name: prorroga_oficio + - name: pagamento_tributo + - name: pagamento + - name: licitacao + - name: ingresso_contrapartida + - name: empenho + - name: historico_situacao + - name: cronograma_desembolso + - name: meta_crono_fisico \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/profiles.yml b/airflow_lappis/dags/dbt/mir/profiles.yml new file mode 100755 index 00000000..c5ef73cc --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/profiles.yml @@ -0,0 +1,11 @@ +mir: + target: prod + outputs: + prod: + type: postgres + host: "{{ env_var('DB_DW_HOST_MIR', 'postgres') }}" + user: "{{ env_var('DB_DW_USER_MIR', 'postgres_dw') }}" + password: "{{ env_var('DB_DW_PASSWORD_MIR', 'postgres_dw') }}" + port: "{{ env_var('DB_DW_PORT_MIR', '5432') | int }}" + dbname: "{{ env_var('DB_DW_DBNAME_MIR', 'data_warehouse') }}" + schema: "{{ env_var('DB_DW_SCHEMA_MIR', 'mir') }}" \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/seeds/partidos_logo.csv b/airflow_lappis/dags/dbt/mir/seeds/partidos_logo.csv new file mode 100644 index 00000000..2dfcb601 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/seeds/partidos_logo.csv @@ -0,0 +1,29 @@ +sigla,nome,logo_url +AVANTE,Avante,https://upload.wikimedia.org/wikipedia/commons/7/7a/Bandeira-partido-avante.png +CIDADANIA,Cidadania,https://upload.wikimedia.org/wikipedia/commons/d/d7/Logo_do_Cidadania_23.png +DC,Democracia Cristã,https://upload.wikimedia.org/wikipedia/commons/c/cd/Bandeira-democracia-crist%C3%A3.png +MDB,Movimento Democrático Brasileiro,https://upload.wikimedia.org/wikipedia/commons/a/a8/Movimento_Democr%C3%A1tico_Brasileiro_%282017%29.png +MISSÃO,Missão,https://upload.wikimedia.org/wikipedia/commons/6/6e/Partido_Miss%C3%A3o_logo_%28dark%29.svg +NOVO,Partido Novo,https://upload.wikimedia.org/wikipedia/commons/6/64/Partido_Novo_logo_%282020%29.svg +PCdoB,Partido Comunista do Brasil,https://upload.wikimedia.org/wikipedia/commons/e/e5/PCdoB_flag.svg +PCO,Partido da Causa Operária,https://upload.wikimedia.org/wikipedia/commons/4/4a/Bandeira_do_Partido_da_Causa_Oper%C3%A1ria%2C_do_Brasil.png +PDT,Partido Democrático Trabalhista,https://upload.wikimedia.org/wikipedia/commons/5/51/LogoPDT.svg +PL,Partido Liberal,https://upload.wikimedia.org/wikipedia/commons/1/12/Partido_Liberal_%28Brazil%29_logo.svg +PMB,Partido da Mulher Brasileira,https://upload.wikimedia.org/wikipedia/commons/f/f2/Logomarca_do_Partido_da_Mulher_Brasileira_%282008%29.png +PODE,Podemos,https://upload.wikimedia.org/wikipedia/commons/9/99/Logo_Podemos_20.png +PP,Progressistas,https://upload.wikimedia.org/wikipedia/commons/0/0d/Partido_Progressista_%28Brazil%29_logo.svg +PRD,Partido Renovação Democrática,https://upload.wikimedia.org/wikipedia/commons/2/2c/PRD_logo.png +PROS,Partido Republicano da Ordem Social,https://upload.wikimedia.org/wikipedia/commons/8/8a/PROS_logo.png +PRTB,Partido Renovador Trabalhista Brasileiro,https://upload.wikimedia.org/wikipedia/commons/7/76/PRTB-LOGO-04-1024x393.png +PSB,Partido Socialista Brasileiro,https://upload.wikimedia.org/wikipedia/commons/1/19/Partido_Socialista_Brasileiro_logo.png +PSC,Partido Social Cristão,https://upload.wikimedia.org/wikipedia/commons/a/a9/PSC_logo%28cortado%29.png +PSD,Partido Social Democrático,https://upload.wikimedia.org/wikipedia/commons/0/0c/PSD_Brazil_logo.svg +PSDB,Partido da Social Democracia Brasileira,https://upload.wikimedia.org/wikipedia/commons/9/9c/Logomarca_do_Partido_da_Social_Democracia_Brasileira.png +PSOL,Partido Socialismo e Liberdade,https://upload.wikimedia.org/wikipedia/commons/f/f4/Logo_PSOL_roxo.svg +PT,Partido dos Trabalhadores,https://upload.wikimedia.org/wikipedia/commons/b/be/Logo_do_Partido_dos_Trabalhadores.svg +PV,Partido Verde,https://upload.wikimedia.org/wikipedia/commons/4/46/Logomarca_do_Partido_Verde.svg +REDE,Rede Sustentabilidade,https://upload.wikimedia.org/wikipedia/commons/2/2d/Logomarca_da_Rede_Sustentabilidade_(REDE)%2C_do_Brasil.png +REPUBLICANOS,Republicanos,https://upload.wikimedia.org/wikipedia/commons/3/3c/Logomarca_do_Partido_Republicano_Brasileiro_%282005%E2%80%932012%29.png +SOLIDARIEDADE,Solidariedade,https://upload.wikimedia.org/wikipedia/commons/f/fe/Logomarca_do_Partido_Solidariedade.png +UNIÃO,União Brasil,https://upload.wikimedia.org/wikipedia/commons/7/73/Uni%C3%A3o_Brasil_logo.svg +UP,Unidade Popular,https://upload.wikimedia.org/wikipedia/commons/b/bd/UP_Flag2.svg \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/seeds/partidos_map.csv b/airflow_lappis/dags/dbt/mir/seeds/partidos_map.csv new file mode 100644 index 00000000..ee1d85fe --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/seeds/partidos_map.csv @@ -0,0 +1,2 @@ +sigla_origem,sigla_canonica +PODEMOS,PODE \ No newline at end of file diff --git a/airflow_lappis/dags/dbt/mir/seeds/schema.yml b/airflow_lappis/dags/dbt/mir/seeds/schema.yml new file mode 100644 index 00000000..e5676468 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/seeds/schema.yml @@ -0,0 +1,14 @@ +version: 2 + +seeds: + - name: partidos_logo + config: + column_types: + sigla: text + nome: text + logo_url: text + - name: partidos_map + config: + column_types: + sigla_origem: text + sigla_canonica: text diff --git a/airflow_lappis/dags/dbt/mir/tests/test_parlamentares_logo_partido.sql b/airflow_lappis/dags/dbt/mir/tests/test_parlamentares_logo_partido.sql new file mode 100644 index 00000000..bab1d974 --- /dev/null +++ b/airflow_lappis/dags/dbt/mir/tests/test_parlamentares_logo_partido.sql @@ -0,0 +1,19 @@ +-- Falha (ou gera warning) se existir algum parlamentar com sigla de partido +-- que não tenha correspondência no seed partidos_logo. + +{{ config(severity='warn') }} + +with parlamentares as ( + select * from {{ ref('parlamentares') }} +), +partidos_logo as ( + select * from {{ ref('partidos_logo') }} +) + +select distinct + p.sigla_partido +from parlamentares p +left join partidos_logo pl + on upper(trim(p.sigla_partido)) = upper(trim(pl.sigla)) +where p.sigla_partido is not null + and pl.sigla is null diff --git a/airflow_lappis/helpers/dados_funcionais_handler.py b/airflow_lappis/helpers/dados_funcionais_handler.py new file mode 100644 index 00000000..d56d3154 --- /dev/null +++ b/airflow_lappis/helpers/dados_funcionais_handler.py @@ -0,0 +1,142 @@ +""" +Handler para processamento de dados funcionais do SIAPE. +Responsável por filtrar e selecionar registros funcionais ativos. +""" + +import logging +from typing import Dict, List +import xml.etree.ElementTree as ET + + +class DadosFuncionaisHandler: + """Handler para processar dados funcionais do SIAPE.""" + + @staticmethod + def extract_dados_funcionais_elements(xml_string: str) -> List[ET.Element]: + """Extrai elementos DadosFuncionais do XML.""" + ns = { + "soap": "http://schemas.xmlsoap.org/soap/envelope/", + "soapenv": "http://schemas.xmlsoap.org/soap/envelope/", + } + root = ET.fromstring(xml_string) + + # Busca o body usando ambos os namespaces possíveis + body = root.find("soap:Body", ns) or root.find("soapenv:Body", ns) + if body is None: + logging.warning("SOAP Body não encontrado no XML") + return [] + + # Busca todos os elementos DadosFuncionais usando iter() que ignora namespaces + dados_funcionais_items = [] + for elem in body.iter(): + tag = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag + if tag == "DadosFuncionais": + dados_funcionais_items.append(elem) + + return dados_funcionais_items + + @staticmethod + def convert_elements_to_registros( + elementos: List[ET.Element], + ) -> List[Dict[str, str | None]]: + """Converte elementos XML em dicionários de registros.""" + registros = [] + for item in elementos: + registro = {} + for elem in item: + tag = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag + registro[tag] = ( + elem.text.strip() if elem.text and elem.text.strip() else None + ) + registros.append(registro) + return registros + + @staticmethod + def select_best_registro( + registros: List[Dict[str, str | None]], + ) -> Dict[str, str | None]: + """Seleciona o melhor registro funcional baseado nas regras de negócio.""" + if not registros: + return {} + + # Filtra registros ativos (sem dataOcorrExclusao ou com valor vazio/None) + registros_ativos = [ + r + for r in registros + if not r.get("dataOcorrExclusao") or r.get("dataOcorrExclusao") == "" + ] + + logging.info(f"Registros ativos (sem dataOcorrExclusao): {len(registros_ativos)}") + + if not registros_ativos: + return DadosFuncionaisHandler._handle_no_active_records(registros) + + if len(registros_ativos) == 1: + return DadosFuncionaisHandler._handle_single_active_record( + registros_ativos[0] + ) + + return DadosFuncionaisHandler._handle_multiple_active_records(registros_ativos) + + @staticmethod + def _handle_no_active_records( + registros: List[Dict[str, str | None]], + ) -> Dict[str, str | None]: + """Lida com o caso onde não há registros ativos.""" + logging.warning( + "Nenhum registro funcional ativo encontrado (todos têm dataOcorrExclusao)" + ) + # Se não há registros ativos, retorna o mais recente + # baseado em dataIngressoFuncao + registros_com_data = [r for r in registros if r.get("dataIngressoFuncao")] + if registros_com_data: + registros_ordenados = sorted( + registros_com_data, + key=lambda x: x.get("dataIngressoFuncao") or "00000000", + reverse=True, + ) + data_ingresso = registros_ordenados[0].get("dataIngressoFuncao") + logging.info( + f"Retornando registro mais recente: " + f"dataIngressoFuncao={data_ingresso}" + ) + return registros_ordenados[0] + else: + # Se não há datas de ingresso, retorna o primeiro + return registros[0] if registros else {} + + @staticmethod + def _handle_single_active_record( + registro: Dict[str, str | None], + ) -> Dict[str, str | None]: + """Lida com o caso onde há apenas um registro ativo.""" + matricula = registro.get("matriculaSiape") + logging.info(f"Retornando único registro ativo: matricula={matricula}") + return registro + + @staticmethod + def _handle_multiple_active_records( + registros_ativos: List[Dict[str, str | None]], + ) -> Dict[str, str | None]: + """Lida com o caso onde há múltiplos registros ativos.""" + # Se há múltiplos registros ativos, retorna o mais recente + # baseado em dataIngressoFuncao + registros_ativos_com_data = [ + r for r in registros_ativos if r.get("dataIngressoFuncao") + ] + + if registros_ativos_com_data: + registro_mais_recente = max( + registros_ativos_com_data, + key=lambda x: x.get("dataIngressoFuncao") or "00000000", + ) + else: + # Se nenhum tem data de ingresso, pega o primeiro ativo + registro_mais_recente = registros_ativos[0] + + logging.info( + f"Múltiplos registros ativos encontrados, selecionando o mais recente: " + f"dataIngressoFuncao={registro_mais_recente.get('dataIngressoFuncao', 'N/A')}, " # noqa: E501 + f"matricula={registro_mais_recente.get('matriculaSiape', 'N/A')}" + ) + return registro_mais_recente diff --git a/airflow_lappis/helpers/postgres_helpers.py b/airflow_lappis/helpers/postgres_helpers.py new file mode 100755 index 00000000..de35bcf0 --- /dev/null +++ b/airflow_lappis/helpers/postgres_helpers.py @@ -0,0 +1,24 @@ +import logging +from airflow.providers.postgres.hooks.postgres import PostgresHook + + +def get_postgres_conn(data_base_name: str = "postgres_default") -> str: + try: + hook = PostgresHook(postgres_conn_id=data_base_name) + conn = hook.get_conn() + try: + schema = conn.info.dbname + logging.info( + f"[postgres_helpers] Obtained PostgreSQL connection: " + f"dbname={schema}, user={conn.info.user}," + f"host={conn.info.host}, port={conn.info.port}" + ) + return ( + f"dbname={schema} user={conn.info.user} password={conn.info.password} " + f"host={conn.info.host} port={conn.info.port}" + ) + finally: + conn.close() + except Exception as e: + logging.error(f"Failed to obtain PostgreSQL connection: {e}") + raise diff --git a/airflow_lappis/helpers/quilombolas_parser.py b/airflow_lappis/helpers/quilombolas_parser.py new file mode 100644 index 00000000..4daddff0 --- /dev/null +++ b/airflow_lappis/helpers/quilombolas_parser.py @@ -0,0 +1,459 @@ +""" +Parsing e limpeza dos arquivos do Censo 2022 — Quilombolas: +alfabetização e características dos domicílios (universo). +""" + +from __future__ import annotations + +import io +import logging +import re +import unicodedata +from dataclasses import dataclass, field + +import pandas as pd + +PREFIXO_TABELA = "Q_A_C_D_" +MAX_COL_LEN = 63 +VALORES_NULOS = frozenset({"nan", "none", ""}) + +SUBPASTAS_DADOS = ("Apendices", "Tabelas_de_resultados", "Tabelas_selecionadas") +INDICES_FTP: list[tuple[str, str]] = [ + ("Tabelas_de_resultados", "indice_de_tabelas_de_resultados.txt"), + ("Tabelas_selecionadas", "indice_de_tabelas_seleciondas.txt"), +] + + +@dataclass +class ChunkProcessado: + """Resultado do processamento de um bloco tabular.""" + + df: pd.DataFrame + table_name: str + table_comment: str + primary_key: list[str] = field(default_factory=list) + col_comments: dict[str, str] = field(default_factory=dict) + arquivo: str = "" + subcaminho: str = "" + sheet_name: str = "" + + +def _remover_acentos(texto: str) -> str: + return "".join( + c for c in unicodedata.normalize("NFD", texto) if unicodedata.category(c) != "Mn" + ) + + +def normalizar_nome_coluna(nome: str, idx: int) -> str: + """Converte descrição original em identificador PostgreSQL.""" + sem_acento = _remover_acentos(str(nome)) + limpo = re.sub( + r"[^\w%]", + "", + sem_acento.lower() + .replace("%", "_porcentagem") + .replace(" ", "_") + .replace("-", "_"), + ) + if not limpo or limpo == "none": + return f"coluna_{idx}" + if len(limpo) > MAX_COL_LEN: + limpo = limpo[:MAX_COL_LEN] + return limpo + + +def deduplicar_colunas(colunas: list[str]) -> list[str]: + contagem: dict[str, int] = {} + resultado: list[str] = [] + for col in colunas: + if col not in contagem: + contagem[col] = 0 + resultado.append(col) + continue + contagem[col] += 1 + sufixo = f"_{contagem[col]}" + if len(col) + len(sufixo) > MAX_COL_LEN: + base = col[: MAX_COL_LEN - len(sufixo)] + else: + base = col + resultado.append(f"{base}{sufixo}") + return resultado + + +def construir_nome_tabela(arquivo: str, sufixo: str = "") -> str: + """Gera nome no padrão Q_A_C_D_[nome_da_tabela].""" + stem = arquivo.rsplit(".", maxsplit=1)[0] + nome = re.sub(r"[^\w]", "_", _remover_acentos(stem).lower()) + nome = re.sub(r"_+", "_", nome).strip("_") + return f"{PREFIXO_TABELA}{nome}{sufixo}" + + +def construir_nome_tabela_indice(subpasta: str) -> str: + slug = re.sub(r"[^\w]", "_", _remover_acentos(subpasta).lower()) + slug = re.sub(r"_+", "_", slug).strip("_") + return f"{PREFIXO_TABELA}indice_{slug}" + + +def _identificar_chunks_horizontais(df_aba: pd.DataFrame) -> list[pd.DataFrame]: + cols_vazias = [ + i for i, col in enumerate(df_aba.columns) if df_aba[col].isnull().all() + ] + pontos = [-1] + cols_vazias + [len(df_aba.columns)] + chunks: list[pd.DataFrame] = [] + for i in range(len(pontos) - 1): + chunk = df_aba.iloc[:, pontos[i] + 1 : pontos[i + 1]].copy() + chunk = chunk.dropna(axis=1, how="all").dropna(axis=0, how="all") + if not chunk.empty and len(chunk.columns) > 1: + chunks.append(chunk.reset_index(drop=True)) + return chunks + + +def _texto_eh_linha_titulo(texto: str) -> bool: + """Detecta linha de título IBGE sem regex sujeito a backtracking (ReDoS).""" + lower = texto.lower().lstrip() + prefixos = ( + "tabela complementar", + "tabela de resultado", + "tabela de resultados", + "tabela", + "apêndice", + "apendice", + ) + for prefixo in prefixos: + if not lower.startswith(prefixo): + continue + sufixo = lower[len(prefixo) :].lstrip() + return bool(sufixo) and sufixo[0].isdigit() + return False + + +def _localizar_linha_titulo(df_raw: pd.DataFrame) -> int | None: + for idx in range(len(df_raw)): + texto = " ".join( + str(v).strip() + for v in df_raw.iloc[idx].tolist() + if str(v).strip().lower() not in VALORES_NULOS + ) + if _texto_eh_linha_titulo(texto): + return idx + return None + + +_PALAVRAS_CABECALHO_DIM = frozenset( + { + "estado", + "codigo", + "código", + "sigla", + "nome", + "territorio", + "território", + "unidade", + "status", + "simbolo", + "símbolo", + "significado", + "legenda", + } +) + + +def _eh_linha_cabecalho_dimensao(linha: pd.Series) -> bool: + textos = [ + str(v).strip().lower() + for v in linha.tolist() + if str(v).strip().lower() not in VALORES_NULOS + ] + if len(textos) < 2: + return False + return any( + any(palavra in texto for palavra in _PALAVRAS_CABECALHO_DIM) for texto in textos + ) + + +def _detectar_inicio_dados(df_raw: pd.DataFrame, idx_titulo: int | None) -> int | None: + mascara_num = df_raw.apply( + lambda r: pd.to_numeric(r, errors="coerce").notna().sum() > 1, axis=1 + ) + if mascara_num.any(): + return int(mascara_num.idxmax()) + + inicio_busca = (idx_titulo + 1) if idx_titulo is not None else 0 + for idx in range(inicio_busca, len(df_raw)): + linha = df_raw.iloc[idx] + textos = [ + str(v).strip() + for v in linha.tolist() + if str(v).strip().lower() not in VALORES_NULOS + ] + if len(textos) < 2: + continue + joined = " ".join(textos) + if re.search(r"fonte:|nota:|legenda", joined, re.IGNORECASE): + continue + if _eh_linha_cabecalho_dimensao(linha) and idx + 1 < len(df_raw): + return idx + 1 + return idx + return None + + +def _extrair_comentario_tabela(df_raw: pd.DataFrame, idx_titulo: int | None) -> str: + if idx_titulo is None: + return "" + texto = " ".join( + str(v).strip() + for v in df_raw.iloc[idx_titulo].tolist() + if str(v).strip().lower() not in VALORES_NULOS + ) + if " - " in texto: + return texto.split(" - ", maxsplit=1)[-1].strip() + return texto.strip() + + +def _construir_cabecalho_flat( + df_raw: pd.DataFrame, idx_titulo: int | None, idx_dados: int +) -> pd.DataFrame: + inicio = (idx_titulo + 1) if idx_titulo is not None else 0 + fim_cab = idx_dados + if idx_dados > 0 and _eh_linha_cabecalho_dimensao(df_raw.iloc[idx_dados - 1]): + fim_cab = idx_dados + cab = df_raw.iloc[inicio:fim_cab].copy().ffill(axis=1) + linhas_validas = [] + for row_idx in range(len(cab)): + valores = [ + str(v).strip() + for v in cab.iloc[row_idx].tolist() + if str(v).strip().lower() not in VALORES_NULOS + ] + if valores: + linhas_validas.append(row_idx) + return cab.iloc[linhas_validas] if linhas_validas else cab.iloc[0:0] + + +def _extrair_nome_coluna_flat(cabecalho: pd.DataFrame, col_idx: int) -> str: + pedacos: list[str] = [] + for row_idx in range(len(cabecalho)): + val = str(cabecalho.iloc[row_idx, col_idx]).strip() + if val.lower() in VALORES_NULOS: + continue + row_vals = cabecalho.iloc[row_idx].dropna().astype(str).str.strip() + unicos = [v for v in row_vals.unique() if v.lower() not in VALORES_NULOS] + if len(unicos) > 1: + pedacos.append(val.split(" - ")[-1].strip()) + elif len(unicos) == 1 and len(row_vals) == 1: + pedacos.append(val.split(" - ")[-1].strip()) + return "_".join(pedacos) if pedacos else f"coluna_{col_idx}" + + +_COLUNAS_AUDITORIA = frozenset({"dt_ingest", "nome_fonte", "subcaminho_fonte"}) + + +def _colunas_dados(df: pd.DataFrame) -> list[str]: + return [c for c in df.columns if c not in _COLUNAS_AUDITORIA] + + +def resolver_chave_primaria(df: pd.DataFrame) -> list[str]: + """ + Define a menor chave composta (prefixo à esquerda) que identifica cada linha. + + Nas tabelas IBGE as dimensões ficam à esquerda e as medidas à direita; usar + só as 3 primeiras colunas falha quando há vários territórios por UF. + """ + cols = _colunas_dados(df) + if not cols: + return [df.columns[0]] + if len(cols) == 1: + return cols + + for n in range(1, len(cols) + 1): + subset = cols[:n] + if df.drop_duplicates(subset=subset).shape[0] == len(df): + return subset + + return cols + + +def _limpar_dataframe(df: pd.DataFrame) -> pd.DataFrame: + if df.empty: + return df + col_dim = df.columns[0] + df = df.dropna(subset=[col_dim]) + df = df[ + ~df[col_dim] + .astype(str) + .str.contains(r"Fonte:|Nota:|Legenda", case=False, na=False) + ] + return df.drop_duplicates() + + +def processar_chunk_excel( + df_raw: pd.DataFrame, + arquivo: str, + subcaminho: str, + sheet_name: str, + sufixo: str = "", +) -> ChunkProcessado | None: + """Aplica flattening de cabeçalhos IBGE e prepara metadados de comentários.""" + idx_titulo = _localizar_linha_titulo(df_raw) + idx_dados = _detectar_inicio_dados(df_raw, idx_titulo) + if idx_dados is None: + logging.warning( + "[quilombolas_parser] Sem dados tabulares em %s / %s", arquivo, sheet_name + ) + return None + + cabecalho = _construir_cabecalho_flat(df_raw, idx_titulo, idx_dados) + nomes_originais = [ + _extrair_nome_coluna_flat(cabecalho, i) for i in range(len(df_raw.columns)) + ] + nomes_norm = deduplicar_colunas( + [normalizar_nome_coluna(n, i) for i, n in enumerate(nomes_originais)] + ) + + df = df_raw.iloc[idx_dados:].copy() + df.columns = nomes_norm + col_comments = { + norm: orig + for norm, orig in zip(nomes_norm, nomes_originais, strict=True) + if norm != orig or orig + } + df = _limpar_dataframe(df) + + colunas_fantasma = [c for c in df.columns if c.startswith("coluna_")] + if colunas_fantasma: + df = df.drop(columns=colunas_fantasma) + col_comments = { + k: v for k, v in col_comments.items() if k not in colunas_fantasma + } + + if df.empty or len(df.columns) == 0: + return None + + return ChunkProcessado( + df=df, + table_name=construir_nome_tabela(arquivo, sufixo=sufixo), + table_comment=_extrair_comentario_tabela(df_raw, idx_titulo), + primary_key=resolver_chave_primaria(df), + col_comments=col_comments, + arquivo=arquivo, + subcaminho=subcaminho, + sheet_name=sheet_name, + ) + + +def extrair_chunks_de_excel( + buffer: io.BytesIO, + arquivo: str, + subcaminho: str, +) -> list[ChunkProcessado]: + """Processa todas as abas e blocos horizontais de um arquivo Excel.""" + excel_file = pd.ExcelFile(buffer) + abas_validas = [ + a + for a in excel_file.sheet_names + if "gráfico" not in a.lower() + and "grafico" not in a.lower() + and "nota" not in a.lower() + ] + if not abas_validas: + abas_validas = [excel_file.sheet_names[0]] + + chunks: list[ChunkProcessado] = [] + for sheet_name in abas_validas: + df_aba = excel_file.parse(sheet_name, header=None) + partes = _identificar_chunks_horizontais(df_aba) + for idx, df_raw in enumerate(partes): + sufixo = f"_parte_{idx + 1}" if len(partes) > 1 else "" + resultado = processar_chunk_excel( + df_raw, arquivo, subcaminho, sheet_name, sufixo=sufixo + ) + if resultado: + chunks.append(resultado) + return chunks + + +_TIPOS_INDICE: tuple[tuple[str, str], ...] = ( + ("cartograma", "Cartograma"), + ("tabela", "Tabela"), + ("apêndice", "Apêndice"), + ("apendice", "Apêndice"), +) + + +def _parsear_linha_indice(linha: str) -> tuple[str, str, str] | None: + """Extrai tipo, número e descrição de uma linha de índice (sem ReDoS).""" + lower = linha.lower() + for chave, rotulo in _TIPOS_INDICE: + if not lower.startswith(chave): + continue + resto = linha[len(chave) :].lstrip() + numero_match = re.match(r"[0-9]+", resto) + if not numero_match: + return None + numero = numero_match.group(0) + descricao = resto[numero_match.end() :].lstrip() + if descricao and descricao[0] in "-–:": + descricao = descricao[1:].lstrip() + return rotulo, numero, descricao or linha + return None + + +def parsear_arquivo_indice(conteudo: str, subpasta: str) -> ChunkProcessado: + """Converte arquivo de índice TXT em tabela estruturada.""" + linhas = [ln.strip() for ln in conteudo.splitlines() if ln.strip()] + registros: list[dict[str, str]] = [] + for idx, linha in enumerate(linhas, start=1): + parsed = _parsear_linha_indice(linha) + if parsed: + tipo, numero, descricao = parsed + registros.append( + { + "ordem": str(idx), + "tipo": tipo, + "numero": numero, + "descricao": descricao, + "linha_original": linha, + } + ) + else: + registros.append( + { + "ordem": str(idx), + "tipo": "", + "numero": "", + "descricao": linha, + "linha_original": linha, + } + ) + + df = pd.DataFrame(registros) + return ChunkProcessado( + df=df, + table_name=construir_nome_tabela_indice(subpasta), + table_comment=f"Índice de tabelas — {subpasta.replace('_', ' ')}", + primary_key=["ordem"], + col_comments={ + "ordem": "Ordem da linha no arquivo de índice", + "tipo": "Tipo do item (Tabela, Cartograma, Apêndice)", + "numero": "Número do item no índice", + "descricao": "Descrição original do item", + "linha_original": "Texto original da linha no arquivo de índice", + }, + arquivo=f"indice_{subpasta.lower()}.txt", + subcaminho=subpasta, + sheet_name="indice", + ) + + +def preparar_registros_insercao(chunk: ChunkProcessado) -> list[dict[str, str]]: + """Adiciona colunas de auditoria, deduplica pela PK e serializa para inserção.""" + from datetime import datetime + + df = chunk.df.copy() + pk = chunk.primary_key or resolver_chave_primaria(df) + df = df.drop_duplicates(subset=pk, keep="last") + df["dt_ingest"] = datetime.now().isoformat() + df["nome_fonte"] = chunk.arquivo + df["subcaminho_fonte"] = chunk.subcaminho + return df.astype(str).to_dict(orient="records") + diff --git a/airflow_lappis/helpers/retry_helpers.py b/airflow_lappis/helpers/retry_helpers.py new file mode 100644 index 00000000..b3e20fb5 --- /dev/null +++ b/airflow_lappis/helpers/retry_helpers.py @@ -0,0 +1,80 @@ +import logging +import time +import functools +from typing import Callable, TypeVar, Any, cast, Tuple + +# Configuração do logger +logger = logging.getLogger(__name__) + +# Definindo um tipo genérico para o decorador +T = TypeVar("T") + + +def retry_on_exception( + max_attempts: int = 3, + initial_delay: float = 1.0, + backoff_factor: float = 2.0, + exceptions_to_retry: Tuple = (Exception,), +) -> Callable[[Callable[..., T]], Callable[..., T]]: + """ + Decorador que implementa um mecanismo de retry para funções que podem falhar. + + Args: + max_attempts (int): Número máximo de tentativas + initial_delay (float): Tempo inicial de espera entre tentativas em segundos + backoff_factor (float): Multiplicador do tempo de espera para cada nova tentativa + exceptions_to_retry (tuple): Exceções que devem ativar o retry + + Returns: + Callable: Função decorada com mecanismo de retry + """ + + def decorator(func: Callable[..., T]) -> Callable[..., T]: + @functools.wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> T: + last_exception = None + delay = initial_delay + + # Extrai informações para logging mais claro + method_name = func.__name__ + + for attempt in range(1, max_attempts + 1): + try: + if attempt > 1: + logger.info( + f"Tentativa {attempt}/{max_attempts} para {method_name}" + ) + + return func(*args, **kwargs) + + except exceptions_to_retry as e: + last_exception = e + + if attempt < max_attempts: + + error_msg = ( + f"Tentativa {attempt} falhou para {method_name}: {str(e)}" + ) + logger.warning(error_msg) + + logger.info( + f"Aguardando {delay:.2f}s antes da próxima tentativa..." + ) + time.sleep(delay) + # Aumenta o delay para a próxima tentativa (backoff exponencial) + delay *= backoff_factor + else: + logger.error( + f"Todas as {max_attempts} tentativas falharam: {method_name}" + ) + + # Se chegou aqui, todas as tentativas falharam + if last_exception: + raise last_exception + + # Este ponto nunca deveria ser alcançado, mas é necessário para tipagem + raise RuntimeError("Erro inesperado no mecanismo de retry") + + return cast(Callable[..., T], wrapper) + + return decorator diff --git a/airflow_lappis/helpers/safe_request.py b/airflow_lappis/helpers/safe_request.py new file mode 100644 index 00000000..f4985119 --- /dev/null +++ b/airflow_lappis/helpers/safe_request.py @@ -0,0 +1,79 @@ +from typing import Any, Optional, Tuple +from http import HTTPStatus +import logging +import time +import json +import httpx + + +def request_safe( + self: Any, method: str, path: str, **kwargs: Any +) -> Tuple[HTTPStatus, Optional[dict | list | str]]: + """ + Versão tolerante a 204 / corpo vazio / não-JSON / JSON inválido. + Usa atributos e client do `self` (DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, + DEFAULT_SLEEP_SECONDS, client). + NÃO altera ClienteBase; apenas é chamada passando `self`. + """ + timeout = kwargs.get("timeout", getattr(self, "DEFAULT_TIMEOUT", 10)) + kwargs["timeout"] = timeout + + max_retries = getattr(self, "DEFAULT_MAX_RETRIES", 3) + sleep_s = getattr(self, "DEFAULT_SLEEP_SECONDS", 2) + + base_url = getattr(self, "base_url", "") # opcional, só para logs + + for attempt in range(max_retries): + try: + logging.info( + "[safe_request] Attempt %s %s %s%s | kwargs=%s", + attempt + 1, + method, + base_url, + path, + {k: v for k, v in kwargs.items() if k != "timeout"}, + ) + + resp = self.client.request(method, path, **kwargs) + status = HTTPStatus(resp.status_code) + + # 204 ou corpo vazio → não tentar json() + if status == HTTPStatus.NO_CONTENT or not resp.content: + logging.info("[safe_request] No content (status=%s)", status) + return status, None + + # Checar Content-Type + ct = (resp.headers.get("Content-Type") or "").lower() + if "application/json" not in ct: + preview = resp.text[:200] if resp.text else "" + logging.warning( + "[safe_request] Non-JSON content (status=%s, ct=%s) | preview=%r", + status, + ct, + preview, + ) + return status, resp.text + + # Tentar JSON com fallback + try: + return status, resp.json() + except json.JSONDecodeError as e: + preview = resp.text[:200] if resp.text else "" + logging.warning( + "[safe_request] Invalid JSON (status=%s): %s | preview=%r", + status, + e, + preview, + ) + return status, resp.text + + except httpx.HTTPError as e: + logging.warning("[safe_request] HTTPError on attempt %s: %s", attempt + 1, e) + if attempt < max_retries - 1: + time.sleep((attempt**2) * sleep_s) + else: + # última tentativa: não levanta exceção — retorna erro “seguro” + return HTTPStatus.SERVICE_UNAVAILABLE, f"request_error: {e}" + + # Fallback: não deveria chegar aqui, mas para satisfazer MyPy + return HTTPStatus.INTERNAL_SERVER_ERROR, "unexpected_error" diff --git a/airflow_lappis/plugins/cliente_base.py b/airflow_lappis/plugins/cliente_base.py new file mode 100755 index 00000000..3f8e6269 --- /dev/null +++ b/airflow_lappis/plugins/cliente_base.py @@ -0,0 +1,66 @@ +import logging +import time +import httpx +from typing import Any, Optional, Tuple +from http import HTTPStatus + + +class ClienteBase: + DEFAULT_TIMEOUT = 10 + DEFAULT_MAX_RETRIES = 3 + DEFAULT_SLEEP_SECONDS = 2 + + def __init__(self, base_url: str, headers: Optional[dict] = None) -> None: + self.base_url = base_url + self.client = httpx.Client(base_url=base_url, headers=headers) + logging.info( + f"[cliente_base.py] Initialized ClienteBase with base_url: {base_url}" + ) + + def request( + self, method: str, path: str, **kwargs: Any + ) -> Tuple[HTTPStatus, Optional[dict | list]]: + """ + Faz uma requisição HTTP em até DEFAULT_MAX_RETRIES+1 tentativas. + + Args: + method (str): HTTP Method. + path (str): URL path. + + Returns: + Tuple[HTTPStatus, dict]: status e resposta da requisição HTTP. + """ + kwargs["timeout"] = kwargs.get("timeout", self.DEFAULT_TIMEOUT) + response = None + + for attempt in range(self.DEFAULT_MAX_RETRIES): + try: + logging.info( + f"[cliente_base.py] Attempt {attempt + 1} for {method} " + f"{self.base_url}{path} with kwargs: {kwargs}" + ) + response = self.client.request(method, path, **kwargs) + response.raise_for_status() + logging.info( + f"[cliente_base.py] Request successful with status " + f"{response.status_code}" + ) + return HTTPStatus(response.status_code), response.json() + except httpx.HTTPError as e: + status = response.status_code if response else "Unknown" + logging.warning( + f"[cliente_base.py] API failed with status {status} on " + f"attempt {attempt + 1}. Error: {str(e)}" + ) + if attempt < self.DEFAULT_MAX_RETRIES: + time.sleep(attempt**2 * self.DEFAULT_SLEEP_SECONDS) + else: + logging.error( + f"[cliente_base.py] API failed after " + f"{self.DEFAULT_MAX_RETRIES} attempts. Error: {str(e)}" + ) + raise Exception( + "API failed after the maximum number of attempts!" + ) from e + + return HTTPStatus.INTERNAL_SERVER_ERROR, None diff --git a/airflow_lappis/plugins/cliente_contratos.py b/airflow_lappis/plugins/cliente_contratos.py new file mode 100755 index 00000000..6711fcb0 --- /dev/null +++ b/airflow_lappis/plugins/cliente_contratos.py @@ -0,0 +1,191 @@ +import http +import logging +from cliente_base import ClienteBase + + +class ClienteContratos(ClienteBase): + BASE_URL = "https://contratos.comprasnet.gov.br/api" + BASE_HEADER = {"accept": "application/json"} + + def __init__(self) -> None: + super().__init__(base_url=ClienteContratos.BASE_URL) + logging.info( + "[cliente_contratos.py] Initialized ClienteContratos with base_url: " + f"{ClienteContratos.BASE_URL}" + ) + + def get_contratos_by_ug(self, ug_code: str) -> list | None: + """ + Obter todos os contratos ativos de uma UG específica. + + Args: + ug_code (str): UG code + + Returns: + list: lista de contratos por ug + """ + endpoint = f"/contrato/ug/{ug_code}" + logging.info(f"[cliente_contratos.py] Fetching contratos for UG code: {ug_code}") + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + "[cliente_contratos.py] Successfully fetched contratos for UG code: " + f"{ug_code}" + ) + return data + else: + logging.warning( + "[cliente_contratos.py] Failed to fetch contratos for UG code: " + f"{ug_code} with status: {status}" + ) + return None + + def get_contratos_inativos_by_ug(self, ug_code: str) -> list | None: + """ + Obter todos os contratos inativos de uma UG específica. + + Args: + ug_code (str): UG code + + Returns: + list: lista de contratos inativos por ug + """ + endpoint = f"/contrato/inativo/ug/{ug_code}" + logging.info(f"[cliente_contratos.py] Fetching contratos for UG code: {ug_code}") + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + "[cliente_contratos.py] Successfully fetched contratos for UG code: " + f"{ug_code}" + ) + return data + else: + logging.warning( + "[cliente_contratos.py] Failed to fetch contratos for UG code: " + f"{ug_code} with status: {status}" + ) + return None + + def get_faturas_by_contrato_id(self, contrato_id: str) -> list | None: + """ + Obter todas as faturas de um contrato específico. + + Args: + contrato_id (str): id do contrato + + Returns: + list: as faturas de um contrato específico. + """ + endpoint = f"/contrato/{contrato_id}/faturas" + logging.info( + f"[cliente_contratos.py] Fetching faturas for contrato ID: {contrato_id}" + ) + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + "[cliente_contratos.py] Successfully fetched faturas for contrato ID: " + f"{contrato_id}" + ) + return data + else: + logging.warning( + "[cliente_contratos.py] Failed to fetch faturas for contrato ID: " + f"{contrato_id} with status: {status}" + ) + return None + + def get_empenhos_by_contrato_id(self, contrato_id: str) -> list | None: + """ + Obter todos os empenhos de um contrato específico. + + Args: + contrato_id (str): id do contrato + + Returns: + list: os empenhos de um contrato específico. + """ + endpoint = f"/contrato/{contrato_id}/empenhos" + logging.info( + f"[cliente_contratos.py] Fetching empenhos for contrato ID: {contrato_id}" + ) + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + "[cliente_contratos.py] Successfully fetched empenhos for contrato ID: " + f"{contrato_id}" + ) + return data + else: + logging.warning( + "[cliente_contratos.py] Failed to fetch empenhos for contrato ID: " + f"{contrato_id} with status: {status}" + ) + return None + + def get_cronograma_by_contrato_id(self, contrato_id: str) -> list | None: + """ + Obter todos os cronogramas de um contrato específico. + + Args: + contrato_id (str): id do contrato + + Returns: + list: cronogramas de um contrato específico. + """ + endpoint = f"/contrato/{contrato_id}/cronograma" + logging.info( + f"[cliente_contratos.py] Fetching cronograma for contrato ID: {contrato_id}" + ) + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + "[cliente_contratos.py] Successfully fetched cronograma for contrato ID: " + f"{contrato_id}" + ) + return data + else: + logging.warning( + "[cliente_contratos.py] Failed to fetch cronograma for contrato ID: " + f"{contrato_id} with status: {status}" + ) + return None + + def get_terceirizados_by_contrato_id(self, contrato_id: str) -> list | None: + """ + Obter todos os terceirizados de um contrato específico. + + Args: + contrato_id (str): id do contrato + + Returns: + list: os terceirizados de um contrato específico. + """ + endpoint = f"/contrato/{contrato_id}/terceirizados" + logging.info( + f"[cliente_contratos.py] Fetching terceirizados for contrato: {contrato_id}" + ) + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + "[cliente_contratos.py] Successfully fetched terceirizados for contrato: " + f"{contrato_id}" + ) + return data + else: + logging.warning( + "[cliente_contratos.py] Failed to fetch terceirizados for contrato ID: " + f"{contrato_id} with status: {status}" + ) + return None diff --git a/airflow_lappis/plugins/cliente_deputados.py b/airflow_lappis/plugins/cliente_deputados.py new file mode 100644 index 00000000..48fa27b0 --- /dev/null +++ b/airflow_lappis/plugins/cliente_deputados.py @@ -0,0 +1,126 @@ +import http +import logging +from typing import Any +from cliente_base import ClienteBase + + +class ClienteDeputados(ClienteBase): + """ + Cliente para consumir a API de Dados Abertos da Câmara dos Deputados. + """ + + BASE_URL = "https://dadosabertos.camara.leg.br/api/v2" + BASE_HEADER = {"accept": "application/json"} + PAGE_SIZE = 100 + + def __init__(self) -> None: + super().__init__(base_url=ClienteDeputados.BASE_URL) + logging.info( + "[cliente_deputados.py] Initialized ClienteDeputados with base_url: " + f"{ClienteDeputados.BASE_URL}" + ) + + def get_deputados(self, **params: Any) -> list: + """ + Obter lista de deputados + """ + endpoint = "/deputados" + logging.info(f"[cliente_deputados.py] Fetching deputados with params: {params}") + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, dict): + deputados: list[dict[str, Any]] = data.get("dados", []) + logging.info( + f"[cliente_deputados.py] Successfully fetched {len(deputados)} deputados" + ) + return deputados + else: + logging.warning( + f"[cliente_deputados.py] Failed to fetch deputados with status: {status}" + ) + return None + + def get_all_deputados(self) -> list: + """ + Itera por todas as páginas da API e retorna a lista completa de deputados. + """ + all_deputados = [] + pagina = 1 + + while True: + params = { + "pagina": pagina, + "itens": self.PAGE_SIZE, + "dataInicio": "1823-01-01", + } + deputados = self.get_deputados(**params) + + if not deputados: + break + + all_deputados.extend(deputados) + + if len(deputados) < self.PAGE_SIZE: + break + + pagina += 1 + + return all_deputados + + def get_deputados_atuais(self) -> list[dict[str, Any]] | None: + """Retorna a lista atual de deputados (sem recorte histórico).""" + all_deputados = [] + pagina = 1 + + while True: + params = {"pagina": pagina, "itens": self.PAGE_SIZE} + deputados = self.get_deputados(**params) + + # Falha de API nao deve ser confundida com snapshot vazio. + if deputados is None: + logging.error( + "[cliente_deputados.py] Falha ao buscar deputados atuais na " + f"pagina={pagina}; abortando snapshot de atuais" + ) + return None + + if not deputados: + break + + all_deputados.extend(deputados) + + if len(deputados) < self.PAGE_SIZE: + break + + pagina += 1 + + return all_deputados + + def get_historico_deputado( + self, deputado_id: int | str + ) -> list[dict[str, Any]] | None: + """Obtém o histórico de um deputado específico.""" + endpoint = f"/deputados/{deputado_id}/historico" + logging.info( + f"[cliente_deputados.py] Fetching historico for deputado_id={deputado_id}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + + if status == http.HTTPStatus.OK and isinstance(data, dict): + historico = data.get("dados", []) + if isinstance(historico, list): + return historico + if isinstance(historico, dict): + return [historico] + + logging.warning( + "[cliente_deputados.py] Failed to fetch historico for " + f"deputado_id={deputado_id} with status: {status}" + ) + return None diff --git a/airflow_lappis/plugins/cliente_email.py b/airflow_lappis/plugins/cliente_email.py new file mode 100755 index 00000000..2f41738c --- /dev/null +++ b/airflow_lappis/plugins/cliente_email.py @@ -0,0 +1,210 @@ +import logging +import io +import zipfile +from typing import Optional, cast, List, Dict +import pandas as pd +from pandas.errors import EmptyDataError +from imap_tools import MailBox, AND +import chardet +from datetime import datetime, date +import pytz + +# Configuração do log +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) + + +def format_csv( + csv_data: str, column_mapping: Optional[Dict[int, str]], skiprows: int +) -> pd.DataFrame: + """Formata um arquivo CSV conforme mapeamento de colunas.""" + if column_mapping: + df = pd.read_csv(io.StringIO(csv_data), skiprows=skiprows, header=None) + column_names: List[str] = [ + column_mapping.get(i, f"col_{i}") for i in range(len(df.columns)) + ] + df.columns = pd.Index(column_names) + else: + df = pd.read_csv(io.StringIO(csv_data), skiprows=skiprows, header=0) + return df + + +def extract_csv_from_zip( + zip_payload: bytes, column_mapping: dict, skiprows: int = 0 +) -> Optional[pd.DataFrame]: + """Extrai e formata o primeiro arquivo CSV encontrado em um ZIP.""" + with zipfile.ZipFile(io.BytesIO(zip_payload)) as zip_file: + for file_name in zip_file.namelist(): + if file_name.lower().endswith(".csv"): + raw_data = zip_file.read(file_name) + encoding = chardet.detect(raw_data)["encoding"] + + if not raw_data.strip(): + logging.warning("CSV vazio no anexo ZIP: %s", file_name) + continue + + try: + decoded_data = raw_data.decode(encoding or "utf-8", errors="replace") + if not decoded_data.strip(): + logging.warning("CSV vazio no anexo ZIP: %s", file_name) + continue + return format_csv(decoded_data, column_mapping, skiprows) + except EmptyDataError: + logging.warning( + "CSV sem colunas apos skiprows=%s no arquivo: %s", + skiprows, + file_name, + ) + continue + return None + + +def fetch_email_with_zip( + imap_server: str, + email: str, + password: str, + sender_email: str, + subject: str, + target_date: Optional[date] = None, +) -> List[bytes]: + """Busca e-mails da data alvo (ou dia atual) e retorna os anexos ZIP.""" + query_date = target_date or datetime.now(pytz.timezone("America/Sao_Paulo")).date() + zip_payloads: List[bytes] = [] + with MailBox(imap_server).login(email, password) as mailbox: + # bulk=True: single IMAP FETCH command for all messages (avoids overquota) + for msg in mailbox.fetch( + AND(date=query_date, from_=sender_email, subject=subject), + bulk=True, + ): + for attachment in msg.attachments: + if attachment.filename.lower().endswith(".zip"): + zip_payloads.append(cast(bytes, attachment.payload)) + return zip_payloads + + + +def fetch_email_with_csv( + imap_server: str, email: str, password: str, sender_email: str, subject: str +) -> List[bytes]: + """Busca todos os e-mails do dia atual e retorna anexos CSV diretos.""" + today = datetime.now(pytz.timezone("America/Sao_Paulo")).date() + csv_payloads: List[bytes] = [] + with MailBox(imap_server).login(email, password) as mailbox: + # bulk=True: single IMAP FETCH command for all messages (avoids overquota) + for msg in mailbox.fetch(AND(date=today, from_=sender_email, subject=subject), bulk=True): + for attachment in msg.attachments: + file_name = (attachment.filename or "").lower() + if file_name.endswith(".csv"): + csv_payloads.append(cast(bytes, attachment.payload)) + return csv_payloads + + +def extract_csv_from_payload( + payload: bytes, column_mapping: dict, skiprows: int = 0 +) -> Optional[pd.DataFrame]: + """Decodifica payload CSV e aplica formatação padrão.""" + if not payload.strip(): + logging.warning("Anexo CSV vazio.") + return None + + encoding = chardet.detect(payload)["encoding"] + decoded_data = payload.decode(encoding or "utf-8", errors="replace") + if not decoded_data.strip(): + logging.warning("Anexo CSV vazio apos decodificacao.") + return None + + try: + return format_csv(decoded_data, column_mapping, skiprows) + except EmptyDataError: + logging.warning("CSV sem colunas apos skiprows=%s.", skiprows) + return None + + +def fetch_and_process_email( + imap_server: str, + email: str, + password: str, + sender_email: str, + subject: str, + column_mapping: dict, + skiprows: int = 0, + target_date: Optional[date] = None, +) -> Optional[str]: + """Busca e processa e-mails da data alvo (ou dia atual), extraindo CSVs.""" + try: + zip_payloads = fetch_email_with_zip( + imap_server, + email, + password, + sender_email, + subject, + target_date=target_date, + ) + if not zip_payloads: + logging.warning("Nenhum anexo ZIP encontrado.") + return None + + logging.info("Total de anexos ZIP encontrados: %s", len(zip_payloads)) + + dataframes: List[pd.DataFrame] = [] + for idx, zip_payload in enumerate(zip_payloads, start=1): + csv_data = extract_csv_from_zip(zip_payload, column_mapping, skiprows) + if csv_data is not None: + dataframes.append(csv_data) + else: + logging.warning( + "ZIP %s ignorado por nao conter CSV valido.", + idx, + ) + + if dataframes: + combined_df = pd.concat(dataframes, ignore_index=True) + return combined_df.to_csv(index=False) + + logging.warning("Nenhum CSV processado.") + except Exception as e: + logging.error(f"Erro ao processar e-mails: {e}") + raise + + +def fetch_and_process_email_csv_attachment( + imap_server: str, + email: str, + password: str, + sender_email: str, + subject: str, + column_mapping: dict, + skiprows: int = 0, +) -> Optional[str]: + """Busca e processa e-mails do dia, extraindo CSVs anexados diretamente.""" + try: + csv_payloads = fetch_email_with_csv( + imap_server, email, password, sender_email, subject + ) + if not csv_payloads: + logging.warning("Nenhum anexo CSV encontrado.") + return None + + logging.info("Total de anexos CSV encontrados: %s", len(csv_payloads)) + + dataframes: List[pd.DataFrame] = [] + for idx, payload in enumerate(csv_payloads, start=1): + csv_data = extract_csv_from_payload(payload, column_mapping, skiprows) + if csv_data is not None: + dataframes.append(csv_data) + else: + logging.warning( + "CSV %s ignorado por nao conter dados validos.", + idx, + ) + + if dataframes: + combined_df = pd.concat(dataframes, ignore_index=True) + return combined_df.to_csv(index=False) + + logging.warning("Nenhum CSV processado.") + return None + except Exception as e: + logging.error(f"Erro ao processar e-mails com CSV direto: {e}") + raise diff --git a/airflow_lappis/plugins/cliente_github.py b/airflow_lappis/plugins/cliente_github.py new file mode 100644 index 00000000..6d0e33d2 --- /dev/null +++ b/airflow_lappis/plugins/cliente_github.py @@ -0,0 +1,137 @@ +""" +Cliente para interagir com a API do GitHub. +""" + +import base64 +import logging +from typing import Dict, Any, Optional + +import requests + + +class ClienteGitHub: + """Cliente para operações com GitHub API.""" + + BASE_URL = "https://api.github.com" + + def __init__(self, token: str) -> None: + """ + Inicializa o cliente GitHub. + + Args: + token: Token de acesso pessoal do GitHub + """ + self.token = token + self.headers = { + "Authorization": f"token {token}", + "Accept": "application/vnd.github.v3+json", + } + logging.info("[cliente_github.py] Cliente GitHub inicializado") + + def get_file_sha( + self, owner: str, repo: str, path: str, branch: str = "main" + ) -> Optional[str]: + """ + Obtém o SHA de um arquivo no repositório. + + Args: + owner: Proprietário do repositório + repo: Nome do repositório + path: Caminho do arquivo no repositório + branch: Branch (padrão: main) + + Returns: + SHA do arquivo ou None se não existir + """ + url = f"{self.BASE_URL}/repos/{owner}/{repo}/contents/{path}" + params = {"ref": branch} + + try: + response = requests.get(url, headers=self.headers, params=params) + if response.status_code == 200: + data = response.json() + sha: Optional[str] = data.get("sha") + logging.info(f"[cliente_github.py] SHA obtido para {path}: {sha}") + return sha + elif response.status_code == 404: + logging.info(f"[cliente_github.py] Arquivo {path} não existe ainda") + return None + else: + logging.error( + f"[cliente_github.py] Erro ao obter SHA: {response.status_code} - " + f"{response.text}" + ) + return None + except Exception as e: + logging.error(f"[cliente_github.py] Erro ao obter SHA: {str(e)}") + return None + + def update_file( + self, + owner: str, + repo: str, + path: str, + content: str, + message: str, + branch: str = "main", + ) -> Dict[str, Any]: + """ + Cria ou atualiza um arquivo no repositório. + + Args: + owner: Proprietário do repositório + repo: Nome do repositório + path: Caminho do arquivo no repositório + content: Conteúdo do arquivo (será codificado em base64) + message: Mensagem do commit + branch: Branch (padrão: main) + + Returns: + Resposta da API do GitHub + + Raises: + Exception: Se houver erro na atualização + """ + url = f"{self.BASE_URL}/repos/{owner}/{repo}/contents/{path}" + + # Codificar conteúdo em base64 + content_bytes = content.encode("utf-8") + content_base64 = base64.b64encode(content_bytes).decode("utf-8") + + # Obter SHA do arquivo existente (necessário para atualização) + sha = self.get_file_sha(owner, repo, path, branch) + + # Preparar payload + payload = { + "message": message, + "content": content_base64, + "branch": branch, + } + + if sha: + payload["sha"] = sha + logging.info(f"[cliente_github.py] Atualizando arquivo existente: {path}") + else: + logging.info(f"[cliente_github.py] Criando novo arquivo: {path}") + + try: + response = requests.put(url, headers=self.headers, json=payload) + + if response.status_code in [200, 201]: + data: Dict[str, Any] = response.json() + logging.info( + f"[cliente_github.py] Arquivo {path} " + f"{'atualizado' if sha else 'criado'} com sucesso" + ) + return data + else: + error_msg = ( + f"Erro ao atualizar arquivo: {response.status_code} - " + f"{response.text}" + ) + logging.error(f"[cliente_github.py] {error_msg}") + raise Exception(error_msg) + + except Exception as e: + logging.error(f"[cliente_github.py] Erro ao atualizar arquivo: {str(e)}") + raise diff --git a/airflow_lappis/plugins/cliente_ibge.py b/airflow_lappis/plugins/cliente_ibge.py new file mode 100644 index 00000000..c70bcc91 --- /dev/null +++ b/airflow_lappis/plugins/cliente_ibge.py @@ -0,0 +1,190 @@ +import io +import logging +from contextlib import contextmanager + +# ftp.ibge.gov.br é um servidor público do governo +# brasileiro que não oferece suporte a FTPS/SFTP. Apenas dados +# públicos e anônimos são trafegados nessa conexão. +from ftplib import FTP # NOSONAR + +from cliente_base import ClienteBase + + +class ClienteIBGE(ClienteBase): + FTP_HOST = "ftp.ibge.gov.br" + BASE_DIR = "/Censos/Censo_Demografico_2022/" + + def __init__(self, database: str) -> None: + self.host = ClienteIBGE.FTP_HOST + self.database = database + logging.info("[cliente_ibge] Inicializando conexão FTP com: %s", self.host) + + @contextmanager + def _conectar(self, subcaminho: str = ""): + """ + Abre uma conexão FTP com o servidor público do IBGE. + + Uso: + with self._conectar() as ftp: + ftp.nlst() + """ + full_path = self._caminho_remoto(subcaminho) + ftp = FTP(timeout=30) # NOSONAR + try: + ftp.connect(self.host) + resp = ftp.login(user="anonymous", passwd="anonymous@") + logging.info("[cliente_ibge] FTP login: %s", resp) + ftp.set_pasv(True) + ftp.cwd(full_path) + yield ftp + finally: + try: + ftp.quit() + except Exception: + ftp.close() + + # Interface pública + def listar_arquivos_alvo(self) -> list[str]: + """Lista arquivos Excel/CSV do diretório do Censo 2022.""" + try: + with self._conectar() as ftp: + arquivos = ftp.nlst() + + filtrados = [f for f in arquivos if f.endswith((".xlsx", ".xls", ".csv"))] + logging.info("[cliente_ibge] %d arquivo(s) encontrado(s).", len(filtrados)) + return filtrados + + except Exception as exc: + logging.error("[cliente_ibge] Erro ao listar arquivos: %s", exc) + return [] + + def _caminho_remoto(self, subcaminho: str = "") -> str: + base = f"{self.BASE_DIR.rstrip('/')}/{self.database.lstrip('/')}" + if not subcaminho: + return base + return f"{base}/{subcaminho.strip('/')}" + + def listar_arquivos_em_subpastas( + self, + subpastas: list[str], + extensoes: tuple[str, ...] = (".xlsx", ".xls", ".csv"), + formato_preferido: str | None = "xlsx", + ) -> list[dict[str, str]]: + """ + Lista arquivos alvo em subpastas relativas ao diretório do tema. + + Retorna lista de dicts com chaves ``subcaminho`` e ``arquivo``. + Quando ``formato_preferido`` é informado, prioriza arquivos dessa + subpasta (ex.: ``xlsx`` em vez de ``ods``). + """ + resultado: list[dict[str, str]] = [] + try: + with self._conectar() as ftp: + base = self._caminho_remoto() + for subpasta in subpastas: + caminho = f"{base}/{subpasta.strip('/')}" + if formato_preferido: + caminho = f"{caminho}/{formato_preferido}" + try: + ftp.cwd(caminho) + except Exception as exc: + logging.warning( + "[cliente_ibge] Subpasta inacessível '%s': %s", + caminho, + exc, + ) + continue + + for nome in ftp.nlst(): + if nome in (".", ".."): + continue + if nome.endswith(extensoes): + resultado.append( + { + "subcaminho": ( + f"{subpasta.strip('/')}/{formato_preferido}" + if formato_preferido + else subpasta.strip("/") + ), + "arquivo": nome, + } + ) + + logging.info( + "[cliente_ibge] %d arquivo(s) em subpastas: %s", + len(resultado), + subpastas, + ) + return resultado + + except Exception as exc: + logging.error("[cliente_ibge] Erro ao listar subpastas: %s", exc) + return [] + + def listar_arquivos_texto( + self, entradas: list[tuple[str, str]] + ) -> list[dict[str, str]]: + """ + Lista arquivos de texto (índices) em subpastas. + + ``entradas`` é uma lista de tuplas ``(subpasta, nome_arquivo)``. + """ + encontrados: list[dict[str, str]] = [] + try: + with self._conectar() as ftp: + base = self._caminho_remoto() + for subpasta, nome_arquivo in entradas: + caminho = f"{base}/{subpasta.strip('/')}" + try: + ftp.cwd(caminho) + if nome_arquivo in ftp.nlst(): + encontrados.append( + { + "subcaminho": subpasta.strip("/"), + "arquivo": nome_arquivo, + } + ) + except Exception as exc: + logging.warning( + "[cliente_ibge] Índice não encontrado em '%s': %s", + caminho, + exc, + ) + return encontrados + except Exception as exc: + logging.error("[cliente_ibge] Erro ao listar índices: %s", exc) + return [] + + def obter_conteudo_arquivo( + self, nome_arquivo: str, subcaminho: str = "" + ) -> io.BytesIO | None: + """Baixa um arquivo do FTP diretamente para memória.""" + buffer = io.BytesIO() + try: + with self._conectar(subcaminho=subcaminho) as ftp: + logging.info( + "[cliente_ibge] Baixando: %s/%s", subcaminho or ".", nome_arquivo + ) + ftp.retrbinary(f"RETR {nome_arquivo}", buffer.write) + + buffer.seek(0) + return buffer + + except Exception as exc: + logging.error("[cliente_ibge] Erro ao baixar '%s': %s", nome_arquivo, exc) + return None + + def obter_conteudo_texto( + self, nome_arquivo: str, subcaminho: str = "" + ) -> str | None: + """Baixa um arquivo de texto do FTP e retorna o conteúdo decodificado.""" + buffer = self.obter_conteudo_arquivo(nome_arquivo, subcaminho=subcaminho) + if not buffer: + return None + raw = buffer.read() + for encoding in ("utf-8", "latin-1", "cp1252"): + try: + return raw.decode(encoding) + except UnicodeDecodeError: + continue + return raw.decode("latin-1", errors="replace") diff --git a/airflow_lappis/plugins/cliente_mrv.py b/airflow_lappis/plugins/cliente_mrv.py new file mode 100644 index 00000000..ecc88cc4 --- /dev/null +++ b/airflow_lappis/plugins/cliente_mrv.py @@ -0,0 +1,16 @@ +from airflow_lappis.plugins.cliente_base import ClienteBase +from typing import Optional, Dict, Any, Tuple +from http import HTTPStatus + +class ClienteMRV(ClienteBase): + """ + Cliente para integração com a API da MRV. + """ + def __init__(self, base_url: str = "https://api.mrv.com.br", headers: Optional[dict] = None) -> None: + super().__init__(base_url=base_url, headers=headers) + + def consultar_empreendimentos(self, params: Optional[Dict[str, Any]] = None) -> Tuple[HTTPStatus, Optional[dict | list]]: + """ + Consulta a lista de empreendimentos imobiliários da MRV. + """ + return self.request("GET", "/empreendimentos", params=params) diff --git a/airflow_lappis/plugins/cliente_partidos.py b/airflow_lappis/plugins/cliente_partidos.py new file mode 100644 index 00000000..c6d7b363 --- /dev/null +++ b/airflow_lappis/plugins/cliente_partidos.py @@ -0,0 +1,86 @@ +import http +import logging +from typing import Any +from cliente_base import ClienteBase + + +class ClientePartidos(ClienteBase): + """ + Cliente para consumir a API de Dados Abertos da Câmara dos Deputados para pegar a logo dos partidos. + """ + + BASE_URL = "https://dadosabertos.camara.leg.br/api/v2" + BASE_HEADER = {"accept": "application/json"} + + def __init__(self) -> None: + super().__init__(base_url=ClientePartidos.BASE_URL) + logging.info( + "[cliente_partidos.py] Initialized ClientePartidos with base_url: " + f"{ClientePartidos.BASE_URL}" + ) + + def get_partidos(self, **params: Any) -> list: + """ + Obter lista de partidos + """ + endpoint = "/partidos" + logging.info(f"[cliente_partidos.py] Fetching partidos with params: {params}") + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, dict): + partidos: list[dict[str, Any]] = data.get("dados", []) + logging.info( + f"[cliente_partidos.py] Successfully fetched {len(partidos)} partidos" + ) + return partidos + else: + logging.warning( + f"[cliente_partidos.py] Failed to fetch partidos with status: {status}" + ) + return None + + def get_all_partidos(self) -> list: + """ + Itera por todas as páginas da API e retorna a lista completa de partidos. + """ + all_partidos = [] + pagina = 1 + + while True: + params = {"pagina": pagina, "itens": 100, "ordem": "ASC", "ordenarPor": "sigla"} + partidos = self.get_partidos(**params) + + if not partidos: + break + + all_partidos.extend(partidos) + + if len(partidos) < 100: + break + + pagina += 1 + + return all_partidos + + def get_partido_by_id(self, partido_id: int) -> dict: + """ + Obter detalhes de um partido específico pelo ID + """ + endpoint = f"/partidos/{partido_id}" + logging.info(f"[cliente_partidos.py] Fetching partido ID: {partido_id}") + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + + if status == http.HTTPStatus.OK and isinstance(data, dict): + partido: dict[str, Any] = data.get("dados", {}) + return partido + else: + logging.warning( + f"[cliente_partidos.py] Failed to fetch partido {partido_id} with status: {status}" + ) + return None diff --git a/airflow_lappis/plugins/cliente_pncp.py b/airflow_lappis/plugins/cliente_pncp.py new file mode 100644 index 00000000..57629210 --- /dev/null +++ b/airflow_lappis/plugins/cliente_pncp.py @@ -0,0 +1,506 @@ +import http +import logging +from typing import Any, Dict, List, Optional, Tuple +from cliente_base import ClienteBase +from safe_request import request_safe + + +# logging.basicConfig( +# level=logging.INFO, # ou DEBUG para depurar +# format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", +# handlers=[logging.StreamHandler(sys.stdout)], +# force=True # só no CLI; evita configs antigas bloqueando +# ) + +logger = logging.getLogger(__name__) + + +def parse_numero_controle(numero_controle: str) -> Tuple[str, str, str, str]: + """ + Recebe string no formato 'CNPJ-DIGITO-SEQUENCIAL/ANO' + e retorna (cnpj, digito, sequencial, ano). + """ + # Primeiro divide pelo '/' para separar ano + parte_esquerda, ano = numero_controle.split("/") + + # Depois divide a parte esquerda pelos '-' + cnpj, digito, sequencial = parte_esquerda.split("-") + + return cnpj, digito, sequencial, ano + + +def _ymd(ano: int, mes: int, dia: int) -> str: + """Formata YYYYMMDD com zero-padding correto.""" + return f"{ano:04d}{mes:02d}{dia:02d}" + + +class ClientePNCP(ClienteBase): + """ + Cliente para consultar publicações de contratações no PNCP. + + Documentação (resumo do uso): + - Base: https://pncp.gov.br/api/consulta + - Endpoint: /v1/contratacoes/publicacao + - Parâmetros (querystring): + dataInicial (yyyymmdd) + dataFinal (yyyymmdd) + codigoModalidadeContratacao (int) + uf (sigla do estado, ex.: 'DF') + codigoMunicipioIbge (int - 7 dígitos) + cnpj (apenas dígitos) + codigoUnidadeAdministrativa (int) + idUsuario (int) + pagina (int) + """ + + BASE_URL = "https://pncp.gov.br/api" + BASE_HEADER = {"accept": "*/*"} + + def __init__(self, rate_limit_per_min: int = 120) -> None: + super().__init__(base_url=ClientePNCP.BASE_URL) + logger.info( + "[cliente_pncp.py] Initialized ClientePNCP with base_url: %s", + ClientePNCP.BASE_URL, + ) + + def get_contratacoes_publicacao( + self, + data_inicial: str, + data_final: str, + codigo_modalidade_contratacao: Optional[int] = None, + uf: Optional[str] = None, + codigo_municipio_ibge: Optional[int] = None, + cnpj: Optional[str] = None, + codigo_unidade_administrativa: Optional[int] = None, + id_usuario: Optional[int] = None, + pagina: int = 1, + ) -> Tuple[List[Dict[str, Any]], int]: # <- mudei o tipo de retorno + """ + Busca publicações de contratações no PNCP (uma página). + + Returns: + (lista_itens, total_paginas) + """ + endpoint = "/consulta/v1/contratacoes/publicacao" + + params: Dict[str, Any] = { + "dataInicial": data_inicial, + "dataFinal": data_final, + "pagina": pagina, + } + params["codigoModalidadeContratacao"] = codigo_modalidade_contratacao + params["cnpj"] = cnpj + + logger.info( + "[cliente_pncp.py] Fetching PNCP | params=%s | pagina=%s", + {k: v for k, v in params.items() if k != "pagina"}, + pagina, + ) + + status, data = request_safe( + self, + http.HTTPMethod.GET, + endpoint, + headers={"accept": "application/json"}, + params=params, + ) + + # Se não veio 200, não tente decodificar estrutura + if status != http.HTTPStatus.OK: + logger.warning( + "[cliente_pncp.py] HTTP %s | pagina=%s | tipo=%s", + status, + pagina, + type(data).__name__, + ) + return [], 0 + + itens: List[Dict[str, Any]] = [] + total_paginas: int = 0 + + # 1) Se a API devolver uma lista direta + if isinstance(data, list): + itens = data + + # 2) Se vier envelopado em dict + elif isinstance(data, dict): + # tente chaves comuns para itens + key = "data" + val = data.get(key) + if isinstance(val, list): + itens = val + + # tente extrair total de páginas (se existir) + k = "totalPaginas" + if isinstance(data.get(k), int): + total_paginas = data[k] + + if not itens: + logger.warning( + "[cliente_pncp.py] 200 mas sem lista reconhecida. keys=%s", + list(data.keys()), + ) + + # 3) Se vier string/None/outro tipo → trate como vazio + else: + logger.warning( + "[cliente_pncp.py] 200 mas resposta não-JSON-list/dict | tipo=%s", + type(data).__name__, + ) + + logger.info( + "[cliente_pncp.py] OK | pagina=%s | rows=%s | total_paginas=%s", + pagina, + len(itens), + total_paginas, + ) + return itens, total_paginas + + def get_contratacoes_publicacao_paginado( + self, + data_inicial: str, + data_final: str, + codigo_modalidade_contratacao: Optional[int] = None, + uf: Optional[str] = None, + codigo_municipio_ibge: Optional[int] = None, + cnpj: Optional[str] = None, + codigo_unidade_administrativa: Optional[int] = None, + id_usuario: Optional[int] = None, + pagina_inicial: int = 1, + max_paginas: Optional[int] = None, + ) -> List[Dict[str, Any]]: + """ + Busca publicações de contratações no PNCP, agregando múltiplas páginas. + + Itera páginas até: + - retornar lista vazia/None, + - alcançar max_paginas (se fornecido). + + Returns: + list: Lista agregada com todas as linhas coletadas. + """ + agregados: List[Dict[str, Any]] = [] + pagina = pagina_inicial + paginas_coletadas = 0 + + while True: + if max_paginas is not None and paginas_coletadas >= max_paginas: + logger.info("[cliente_pncp.py] Max de páginas atingido: %s", max_paginas) + break + + page_data, max_paginas = self.get_contratacoes_publicacao( + data_inicial=data_inicial, + data_final=data_final, + codigo_modalidade_contratacao=codigo_modalidade_contratacao, + uf=uf, + codigo_municipio_ibge=codigo_municipio_ibge, + cnpj=cnpj, + codigo_unidade_administrativa=codigo_unidade_administrativa, + id_usuario=id_usuario, + pagina=pagina, + ) + + if not page_data: + logger.info( + "[cliente_pncp.py] Fim da paginação (vazio/None) na página %s", + pagina, + ) + break + + agregados.extend(page_data) + paginas_coletadas += 1 + pagina += 1 + + logger.info( + "[cliente_pncp.py] Coleta paginada concluída | total_paginas=%s | " + "total_registros=%s", + paginas_coletadas, + len(agregados), + ) + return agregados + + def get_contratacoes_publicacao_semestral( + self, + data_inicial: str, # 'YYYYMMDD' + data_final: str, # 'YYYYMMDD' + codigo_modalidade_contratacao: Optional[int] = None, + uf: Optional[str] = None, + codigo_municipio_ibge: Optional[int] = None, + cnpj: Optional[str] = None, + codigo_unidade_administrativa: Optional[int] = None, + id_usuario: Optional[int] = None, + pagina_inicial: int = 1, + max_paginas: Optional[int] = None, + ) -> List[Dict[str, Any]]: + """ + Varre semestralmente entre data_inicial e data_final usando janelas + half-open [início, fim). + Em cada janela, pagina até esgotar os resultados. + """ + logger.info( + "[PNCP][semestral] INÍCIO | intervalo_solicitado=[%s, %s] | " + "filtros: modalidade=%s, uf=%s, ibge=%s, cnpj=%s, ua=%s, usuario=%s | " + "pagina_inicial=%s, max_paginas=%s", + data_inicial, + data_final, + codigo_modalidade_contratacao, + uf, + codigo_municipio_ibge, + cnpj, + codigo_unidade_administrativa, + id_usuario, + pagina_inicial, + max_paginas, + ) + + agregados: List[Dict[str, Any]] = [] + try: + ano_ini = int(data_inicial[:4]) + ano_fim = int(data_final[:4]) + except Exception as e: + logger.error( + "[PNCP][semestral] ERRO ao parsear anos de data_inicial/data_final: %s", + e, + exc_info=True, + ) + raise + + limite_inicio = data_inicial + limite_fim = data_final + + logger.debug( + "[PNCP][semestral] anos_detectados: ano_ini=%s, ano_fim=%s | " + "limites_clip=[%s, %s]", + ano_ini, + ano_fim, + limite_inicio, + limite_fim, + ) + + for ano in range(ano_ini, ano_fim + 1): + logger.info("[PNCP][semestral] Ano %s → preparando janelas H1 e H2", ano) + + # H1: [ano-01-01, ano-07-01) + s1_ini = _ymd(ano, 1, 1) + s1_fim = _ymd(ano, 7, 1) + + # H2: [ano-07-01, (ano+1)-01-01) + s2_ini = _ymd(ano, 7, 1) + s2_fim = _ymd(ano + 1, 1, 1) + + # Clip com limites externos + s1_ini_clip = max(s1_ini, limite_inicio) + s1_fim_clip = min(s1_fim, limite_fim) + s2_ini_clip = max(s2_ini, limite_inicio) + s2_fim_clip = min(s2_fim, limite_fim) + + logger.debug( + "[PNCP][semestral] Ano %s | H1=[%s, %s) → clip=[%s, %s) | " + "H2=[%s, %s) → clip=[%s, %s)", + ano, + s1_ini, + s1_fim, + s1_ini_clip, + s1_fim_clip, + s2_ini, + s2_fim, + s2_ini_clip, + s2_fim_clip, + ) + + # --- H1 --- + if s1_ini_clip < s1_fim_clip: + logger.info( + "[PNCP][semestral] Ano %s | H1 CLIP válido: [%s, %s) → " + "iniciando coleta paginada", + ano, + s1_ini_clip, + s1_fim_clip, + ) + try: + page_data = self.get_contratacoes_publicacao_paginado( + data_inicial=s1_ini_clip, + data_final=s1_fim_clip, + codigo_modalidade_contratacao=codigo_modalidade_contratacao, + uf=uf, + codigo_municipio_ibge=codigo_municipio_ibge, + cnpj=cnpj, + codigo_unidade_administrativa=codigo_unidade_administrativa, + id_usuario=id_usuario, + pagina_inicial=pagina_inicial, + max_paginas=max_paginas, + ) + n = len(page_data) if page_data else 0 + logger.info( + "[PNCP][semestral] Ano %s | H1 coletado com sucesso | linhas=%s", + ano, + n, + ) + if page_data: + agregados.extend(page_data) + except Exception as e: + logger.error( + "[PNCP][semestral] Ano %s | H1 FALHOU: %s", ano, e, exc_info=True + ) + else: + logger.info( + "[PNCP][semestral] Ano %s | H1 CLIP vazio/ignorado: [%s, %s)", + ano, + s1_ini_clip, + s1_fim_clip, + ) + + # --- H2 --- + if s2_ini_clip < s2_fim_clip: + logger.info( + "[PNCP][semestral] Ano %s | H2 CLIP válido: [%s, %s) → " + "iniciando coleta paginada", + ano, + s2_ini_clip, + s2_fim_clip, + ) + try: + page_data = self.get_contratacoes_publicacao_paginado( + data_inicial=s2_ini_clip, + data_final=s2_fim_clip, + codigo_modalidade_contratacao=codigo_modalidade_contratacao, + uf=uf, + codigo_municipio_ibge=codigo_municipio_ibge, + cnpj=cnpj, + codigo_unidade_administrativa=codigo_unidade_administrativa, + id_usuario=id_usuario, + pagina_inicial=pagina_inicial, + max_paginas=max_paginas, + ) + n = len(page_data) if page_data else 0 + logger.info( + "[PNCP][semestral] Ano %s | H2 coletado com sucesso | linhas=%s", + ano, + n, + ) + if page_data: + agregados.extend(page_data) + except Exception as e: + logger.error( + "[PNCP][semestral] Ano %s | H2 FALHOU: %s", ano, e, exc_info=True + ) + else: + logger.info( + "[PNCP][semestral] Ano %s | H2 CLIP vazio/ignorado: [%s, %s)", + ano, + s2_ini_clip, + s2_fim_clip, + ) + + logger.info( + "[PNCP][semestral] FIM | anos=%s..%s | total_linhas=%s", + ano_ini, + ano_fim, + len(agregados), + ) + return agregados + + def get_itens_e_resultados( + self, lista_chaves: List[Tuple[str, int, str]] + ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: + """ + Recebe lista de numeroControlePNCP e retorna: + - lista com todos os itens de cada contratação + - lista com todos os resultados dos itens + + Args: + lista_chaves: lista de tuplas (cnpj, ano, sequencial) + + Returns: + Tuple: + - itens (List[Dict]) + - resultados (List[Dict]) + """ + itens_total: List[Dict[str, Any]] = [] + resultados_total: List[Dict[str, Any]] = [] + + for numeroControlePNCP in lista_chaves: + cnpj, digito, sequencial, ano = parse_numero_controle(numeroControlePNCP) + logger.info( + "[PNCP][itens/resultados] Iniciando para CNPJ=%s, ano=%s, seq=%s", + cnpj, + ano, + sequencial, + ) + + # 1) Buscar itens da contratação + endpoint_itens = f"/pncp/v1/orgaos/{cnpj}/compras/{ano}/{sequencial}/itens" + status, data_itens = request_safe( + self, http.HTTPMethod.GET, endpoint_itens, headers=self.BASE_HEADER + ) + + if status == http.HTTPStatus.OK and isinstance(data_itens, list): + for item in data_itens: + item["numeroControlePNCP"] = numeroControlePNCP + itens_total.extend(data_itens) + logger.info("[PNCP][itens] %s itens coletados", len(data_itens)) + else: + logger.warning( + "[PNCP][itens] Falha ao coletar itens | CNPJ=%s, ano=%s, seq=%s | " + "status=%s", + cnpj, + ano, + sequencial, + status, + ) + continue # pula para próxima chave + + # 2) Consultar quantidade de itens + endpoint_qtd = ( + f"/pncp/v1/orgaos/{cnpj}/compras/{ano}/{sequencial}/itens/quantidade" + ) + status, qtd = request_safe( + self, http.HTTPMethod.GET, endpoint_qtd, headers=self.BASE_HEADER + ) + # time.sleep(1) # Sleep after API call to avoid rate limiting + if status != http.HTTPStatus.OK or not isinstance(qtd, int): + logger.warning( + "[PNCP][quantidade] Não foi possível obter quantidade de itens | " + "CNPJ=%s, ano=%s, seq=%s", + cnpj, + ano, + sequencial, + ) + continue + + # Após o narrowing acima, qtd é garantidamente int + qtd_int: int = qtd # type: ignore[unreachable] + + # 3) Para cada item, buscar resultados + if qtd_int > 0: + for numero_item in range(1, qtd_int + 1): + endpoint_res = ( + f"/pncp/v1/orgaos/{cnpj}/compras/{ano}/{sequencial}/" + f"itens/{numero_item}/resultados" + ) + status, data_res = request_safe( + self, http.HTTPMethod.GET, endpoint_res, headers=self.BASE_HEADER + ) + # Sleep after API call to avoid rate limiting (commented out) + # time.sleep(1) + + if status == http.HTTPStatus.OK and isinstance(data_res, list): + # for r in data_res: + # r["numeroControlePNCP"] = numeroControlePNCP + resultados_total.extend(data_res) + logger.info( + "[PNCP][resultados] Item %s → %s resultados", + numero_item, + len(data_res), + ) + else: + logger.warning( + "[PNCP][resultados] Falha no item %s | CNPJ=%s, ano=%s, " + "seq=%s", + numero_item, + cnpj, + ano, + sequencial, + ) + + return itens_total, resultados_total diff --git a/airflow_lappis/plugins/cliente_postgres.py b/airflow_lappis/plugins/cliente_postgres.py new file mode 100755 index 00000000..c30eb36b --- /dev/null +++ b/airflow_lappis/plugins/cliente_postgres.py @@ -0,0 +1,553 @@ +import logging +import re +from contextlib import contextmanager +from typing import Any, Dict, List, Optional, Tuple +import psycopg2 +import psycopg2.extras +from pandas import json_normalize +import pandas as pd +import io + + +class ClientPostgresDB: + """Client for interacting with PostgreSQL database.""" + + SEPARATOR = "__" + TYPE_MAP = {int: "BIGINT", float: "NUMERIC", bool: "BOOLEAN"} + + @staticmethod + def _get_column_type(value: Any) -> str: + return ClientPostgresDB.TYPE_MAP.get(type(value), "TEXT") + + @staticmethod + def _unique_index_name(table_name: str, columns: List[str]) -> str: + raw = f"uq_{table_name}_{'_'.join(columns)}" + return re.sub(r"[^\w]", "_", raw)[:63] + + def _flatten_data(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + return list( + map( + lambda d: { + str(k): v if type(v) is not list else str(v) for k, v in d.items() + }, + json_normalize(data, sep=ClientPostgresDB.SEPARATOR).to_dict( + orient="records" + ), + ) + ) + + def __init__(self, conn_str: str) -> None: + self.conn_str = conn_str + logging.info( + f"[cliente_postgres.py] Initialized ClientPostgresDB with conn_str: " + f"{conn_str}" + ) + + @contextmanager + def _connect(self): + """Context manager that guarantees the connection is closed after use. + + psycopg2's native context manager only handles transactions + (commit/rollback) but does not close the connection. + """ + conn = psycopg2.connect(self.conn_str) + try: + yield conn + finally: + conn.close() + + def create_table_if_not_exists( + self, + sample_data: Dict[str, Any], + table_name: str, + primary_key: Optional[List[str]] = None, + schema: str = "raw", + conn=None, + ) -> None: + def _execute(connection): + with connection.cursor() as cursor: + cursor.execute(f"CREATE SCHEMA IF NOT EXISTS {schema};") + logging.info(f"[cliente_postgres.py] Schema {schema} ensured to exist") + + flattened_sample = self._flatten_data([sample_data])[0] + column_definitions: List[str] = [] + + for column in flattened_sample.keys(): + column_definitions.append(f"{column} TEXT") + + if primary_key: + pk_str = ", ".join(primary_key) + column_definitions.append(f"PRIMARY KEY ({pk_str})") + + create_table_query = ( + f"CREATE TABLE IF NOT EXISTS {schema}.{table_name} (" + f"{', '.join(column_definitions)});" + ) + + try: + cursor.execute(create_table_query) + logging.info( + f"[cliente_postgres.py] Table {schema}.{table_name} created " + f"or already exists" + ) + except psycopg2.Error as err: + logging.error( + f"[cliente_postgres.py] Failed to create table {schema}." + f"{table_name}. Error: {str(err)}" + ) + raise RuntimeError( + f"Failed to create table {schema}.{table_name}" + ) from err + + if conn is not None: + _execute(conn) + else: + with self._connect() as new_conn: + _execute(new_conn) + new_conn.commit() + + def insert_data( + self, + data: List[Dict[str, Any]], + table_name: str, + conflict_fields: Optional[List[str]] = None, + primary_key: Optional[List[str]] = None, + schema: str = "raw", + conn=None, + ) -> None: + if not data: + logging.warning( + f"[cliente_postgres.py] No data to insert into {schema}.{table_name}" + ) + return + + flattened_data = self._flatten_data(data) + columns = list(flattened_data[0].keys()) + column_probe = {col: None for col in columns} + + self.create_table_if_not_exists( + column_probe, table_name, primary_key=primary_key, schema=schema, conn=conn + ) + self.alter_table(column_probe, table_name, schema=schema, conn=conn) + if conflict_fields: + self.ensure_unique_constraint( + schema, table_name, conflict_fields, conn=conn + ) + + values = [tuple(item.get(col) for col in columns) for item in flattened_data] + + sql = f"INSERT INTO {schema}.{table_name} ({', '.join(columns)}) VALUES %s" + + if conflict_fields: + conflict_str = ", ".join(conflict_fields) + update_str = ", ".join([f"{col} = EXCLUDED.{col}" for col in columns]) + sql += f" ON CONFLICT ({conflict_str}) DO UPDATE SET {update_str}" + + def _execute(connection): + with connection.cursor() as cursor: + try: + psycopg2.extras.execute_values(cursor, sql, values) + logging.info( + f"[cliente_postgres.py] Inserted data into {schema}.{table_name}" + ) + except psycopg2.errors.UndefinedColumn as err: + logging.warning( + f"[cliente_postgres.py] Missing column detected in {schema}.{table_name}: " + f"{err}. Tentando alterar tabela e reinserir." + ) + connection.rollback() + column_probe = {col: None for col in columns} + self.alter_table( + column_probe, table_name, schema=schema, conn=connection + ) + psycopg2.extras.execute_values(cursor, sql, values) + logging.info( + f"[cliente_postgres.py] Inserted data into {schema}.{table_name} after alter" + ) + except psycopg2.Error as err: + logging.error( + f"[cliente_postgres.py] Failed to insert data into {schema}." + f"{table_name}. Error: {str(err)}" + ) + raise RuntimeError( + f"Failed to insert data into {schema}.{table_name}" + ) from err + + if conn is not None: + _execute(conn) + else: + with self._connect() as new_conn: + _execute(new_conn) + new_conn.commit() + + def execute_query(self, query: str) -> List[Tuple[Any, ...]]: + logging.info(f"[cliente_postgres.py] Executing query: {query}") + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query) + results = cursor.fetchall() + logging.info( + f"[cliente_postgres.py] Query executed successfully, fetched " + f"{len(results)} rows" + ) + return results + + def get_contratos_ids(self, schema: str = "compras_gov") -> List[int]: + query = f"SELECT id FROM {schema}.contratos" + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query) + return [row[0] for row in cursor.fetchall()] + + def get_id_programas(self) -> List[int]: + query = "SELECT id_programa FROM transfere_gov.programas" + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query) + return [row[0] for row in cursor.fetchall()] + + def get_id_planos_acao(self) -> List[int]: + query = "SELECT id_plano_acao FROM transfere_gov.planos_acao" + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query) + return [row[0] for row in cursor.fetchall()] + + def drop_table_if_exists(self, table_name: str, schema: str = "raw") -> None: + with self._connect() as conn: + with conn.cursor() as cursor: + try: + cursor.execute(f"DROP TABLE IF EXISTS {schema}.{table_name};") + conn.commit() + print(f"Tabela {schema}.{table_name} removida com sucesso.") + except Exception as e: + print(f"Erro ao remover a tabela {schema}.{table_name}: {e}") + + def insert_csv_data( + self, csv_data: str, table_name: str, schema: str = "raw" + ) -> None: + df = pd.read_csv(io.StringIO(csv_data)) + data = df.to_dict(orient="records") + self.drop_table_if_exists(table_name, schema) + self.insert_data(data, table_name, primary_key=None, schema=schema) + + def get_programacao_financeira(self) -> List[Tuple[Any, ...]]: + query = ( + "SELECT tx_numero_programacao, ug_emitente_programacao " + "FROM transfere_gov.programacao_financeira" + ) + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query) + return cursor.fetchall() + + def alter_table( + self, + data: Dict[str, Any], + table_name: str, + schema: str = "raw", + conn=None, + ) -> None: + flattened_data = self._flatten_data([data])[0] + columns = list(flattened_data.keys()) + + def _execute(connection): + with connection.cursor() as cursor: + cursor.execute( + f""" + SELECT column_name + FROM information_schema.columns + WHERE table_schema = '{schema}' + AND table_name = '{table_name}' + """ + ) + existing_columns = [row[0] for row in cursor.fetchall()] + + for column in columns: + if column not in existing_columns: + alter_query = ( + f"ALTER TABLE {schema}.{table_name} " + f"ADD COLUMN IF NOT EXISTS {column} TEXT;" + ) + try: + cursor.execute(alter_query) + logging.info( + f"[cliente_postgres.py] Added column {column} " + f"to {schema}.{table_name}" + ) + except psycopg2.Error as e: + logging.error( + f"[cliente_postgres.py] Failed to add {column} " + f"to {schema}.{table_name}. Error: {str(e)}" + ) + + if conn is not None: + _execute(conn) + else: + with self._connect() as new_conn: + _execute(new_conn) + new_conn.commit() + + logging.info( + f"[cliente_postgres.py] Table {schema}.{table_name} altered successfully" + ) + + def get_nota_credito(self) -> List[Tuple[Any, ...]]: + query = ( + "SELECT cd_ug_emitente_nota, cd_gestao_emitente_nota, tx_numero_nota " + "FROM transfere_gov.notas_de_credito" + ) + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query) + return cursor.fetchall() + + def remove_duplicates( + self, table_name: str, column_mapping: Dict[int, str], schema: str = "siafi" + ) -> None: + columns = ", ".join(column_mapping.values()) + delete_query = f""" + DELETE FROM {schema}.{table_name} + WHERE ctid NOT IN ( + SELECT MIN(ctid) + FROM {schema}.{table_name} + GROUP BY {columns} + ); + """ + vacuum_query = f"VACUUM {schema}.{table_name};" + + try: + logging.info( + f"Executando query para remover duplicados em {schema}.{table_name}" + ) + + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(delete_query) + conn.commit() + logging.info( + f"Duplicados removidos com sucesso de {schema}.{table_name}" + ) + except Exception as e: + logging.error( + f"Erro ao remover duplicados de {schema}.{table_name}: {str(e)}" + ) + raise + + conn = None + try: + conn = psycopg2.connect(self.conn_str) + conn.autocommit = True + with conn.cursor() as cursor: + cursor.execute(vacuum_query) + logging.info(f"VACUUM executado com sucesso em {schema}.{table_name}") + except Exception as e: + logging.warning( + "Falha ao executar VACUUM em %s.%s (deduplicacao concluida): %s", + schema, + table_name, + str(e), + ) + finally: + if conn: + conn.close() + + def get_codigo_unidade(self) -> list[dict]: + query = """ + SELECT codigounidade, ordem_grandeza + FROM pessoas.unidade_organizacional + """ + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query) + rows = cursor.fetchall() + return [ + {"codigounidade": int(row[0]), "ordem_grandeza": int(row[1])} + for row in rows + ] + + def ensure_unique_constraint( + self, + schema: str, + table_name: str, + columns: List[str], + conn=None, + ) -> None: + """ + Garante índice UNIQUE para ON CONFLICT quando a tabela já existia + sem a PK composta correta (CREATE TABLE IF NOT EXISTS não altera constraints). + """ + if not columns: + return + + index_name = self._unique_index_name(table_name, columns) + cols_sql = ", ".join(columns) + query = ( + f"CREATE UNIQUE INDEX IF NOT EXISTS {index_name} " + f"ON {schema}.{table_name} ({cols_sql});" + ) + + def _execute(connection): + with connection.cursor() as cursor: + try: + cursor.execute(query) + logging.info( + "[cliente_postgres.py] Unique index %s on %s.%s (%s)", + index_name, + schema, + table_name, + cols_sql, + ) + except psycopg2.Error as err: + logging.error( + "[cliente_postgres.py] Failed to create unique index on " + "%s.%s: %s", + schema, + table_name, + err, + ) + raise RuntimeError( + f"Failed to ensure unique constraint on {schema}.{table_name}" + ) from err + + if conn is not None: + _execute(conn) + else: + with self._connect() as new_conn: + _execute(new_conn) + new_conn.commit() + + def execute_non_query(self, query: str) -> None: + logging.info(f"[cliente_postgres.py] Executando non-query: {query}") + with self._connect() as conn: + with conn.cursor() as cursor: + try: + cursor.execute(query) + conn.commit() + logging.info("[cliente_postgres.py] Non-query executado com sucesso") + except psycopg2.Error as e: + logging.error( + f"[cliente_postgres.py] Erro ao executar non-query. Erro: {e}" + ) + raise RuntimeError("Erro ao executar comando SQL sem retorno") from e + + def apply_comments( + self, + schema: str, + table_name: str, + table_comment: str | None = None, + column_comments: dict[str, str] | None = None, + ) -> None: + """Aplica COMMENT ON TABLE e COMMENT ON COLUMN no PostgreSQL.""" + queries: list[str] = [] + tabela = f"{schema}.{table_name}" + + if table_comment: + desc = table_comment.replace("'", "''") + queries.append(f"COMMENT ON TABLE {tabela} IS '{desc}';") + + for coluna, descricao in (column_comments or {}).items(): + if not descricao: + continue + desc = descricao.replace("'", "''") + queries.append(f"COMMENT ON COLUMN {tabela}.{coluna} IS '{desc}';") + + for query in queries: + self.execute_non_query(query) + + def get_dashboard_kpis(self) -> Dict[str, int]: + query = "SELECT kpi, valor FROM pessoas.kpis_servidores" + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query) + return {row[0]: row[1] for row in cursor.fetchall()} + + def get_dashboard_genero(self) -> Dict[str, float]: + query = """ + SELECT + genero, + ROUND(percentual_distribuicao * 100, 1) as percentual + FROM pessoas.distribuicao_genero + """ + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query) + genero_data = {} + for row in cursor.fetchall(): + genero = row[0].lower() if row[0] else "n/a" + genero_data[f"{genero}_percent"] = float(row[1]) + return genero_data + + def get_dashboard_raca_cor(self) -> List[Dict[str, Any]]: + query = """ + SELECT + COALESCE(cor_raca, 'NÃO DECLARADA') as nome_cor, + quantidade_servidores as valor + FROM pessoas.distribuicao_raca_cor + ORDER BY quantidade_servidores DESC + """ + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query) + return [ + {"nome_cor": row[0], "valor": row[1]} for row in cursor.fetchall() + ] + + def get_dashboard_situacao_funcional(self) -> List[Dict[str, Any]]: + query = """ + SELECT + situacao_funcional_original as label, + quantidade_servidores as valor + FROM pessoas.distribuicao_situacao_funcional + ORDER BY quantidade_servidores DESC + """ + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query) + return [{"label": row[0], "valor": row[1]} for row in cursor.fetchall()] + + def get_dashboard_mapa_uf(self) -> Dict[str, Dict[str, Any]]: + query = """ + SELECT + sigla_uf, + nome_uf, + valor, + percentual + FROM pessoas.distribuicao_mapa_uf + ORDER BY sigla_uf + """ + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query) + return { + row[0]: {"nome": row[1], "valor": row[2], "percentual": row[3]} + for row in cursor.fetchall() + } + + def get_dashboard_tabela_servidores(self, limit: int = 100) -> List[Dict[str, Any]]: + query = """ + SELECT + cargo, + genero, + situacao, + cidade, + estado, + total + FROM pessoas.tabela_servidores_agregada + ORDER BY total DESC + LIMIT %s + """ + with self._connect() as conn: + with conn.cursor() as cursor: + cursor.execute(query, (limit,)) + return [ + { + "cargo": row[0], + "genero": row[1], + "situacao": row[2], + "cidade": row[3], + "estado": row[4], + "total": row[5], + } + for row in cursor.fetchall() + ] diff --git a/airflow_lappis/plugins/cliente_senadores.py b/airflow_lappis/plugins/cliente_senadores.py new file mode 100644 index 00000000..31d473a8 --- /dev/null +++ b/airflow_lappis/plugins/cliente_senadores.py @@ -0,0 +1,170 @@ +import http +import logging +from typing import Any +from cliente_base import ClienteBase + + +class ClienteSenadores(ClienteBase): + """ + Cliente para consumir a API de Dados Abertos do Senado Federal. + """ + + BASE_URL = "https://legis.senado.leg.br/dadosabertos" + BASE_HEADER = {"accept": "application/json"} + + def __init__(self) -> None: + super().__init__(base_url=ClienteSenadores.BASE_URL) + logging.info( + "[cliente_senadores.py] Initialized ClienteSenadores em: " + f"{ClienteSenadores.BASE_URL}" + ) + + def get_senadores_por_legislatura(self) -> list: + """ + Obtém a lista de senadores ativose inativos. + O Senado geralmente retorna tudo em uma única chamada, sem paginação complexa como a Câmara. + """ + endpoint = "/senador/lista/legislatura/0/100" + logging.info("[cliente_senadores.py] Fetching senadores atuais") + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + + if status == http.HTTPStatus.OK and isinstance(data, dict): + # A estrutura do JSON do Senado é: ListaParlamentarLegislatura -> Parlamentares -> Parlamentar + try: + lista_root = data.get("ListaParlamentarLegislatura", {}) + parlamentares = lista_root.get("Parlamentares", {}).get("Parlamentar", []) + + if isinstance(parlamentares, dict): + parlamentares = [parlamentares] + + logging.info( + "[cliente_senadores.py] Successfully fetched " + f"{len(parlamentares)} senadores" + ) + return parlamentares + except Exception as e: + logging.error( + f"[cliente_senadores.py] Erro ao parsear JSON do Senado: {e}" + ) + return [] + else: + logging.warning(f"[cliente_senadores.py] Failed with status: {status}") + return [] + + def get_periodo_legislacao(self) -> list: + """ + Obtém o período de legislação parlamentar + """ + endpoint = "/dados/ListaLegislatura.json" + logging.info("[cliente_senadores.py] Fetching periodos legislação") + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + + if status == http.HTTPStatus.OK and isinstance(data, dict): + # A estrutura do JSON do Senado é: ListaLegislatura -> Parlamentares -> Parlamentar + try: + lista_root = data.get("ListaLegislatura", {}) + legislaturas = lista_root.get("Legislaturas", {}).get("Legislatura", []) + + if isinstance(legislaturas, dict): + legislaturas = [legislaturas] + + logging.info( + f"[cliente_senadores.py] Successfully fetched {len(legislaturas)} legislaturas" + ) + return legislaturas + except Exception as e: + logging.error( + f"[cliente_senadores.py] Erro ao parsear JSON do Senado: {e}" + ) + return [] + else: + logging.warning(f"[cliente_senadores.py] Failed with status: {status}") + return [] + + def get_filiacoes_senador(self, senador_id: int | str) -> list[dict[str, Any]] | None: + """Obtém o histórico de filiações de um senador.""" + endpoint = f"/senador/{senador_id}/filiacoes" + logging.info( + f"[cliente_senadores.py] Fetching filiacoes for senador_id={senador_id}" + ) + + status, data = self.request( + http.HTTPMethod.GET, + endpoint, + headers=self.BASE_HEADER, + params={"v": 5}, + ) + + if status == http.HTTPStatus.OK and isinstance(data, dict): + try: + root = data.get("FiliacaoParlamentar", data.get("ListaFiliacoesParlamentar", {})) + parlamentar = root.get("Parlamentar") + if isinstance(parlamentar, dict): + root = parlamentar + + filiacao = root.get("Filiacoes", {}).get("Filiacao", []) + + if isinstance(filiacao, dict): + return [filiacao] + if isinstance(filiacao, list): + if filiacao: + return filiacao + logging.warning( + "[cliente_senadores.py] Filiacoes vazias para " + f"senador_id={senador_id}" + ) + return None + except Exception as e: + logging.error( + "[cliente_senadores.py] Erro ao parsear filiacoes do senador " + f"{senador_id}: {e}" + ) + + logging.warning( + "[cliente_senadores.py] Failed to fetch filiacoes for " + f"senador_id={senador_id} with status: {status}" + ) + return None + + def get_senadores_atuais(self) -> list: + """ + Obtém a lista de senadores em exercício. + O Senado geralmente retorna tudo em uma única chamada, + sem paginação complexa como a Câmara. + """ + endpoint = "/senador/lista/atual" + logging.info("[cliente_senadores.py] Fetching senadores atuais") + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + + if status == http.HTTPStatus.OK and isinstance(data, dict): + # Estrutura esperada: ListaParlamentarEmExercicio -> + # Parlamentares -> Parlamentar. + try: + lista_root = data.get("ListaParlamentarEmExercicio", {}) + parlamentares = lista_root.get("Parlamentares", {}).get("Parlamentar", []) + + if isinstance(parlamentares, dict): + parlamentares = [parlamentares] + + logging.info( + "[cliente_senadores.py] Successfully fetched " + f"{len(parlamentares)} senadores" + ) + return parlamentares + except Exception as e: + logging.error( + f"[cliente_senadores.py] Erro ao parsear JSON do Senado: {e}" + ) + return [] + else: + logging.warning(f"[cliente_senadores.py] Failed with status: {status}") + return [] diff --git a/airflow_lappis/plugins/cliente_siafi.py b/airflow_lappis/plugins/cliente_siafi.py new file mode 100644 index 00000000..b8acd5b1 --- /dev/null +++ b/airflow_lappis/plugins/cliente_siafi.py @@ -0,0 +1,322 @@ +import os +import logging +from zeep import Client +from zeep.transports import Transport +from zeep.wsse.username import UsernameToken +from requests import Session +from typing import Dict, Any, Optional +from retry_helpers import retry_on_exception +import requests +import base64 + +# Configuração do logger +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +class ClienteSiafi: + def __init__(self) -> None: + """ + Inicializa o cliente SIAFI com as configurações necessárias. + """ + self.base_url = "https://servicos-siafi.tesouro.gov.br/siafi" + self.cert_path = os.getenv("SIAFI_CERT") + self.key_path = os.getenv("SIAFI_KEY") + self.siafi_username = os.getenv("SIAFI_USERNAME") + self.siafi_password = os.getenv("SIAFI_PASSWORD") + + def _criar_cliente_soap(self, ano: int, endpoint: str) -> Optional[Client]: + """ + Cria e retorna um cliente SOAP para comunicação com o serviço SIAFI + com a URL específica para o ano e endpoint informados. + + Args: + ano (int): Ano para formar a URL do WSDL + endpoint (str): Endpoint específico do serviço SIAFI + + Returns: + Client: Cliente SOAP configurado ou None em caso de falha. + """ + wsdl_url = f"{self.base_url}{ano}/{endpoint}?wsdl" + logger.info(f"Criando cliente SOAP com URL: {wsdl_url}") + + if not isinstance(self.cert_path, str) or not isinstance(self.key_path, str): + logger.error( + f"Certificados inválidos. cert={self.cert_path}, key={self.key_path}" + ) + return None + + session = Session() + session.verify = True # Verificar certificados SSL + session.cert = (self.cert_path, self.key_path) + + transport = Transport(session=session) + wsse = UsernameToken(self.siafi_username, self.siafi_password, use_digest=False) + + try: + client = Client(wsdl_url, transport=transport, wsse=wsse) + logger.info( + f"Cliente SOAP para o ano {ano} e endpoint {endpoint} criado com sucesso." + ) + return client + except requests.exceptions.SSLError as ssl_error: + logger.error( + f"Erro de SSL ao criar cliente SOAP. Verifique os certificados. " + f"cert_path={self.cert_path}, key_path={self.key_path}, erro={ssl_error}" + ) + except requests.exceptions.ConnectionError as conn_error: + logger.error( + f"Erro ao criar cliente SOAP. wsdl_url={wsdl_url}, erro={conn_error}" + ) + except Exception as e: + logger.error( + f"Erro ao criar cliente SOAP para ano {ano} e endpoint {endpoint}. " + f"wsdl_url={wsdl_url}, erro={e}" + ) + return None + + @retry_on_exception(max_attempts=3, initial_delay=2.0, backoff_factor=2.0) + def consultar_programacao_financeira( + self, ug_emitente: str, ano: int, num_lista: str + ) -> Optional[Dict[str, Any]]: + """ + Consulta programações financeiras no SIAFI. + + Args: + ug_emitente (str): UG emitente da programação financeira. + ano (int): Ano da programação financeira. + num_lista (str): Número do documento da programação financeira. + + Returns: + dict: Resposta da consulta ou None em caso de falha. + """ + endpoint = "services/pf/manterProgramacaoFinanceira" + + client = self._criar_cliente_soap(ano, endpoint) + if not client: + logger.error( + f"Falha ao criar cliente SOAP para consultar programação financeira. " + f"ug={ug_emitente}, ano={ano}, num_lista={num_lista}, endpoint={endpoint}" + ) + return None + + soap_headers = { + "cabecalhoSIAFI": { + "nomeSistemaSIAFI": f"SIAFI{str(ano)}", + "ug": ug_emitente, + "bilhetador": {"nonce": "nonce123456"}, + } + } + + try: + logger.info( + f"Consultando PF: UG={ug_emitente}, Ano={ano}, Documento={num_lista}" + ) + response = client.service.pfDetalharProgramacaoFinanceira( + _soapheaders=soap_headers, + ano=ano, + numeroDocumento=num_lista, + codUgEmit=ug_emitente, + ) + logger.info(f"Resposta recebida: {response}") + + response_dict: Dict[str, Any] = dict(response) if response else {} + return response_dict + except requests.exceptions.Timeout as timeout_error: + logger.error( + f"Timeout ao consultar programação financeira. " + f"ug={ug_emitente}, ano={ano}, num_list={num_lista}, erro={timeout_error}" + ) + except Exception as e: + logger.error( + f"Erro inesperado ao consultar programação financeira. " + f"ug_emitente={ug_emitente}, ano={ano}, num_lista={num_lista}, erro={e}" + ) + return None + + @retry_on_exception(max_attempts=3, initial_delay=2.0, backoff_factor=2.0) + def consultar_nota_empenho( + self, ug_emitente: str, ano_empenho: int, num_empenho: str + ) -> Optional[Dict[str, Any]]: + """ + Consulta detalhes de uma Nota de Empenho no SIAFI. + + Args: + ug_emitente (str): UG emitente da Nota de Empenho. + ano_empenho (int): Ano da Nota de Empenho. + num_empenho (str): Número da Nota de Empenho. + + Returns: + dict: Resposta da consulta ou None em caso de falha. + """ + endpoint = "services/orcamentario/manterOrcamentario" + client = self._criar_cliente_soap(ano_empenho, endpoint) + if not client: + logger.error( + f"Falha ao criar cliente SOAP para consultar nota de empenho. " + f"ug={ug_emitente}, ano={ano_empenho}, " + f"numero={num_empenho}, endpoint={endpoint}" + ) + return None + + soap_headers = { + "cabecalhoSIAFI": { + "nomeSistemaSIAFI": f"SIAFI{str(ano_empenho)}", + "ug": ug_emitente, + "bilhetador": {"nonce": "nonce123456"}, + } + } + + parametros_consulta = { + "ugEmitente": ug_emitente, + "anoEmpenho": ano_empenho, + "numEmpenho": num_empenho.zfill(6), + } + + try: + logger.info( + f"Consultando NE: UG={ug_emitente}, Ano={ano_empenho}, Num={num_empenho}" + ) + response = client.service.orcDetalharEmpenho( + parametros_consulta, _soapheaders=soap_headers + ) + logger.info(f"Resposta recebida: {response}") + + response_dict: Dict[str, Any] = dict(response) if response else {} + return response_dict + except requests.exceptions.Timeout as te: + logger.error( + f"Timeout ao consultar nota de empenho. " + f"ug={ug_emitente}, ano={ano_empenho}, num={num_empenho}, erro={te}" + ) + except Exception as e: + logger.error( + f"Erro inesperado ao consultar nota de empenho. " + f"ug={ug_emitente}, ano={ano_empenho}, num={num_empenho}, erro={e}" + ) + return None + + @retry_on_exception(max_attempts=3, initial_delay=2.0, backoff_factor=2.0) + def get_access_token(self) -> Optional[str]: + """ + Obtém um token de acesso usando autenticação HTTP Basic. + + Returns: + str: Token de acesso ou None em caso de falha. + """ + # Credenciais + consumer_key = os.getenv("SIAFI_BEARER_KEY_SERPRO") + consumer_secret = os.getenv("SIAFI_BEARER_SECRET_SERPRO") + + if not consumer_key or not consumer_secret: + logger.error("Credenciais de autenticação não configuradas.") + return None + + # Codificar as credenciais em Base64 + credentials = f"{consumer_key}:{consumer_secret}" + encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode( + "utf-8" + ) + + # URL para obter o token + url = "https://gateway.apiserpro.serpro.gov.br/token" + + # Parâmetros de dados + data = {"grant_type": "client_credentials"} + + # Cabeçalhos com a autorização codificada em Base64 + headers = { + "Authorization": f"Basic {encoded_credentials}", + "Content-Type": "application/x-www-form-urlencoded", + } + + try: + # Realizar a requisição POST para obter o token + response = requests.post(url, data=data, headers=headers) + + if response.status_code == 200: + token = response.json().get("access_token") + if isinstance(token, str): + logger.info("Token obtido com sucesso.") + return token + else: + logger.error("Resposta não contém o campo 'access_token'.") + return None + else: + logger.error( + f"Erro ao obter o token: {response.status_code}, {response.text}" + ) + return None + except Exception as e: + logger.error(f"Erro na requisição para obter o token: {e}") + return None + + @retry_on_exception(max_attempts=3, initial_delay=2.0, backoff_factor=2.0) + def consultar_nota_credito( + self, ug: str, gestao: str, ano: str, numero: str + ) -> Optional[Dict[str, Any]]: + """ + Consulta a nota de crédito na API SIAFI. + + Args: + ug (str): Unidade Gestora. + gestao (str): Código da gestão. + ano (str): Ano da nota. + numero (str): Número da nota. + + Returns: + Optional[dict]: JSON da resposta ou None em caso de erro. + """ + + cpf = os.getenv("SIAFI_CPF_SERPRO") + ug_orgao = "113601" + base_credential = f"{cpf}.{ug_orgao}.SIAFI" + # Obter o x-credencial baseado no ano + x_credencial = f"{base_credential}{ano}" + encoded_x_credencial = base64.b64encode(x_credencial.encode("utf-8")).decode( + "utf-8" + ) + + # URL da requisição + BASE_URL = "https://gateway.apiserpro.serpro.gov.br/api-integra-siafi/api" + url = f"{BASE_URL}/v2/nota-credito/{ug}/{gestao}/{ano}/{numero}" + + # Obtém o token de acesso + token = self.get_access_token() + if not token: + logger.error("Não foi possível obter o token de acesso.") + return {"error": "Falha ao obter o token de acesso."} + + # Configura os cabeçalhos da requisição + headers = { + "accept": "application/json", + "x-credencial": encoded_x_credencial, + "Authorization": f"Bearer {token}", + } + + try: + # Faz a requisição GET + logger.info( + f"Consultando NC: UG={ug} Gestão={gestao} Ano={ano} Número={numero}" + ) + response = requests.get(url, headers=headers) + + # Verifica o status da resposta + if response.status_code == 200: + logger.info("Consulta realizada com sucesso.") + response_json = response.json() + if isinstance(response_json, dict): + return response_json + else: + logger.error("Resposta não é um JSON válido.") + return None + else: + logger.error( + f"Erro na consulta: {response.status_code} - {response.text}" + ) + return None + except Exception as e: + logger.error(f"Erro ao consultar a nota de crédito: {e}") + return None diff --git a/airflow_lappis/plugins/cliente_siape.py b/airflow_lappis/plugins/cliente_siape.py new file mode 100755 index 00000000..a99deb8d --- /dev/null +++ b/airflow_lappis/plugins/cliente_siape.py @@ -0,0 +1,341 @@ +import os +import logging +from typing import Dict, Any +import requests +import xml.etree.ElementTree as ET +from jinja2 import Environment, FileSystemLoader +from dados_funcionais_handler import DadosFuncionaisHandler + + +class ClienteSiape: + """ + Client to consume the SIAPE SOAP API using OAuth2 authentication + and dynamic XML generation with Jinja2 templates. + """ + + BEARER_ENDPOINT = ( + "https://apigateway.conectagov.estaleiro.serpro.gov.br/oauth2/jwt-token/" + ) + SOAP_ENDPOINT = "https://apigateway.conectagov.estaleiro.serpro.gov.br/api-consulta-siape/v1/consulta-siape" + + def __init__(self) -> None: + """ + Initialize the SIAPE client using environment variables: + - SIAPE_BEARER_USER + - SIAPE_BEARER_PASSWORD + - SIAPE_CPF_USER + """ + self.oauth_user = os.getenv("SIAPE_BEARER_USER") + self.oauth_password = os.getenv("SIAPE_BEARER_PASSWORD") + self.cpf_usuario = os.getenv("SIAPE_CPF_USER") + + if not all([self.oauth_user, self.oauth_password, self.cpf_usuario]): + raise ValueError("Variáveis de ambiente do SIAPE estão incompletas") + + token = self._get_token(self.oauth_user, self.oauth_password) + self.headers = self._get_headers(token, self.cpf_usuario) + base_path = os.environ["AIRFLOW_REPO_BASE"] + templates_path = f"{base_path}/templates/siape" + self.env = Environment(loader=FileSystemLoader(templates_path)) + + @staticmethod + def _get_token(oauth_username: str, oauth_password: str) -> str: + """ + Gets the token for the client. + + Args: + oauth_username (str): OAuth username. + oauth_password (str): OAuth password. + + Returns: + str: Access token. + """ + data = {"grant_type": "client_credentials"} + response = requests.post( + ClienteSiape.BEARER_ENDPOINT, + auth=(oauth_username, oauth_password), + data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + json_response: dict[str, Any] = response.json() + return str(json_response["access_token"]) + + @staticmethod + def _get_headers(token: str, cpf_usuario: str) -> Dict[str, str]: + """ + Builds the headers for the client. + + Args: + token (str): The OAuth token. + + Returns: + Dict[str, str]: The headers. + """ + return { + "Authorization": f"Bearer {token}", + "x-cpf-usuario": cpf_usuario, + "Content-Type": "application/xml", + } + + def render_xml(self, template_name: str, context: Dict[str, str]) -> str: + """ + Render XML from a Jinja2 template and context. + + Args: + template_name (str): Template filename + (e.g. 'consultaDadosFuncionais.xml.j2'). + context (Dict[str, str]): Data to inject into the template. + + Returns: + str: Rendered XML string. + """ + template = self.env.get_template(template_name) + rendered_xml: str = template.render(context) + return rendered_xml + + def enviar_soap(self, xml: str) -> str: + """ + Send the XML payload to the SIAPE SOAP endpoint. + + Args: + xml (str): The complete XML request. + + Returns: + str: The raw XML response. + """ + response = requests.post( + ClienteSiape.SOAP_ENDPOINT, headers=self.headers, data=xml + ) + response.raise_for_status() + response_text: str = response.text + return response_text + + def call(self, template_name: str, context: Dict[str, str]) -> str: + """ + Execute a SOAP request using a Jinja2 template and parameters. + + Args: + template_name (str): Jinja2 template file name. + context (Dict[str, str]): Parameters for rendering the XML. + + Returns: + str: The raw XML response. + """ + xml: str = self.render_xml(template_name, context) + soap_response: str = self.enviar_soap(xml) + return soap_response + + @staticmethod + def parse_xml_to_dict(xml_string: str) -> Dict[str, str]: + """ + Parse a SOAP XML response and return a dictionary with tag names and values. + + Args: + xml_string (str): SOAP XML response. + + Returns: + Dict[str, str]: Flattened dictionary of XML data. + """ + ns = {"soapenv": "http://schemas.xmlsoap.org/soap/envelope/"} + root = ET.fromstring(xml_string) + body = root.find("soapenv:Body", ns) + if body is None: + return {"error": "Missing SOAP Body"} + + response_elem = list(body)[0] + return { + child.tag.split("}")[-1]: child.text.strip() + for child in response_elem.iter() + if child.text and child.text.strip() + } + + @staticmethod + def parse_xml_to_list( + xml_string: str, element_tag: str, namespaces: Dict[str, str] + ) -> list[dict[str, str | None]]: + """ + Generic parser for repeating XML elements (like lista servidores). + + Args: + xml_string (str): SOAP XML response. + element_tag (str): Tag do elemento que se repete. + namespaces (Dict[str, str]): XML namespaces. + + Returns: + list[dict[str, str | None]]: Lista de registros. + """ + root = ET.fromstring(xml_string) + body = root.find("soapenv:Body", namespaces) + if body is None: + return [] + + response_elem = list(body)[0] + items = response_elem.findall(f".//{element_tag}", namespaces) + + resultado = [] + for item in items: + row = {} + for elem in item: + tag = elem.tag.split("}")[-1] + row[tag] = elem.text.strip() if elem.text else None + resultado.append(row) + + return resultado + + @staticmethod + def parse_afastamento_historico(xml_string: str) -> list[dict[str, Any]]: + """ + Custom parser for afastamento histórico: extrai DadosFerias e DadosOcorrencias. + + Args: + xml_string (str): SOAP XML response. + + Returns: + list[dict[str, str | None]]: Lista de registros combinando + férias e ocorrências. + """ + ns = { + "soapenv": "http://schemas.xmlsoap.org/soap/envelope/", + "ns2": "http://tipo.servico.wssiapenet", + } + root = ET.fromstring(xml_string) + body = root.find("soapenv:Body", ns) + if body is None: + return [] + + dados = [] + for item in body.findall(".//ns2:DadosFerias", ns): + registro = {} + for elem in item: + tag = elem.tag.split("}")[-1] + registro[tag] = elem.text.strip() if elem.text else None + dados.append(registro) + + for item in body.findall(".//ns2:DadosOcorrencias", ns): + registro = {} + for elem in item: + tag = elem.tag.split("}")[-1] + registro[tag] = elem.text.strip() if elem.text else None + dados.append(registro) + + return dados + + @staticmethod + def parse_dependentes(xml_string: str) -> list[dict[str, Any]]: + """ + Custom parser para consultaDadosDependentes: extrai dados do + dependente e seus benefícios. + + Args: + xml_string (str): SOAP XML response. + + Returns: + list[dict[str, Any]]: Lista de registros normalizados de dependentes. + """ + ns = { + "soapenv": "http://schemas.xmlsoap.org/soap/envelope/", + "ns2": "http://tipo.servico.wssiapenet", + } + root = ET.fromstring(xml_string) + body = root.find("soapenv:Body", ns) + if body is None: + return [] + + resultado = [] + for item in body.findall(".//ns2:DadosDependentes", ns): + base_info: dict[str, Any] = {} + beneficios: list[dict[str, str]] = [] + + for elem in item: + tag = elem.tag.split("}")[-1] + if tag == "arrayBeneficios": + for b in elem: + beneficio = { + e.tag.split("}")[-1]: e.text.strip() for e in b if e.text + } + beneficios.append(beneficio) + else: + base_info[tag] = elem.text.strip() if elem.text else None + + if not beneficios: + resultado.append(base_info) + else: + for beneficio in beneficios: + row = base_info.copy() + row.update(beneficio) + resultado.append(row) + + return resultado + + @staticmethod + def parse_pensoes_instituidas(xml_string: str) -> list[dict[str, Any]]: + """ + Custom parser para consultaPensoesInstituidas: extrai dados do + ArrayPensoesInstituidas. + + Args: + xml_string (str): SOAP XML response. + + Returns: + list[dict[str, str | None]]: Lista de registros de pensões instituídas. + """ + ns = { + "soapenv": "http://schemas.xmlsoap.org/soap/envelope/", + "ns1": "http://tipo.servico.wssiapenet", + } + root = ET.fromstring(xml_string) + body = root.find("soapenv:Body", ns) + if body is None: + return [] + + resultado = [] + + # Busca por PensoesInstituidas dentro da estrutura ArrayPensoesInstituidas + pensoes_items = body.findall(".//ns1:PensoesInstituidas", ns) + + for item in pensoes_items: + registro = {} + for elem in item: + tag = elem.tag.split("}")[-1] + # Pula elementos complexos como arrayFichaFinanceira + if tag != "arrayFichaFinanceira": + registro[tag] = elem.text.strip() if elem.text else None + resultado.append(registro) + + return resultado + + @staticmethod + def parse_dado_funcional(xml_string: str) -> Dict[str, str | None]: + """ + Custom parser para consultaDadosFuncionais: extrai múltiplos DadosFuncionais + e retorna apenas o registro ativo (sem dataOcorrExclusao). + + Args: + xml_string (str): SOAP XML response contendo múltiplos DadosFuncionais. + + Returns: + Dict[str, str]: Registro funcional ativo (mais atual). + """ + try: + handler = DadosFuncionaisHandler() + + # Extrai elementos DadosFuncionais + dados_funcionais_items = handler.extract_dados_funcionais_elements(xml_string) + + if not dados_funcionais_items: + logging.warning("Nenhum elemento DadosFuncionais encontrado") + return {} + + # Converte elementos para registros + registros = handler.convert_elements_to_registros(dados_funcionais_items) + logging.info(f"Encontrados {len(registros)} registros funcionais") + + # Seleciona o melhor registro + return handler.select_best_registro(registros) + + except ET.ParseError as e: + logging.error(f"Erro ao fazer parse do XML: {e}") + return {} + except Exception as e: + logging.error(f"Erro inesperado no parse_dado_funcional: {e}") + return {} diff --git a/airflow_lappis/plugins/cliente_siconv.py b/airflow_lappis/plugins/cliente_siconv.py new file mode 100644 index 00000000..56b672bd --- /dev/null +++ b/airflow_lappis/plugins/cliente_siconv.py @@ -0,0 +1,40 @@ +import zipfile +import csv +import io +import logging +import requests + +class ClienteSiconv: + URL_ZIP = "https://repositorio.dados.gov.br/seges/detru/siconv.zip" + ZIP_PATH = "/tmp/siconv.zip" + + def baixar_zip(self) -> None: + logging.info("[cliente_siconv.py] Baixando arquivo SICONV...") + response = requests.get(self.URL_ZIP, stream=True) + response.raise_for_status() + with open(self.ZIP_PATH, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + logging.info("[cliente_siconv.py] Download concluído") + + def ler_csv(self, nome_csv: str, skip_rows: int = 0, colunas_esperadas: list = None): + logging.info(f"[cliente_siconv.py] Lendo {nome_csv} em modo streaming...") + with zipfile.ZipFile(self.ZIP_PATH, "r") as z: + with z.open(nome_csv) as f: + conteudo = io.TextIOWrapper(f, encoding="utf-8-sig") + reader = csv.DictReader(conteudo, delimiter=";") + + if colunas_esperadas: + colunas_csv = reader.fieldnames or [] + faltando = [c for c in colunas_esperadas if c not in colunas_csv] + if faltando: + raise ValueError(f"[cliente_siconv.py] Colunas faltando em {nome_csv}: {faltando}") + + for i, row in enumerate(reader): + if i < skip_rows: + continue + + if colunas_esperadas: + yield {k.lower(): row[k] for k in colunas_esperadas} + else: + yield {k.lower(): v for k, v in row.items() if k is not None} \ No newline at end of file diff --git a/airflow_lappis/plugins/cliente_siorg.py b/airflow_lappis/plugins/cliente_siorg.py new file mode 100755 index 00000000..dfa36e25 --- /dev/null +++ b/airflow_lappis/plugins/cliente_siorg.py @@ -0,0 +1,90 @@ +import http +from typing import Optional + +from cliente_base import ClienteBase + + +class ClienteSiorg(ClienteBase): + + BASE_URL = "https://estruturaorganizacional.dados.gov.br/doc" + + def __init__(self) -> None: + super().__init__(base_url=ClienteSiorg.BASE_URL) + + def get_estrutura_organizacional_resumida( + self, + codigo_poder: Optional[str] = None, + codigo_esfera: Optional[str] = None, + codigo_unidade: Optional[str] = None, + ) -> Optional[list]: + """ + Consultar Estrutura Organizacional Resumida. + + Args: + codigo_poder (Optional[str]): código do poder + codigo_esfera (Optional[str]): código da esfera + codigo_unidade (Optional[str]): código da unidade + + Returns: + dict: Estrutura Organizacional Resumida. + """ + endpoint = "/estrutura-organizacional/resumida" + params = {} + if codigo_poder: + params["codigoPoder"] = codigo_poder + if codigo_esfera: + params["codigoEsfera"] = codigo_esfera + if codigo_unidade: + params["codigoUnidade"] = codigo_unidade + + status, data = self.request(http.HTTPMethod.GET, endpoint, params=params) + return ( + data.get("unidades", []) + if status == http.HTTPStatus.OK and type(data) is dict + else None + ) + + def get_estrutura_organizacional_cargos( + self, + codigo_unidade: Optional[str] = None, + ) -> Optional[dict]: + """ + Consultar Estrutura Organizacional Cargos. + + Args: + codigo_unidade (Optional[str]): código da unidade + + Returns: + dict: Estrutura Organizacional Cargos. + """ + endpoint = "/instancias/consulta-unidade" + params = {} + if codigo_unidade: + params["codigoUnidade"] = codigo_unidade + + headers = {"accept": "*/*"} + + status, data = self.request( + http.HTTPMethod.GET, endpoint, params=params, headers=headers + ) + return ( + data.get("unidade", []) + if status == http.HTTPStatus.OK and type(data) is dict + else None + ) + + def get_cargos_funcao(self) -> Optional[dict]: + """ + Consultar Cargos Função. + + Returns: + dict: Cargos Função. + """ + endpoint = "/cargo-funcao" + + status, data = self.request(http.HTTPMethod.GET, endpoint) + return ( + data.get("tipoCargoFuncao", []) + if status == http.HTTPStatus.OK and type(data) is dict + else None + ) diff --git a/airflow_lappis/plugins/cliente_sqlserver.py b/airflow_lappis/plugins/cliente_sqlserver.py new file mode 100644 index 00000000..08778bad --- /dev/null +++ b/airflow_lappis/plugins/cliente_sqlserver.py @@ -0,0 +1,90 @@ +import logging +import re +import math +from datetime import date, datetime, time +from typing import Any, Dict, List, Tuple +from airflow.providers.microsoft.mssql.hooks.mssql import MsSqlHook + + +class ClientSQLServerDB: + """Client for interacting with SQL Server using Airflow's MsSqlHook.""" + + def __init__(self, mssql_conn_id: str = "mssql_default") -> None: + self.mssql_conn_id = mssql_conn_id + self.hook = MsSqlHook(mssql_conn_id=mssql_conn_id) + logging.info( + "[cliente_sqlserver.py] Initialized ClientSQLServerDB with " + f"mssql_conn_id={mssql_conn_id}" + ) + + @staticmethod + def _sanitize_value(value: Any) -> Any: + """Normaliza valores vindos do SQL Server para tipos seguros.""" + if value is None: + return None + + if isinstance(value, str): + cleaned_value = value.replace("\x00", "") + if cleaned_value.strip().lower() in {"nat", "nan"}: + return None + return cleaned_value + + if isinstance(value, float) and math.isnan(value): + return None + + if hasattr(value, "is_nan") and value.is_nan(): + return None + + if str(value) == "NaT": + return None + + if isinstance(value, (datetime, date, time)): + return value.isoformat() + + return value + + def fetch_table_all( + self, + schema: str, + table_name: str, + ) -> List[Dict[str, Any]]: + """Fetch all rows from a SQL Server table using SELECT *.""" + + full_table_name = f"{schema}.{table_name}" + + query = f"SELECT * FROM {full_table_name}" + logging.info(f"[cliente_sqlserver.py] Executing query: {query}") + + conn = self.hook.get_conn() + cursor = conn.cursor() + + try: + cursor.execute(query) + rows = cursor.fetchall() + + if not rows: + logging.info( + "[cliente_sqlserver.py] No rows found in %s", + full_table_name, + ) + return [] + + columns = [description[0] for description in cursor.description] + records = [ + { + column: self._sanitize_value(value) + for column, value in zip(columns, row) + } + for row in rows + ] + + logging.info( + "[cliente_sqlserver.py] Query executed successfully, fetched %s rows " + "from %s", + len(records), + full_table_name, + ) + return records + finally: + cursor.close() + conn.close() diff --git a/airflow_lappis/plugins/cliente_ted.py b/airflow_lappis/plugins/cliente_ted.py new file mode 100644 index 00000000..2c6f2c01 --- /dev/null +++ b/airflow_lappis/plugins/cliente_ted.py @@ -0,0 +1,181 @@ +import http +import logging +from cliente_base import ClienteBase + + +class ClienteTed(ClienteBase): + BASE_URL = "https://api.transferegov.gestao.gov.br/ted/" + BASE_HEADER = {"accept": "application/json"} + + def __init__(self) -> None: + super().__init__(base_url=ClienteTed.BASE_URL) + + def get_ted_by_programa_beneficiario(self, tx_codigo_siorg: str) -> list | None: + + endpoint = f"programa_beneficiario?tx_codigo_siorg=eq.{tx_codigo_siorg}" + logging.info( + f"[cliente_ted.py] Fetching ted for programa beneficiario: {tx_codigo_siorg}" + ) + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + "[cliente_ted.py] Successfully fetched ted for programa beneficiario: " + f"{tx_codigo_siorg}" + ) + return data + else: + logging.warning( + "[cliente_ted.py] Failed to fetch ted for programa beneficiario: " + f"{tx_codigo_siorg} with status: {status}" + ) + return None + + def get_programa_by_id_programa(self, id_programa: str) -> list | None: + + endpoint = f"programa?id_programa=eq.{id_programa}" + logging.info(f"[cliente_ted.py] Fetching programa for id_programa: {id_programa}") + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + "[cliente_ted.py] Successfully fetched programa for id_programa: " + f"{id_programa}" + ) + return data + else: + logging.warning( + "[cliente_ted.py] Failed to fetch programa for id_programa: " + f"{id_programa} with status: {status}" + ) + return None + + def get_planos_acao_by_id_programa(self, id_programa: str) -> list | None: + + endpoint = f"plano_acao?id_programa=eq.{id_programa}" + logging.info( + f"[cliente_ted.py] Fetching planos de ação for id_programa: {id_programa}" + ) + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + "[cliente_ted.py] Successfully fetched planos de ação for id_programa: " + f"{id_programa}" + ) + return data + else: + logging.warning( + "[cliente_ted.py] Failed to fetch planos de ação for id_programa: " + f"{id_programa} with status: {status}" + ) + return None + + def get_programas_by_sigla_unidade_descentralizadora(self, sigla: str) -> list | None: + endpoint = f"programa?sigla_unidade_descentralizadora=eq.{sigla}" + logging.info(f"Fetching programas for sigla_unidade_descentralizadora: {sigla}") + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"Successfully fetched programas for sigla_unidade_descentralizadora: " + f"{sigla}" + ) + return data + else: + logging.warning( + f"Failed to fetch programas for sigla_unidade_descentralizadora: " + f"{sigla} with status: {status}" + ) + return None + + def get_notas_de_credito_by_id_plano_acao(self, id_plano_acao: int) -> list | None: + endpoint = f"nota_credito?id_plano_acao=eq.{id_plano_acao}" + + logging.info(f"Buscando notas de crédito pelo plano de ação: {id_plano_acao}") + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info(f"Notas de crédito obtidas para plano de ação {id_plano_acao}") + return data + else: + logging.warning(f"Falha ao buscar notas de crédito - Status: {status}") + return None + + def get_programacao_financeira_by_id_plano_acao( + self, id_plano_acao: int + ) -> list | None: + endpoint = f"programacao_financeira?id_plano_acao=eq.{id_plano_acao}" + + logging.info( + f"Buscando programação financeira pelo plano de ação: {id_plano_acao}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"Programação financeira obtidas para plano de ação {id_plano_acao}" + ) + return data + else: + logging.warning(f"Falha ao buscar programação financeira - Status: {status}") + return None + + def get_todos_programas(self, limit: int = 1000, offset: int = 0) -> list | None: + """ + Função ATÔMICA: Busca uma única 'fatia' (página) de programas. + """ + headers = { + **self.BASE_HEADER, + "Range-Unit": "items", + "Range": f"{offset}-{offset + limit - 1}" + } + + endpoint = "programa" + logging.info(f"[cliente_ted.py] Fetching programas (offset: {offset}, limit: {limit})") + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=headers + ) + + if status in http.HTTPStatus.OK and isinstance(data, list): + logging.info(f"[cliente_ted.py] Sucesso ao buscar {len(data)} programas no offset {offset}.") + return data + else: + logging.error(f"[cliente_ted.py] Erro ao buscar programas. Status: {status}") + return None + + def get_all_programas(self, limit: int = 1000) -> list: + """ + Itera por todas as fatias de dados até o fim. + Segue a lógica da 'get_all_deputados'. + """ + all_programas = [] + current_offset = 0 + + while True: + programas = self.get_todos_programas(limit=limit, offset=current_offset) + + if not programas: + break + + all_programas.extend(programas) + + if len(programas) < limit: + logging.info("[cliente_ted.py] Última página alcançada.") + break + + current_offset += limit + + logging.info(f"[cliente_ted.py] Carga completa finalizada. Total: {len(all_programas)} programas.") + return all_programas \ No newline at end of file diff --git a/airflow_lappis/plugins/cliente_transferegov_emendas.py b/airflow_lappis/plugins/cliente_transferegov_emendas.py new file mode 100644 index 00000000..3fda6ecf --- /dev/null +++ b/airflow_lappis/plugins/cliente_transferegov_emendas.py @@ -0,0 +1,1237 @@ +import http +import logging +from typing import Optional +from cliente_base import ClienteBase + + +class ClienteTransfereGov(ClienteBase): + BASE_URL = "https://api.transferegov.gestao.gov.br/transferenciasespeciais/" + BASE_HEADER = {"accept": "application/json", "User-Agent": "Airflow-GovHub/1.0"} + + def __init__(self) -> None: + super().__init__(base_url=ClienteTransfereGov.BASE_URL) + logging.info( + "[cliente_transfere_gov.py] Initialized ClienteTransfereGov with base_url: " + f"{ClienteTransfereGov.BASE_URL}" + ) + + def get_programas_especiais( + self, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """ + Obter programas especiais com paginação. + + Args: + limit (int): Quantidade de registros por página (padrão: 1000) + offset (int): Deslocamento inicial (padrão: 0) + + Returns: + list: lista de programas especiais ou None se falhar + """ + endpoint = "programa_especial" + params = { + "select": "*", + "order": "id_programa.asc", + "limit": limit, + "offset": offset, + } + + logging.info( + f"[cliente_transfere_gov.py] Fetching programas especiais with " + f"limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} " + "programas especiais" + ) + return data + else: + logging.warning( + f"[cliente_transfere_gov.py] Failed to fetch programas especiais " + f"with status: {status}" + ) + return None + + def get_all_programas_especiais(self, page_size: int = 1000) -> list: + """ + Obter todos os programas especiais com paginação automática. + + Args: + page_size (int): Quantidade de registros por requisição (padrão: 1000) + + Returns: + list: lista completa de programas especiais + """ + all_data = [] + offset = 0 + page = 1 + + logging.info( + "[cliente_transfere_gov.py] Starting full extraction of programas especiais" + ) + + while True: + logging.info( + f"[cliente_transfere_gov.py] Fetching page {page} " f"(offset: {offset})" + ) + + data = self.get_programas_especiais(limit=page_size, offset=offset) + + if not data or len(data) == 0: + logging.info( + "[cliente_transfere_gov.py] No more data received. " + "Extraction complete." + ) + break + + all_data.extend(data) + logging.info( + f"[cliente_transfere_gov.py] Page {page} fetched: {len(data)} records. " + f"Total so far: {len(all_data)}" + ) + + # Se recebemos menos registros que o limite, é a última página + if len(data) < page_size: + logging.info("[cliente_transfere_gov.py] Last page reached.") + break + + offset += page_size + page += 1 + + logging.info( + f"[cliente_transfere_gov.py] Extraction completed. " + f"Total records: {len(all_data)}" + ) + return all_data + + def get_planos_acao_especiais_by_programa( + self, id_programa: int, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """ + Obter planos de ação especiais por ID do programa com paginação. + + Args: + id_programa (int): ID do programa + limit (int): Quantidade de registros por página (padrão: 1000) + offset (int): Deslocamento inicial (padrão: 0) + + Returns: + list: lista de planos de ação especiais ou None se falhar + """ + endpoint = f"plano_acao_especial?id_programa=eq.{id_programa}" + params = {"select": "*", "limit": limit, "offset": offset} + + logging.info( + f"[cliente_transfere_gov.py] Fetching planos de ação especiais for " + f"id_programa={id_programa}, limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} " + f"planos de ação for programa {id_programa}" + ) + return data + else: + logging.warning( + f"[cliente_transfere_gov.py] Failed to fetch planos de ação for " + f"programa {id_programa} with status: {status}" + ) + return None + + def get_all_planos_acao_especiais_by_programa( + self, id_programa: int, page_size: int = 1000 + ) -> list: + """ + Obter todos os planos de ação especiais de um programa com paginação automática. + + Args: + id_programa (int): ID do programa + page_size (int): Quantidade de registros por requisição (padrão: 1000) + + Returns: + list: lista completa de planos de ação especiais + """ + all_data = [] + offset = 0 + + while True: + data = self.get_planos_acao_especiais_by_programa( + id_programa, limit=page_size, offset=offset + ) + + if not data or len(data) == 0: + break + + all_data.extend(data) + + if len(data) < page_size: + break + + offset += page_size + + logging.info( + f"[cliente_transfere_gov.py] Total planos de ação for programa " + f"{id_programa}: {len(all_data)}" + ) + return all_data + + def get_executores_especiais( + self, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """ + Busca uma fatia global de executores com ordenação para maior performance. + """ + endpoint = "executor_especial" + + params = { + "select": "*", + "order": "id_executor.asc", # Ordenação por chave primária + "limit": limit, + "offset": offset, + } + + logging.info( + f"[cliente_transfere_gov.py] Fetching executores (limit={limit}, offset={offset})" + ) + + status, data = self.request( + http.HTTPMethod.GET, + endpoint, + headers=self.BASE_HEADER, + params=params, + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Sucesso: {len(data)} registros retornados." + ) + return data + + logging.warning( + f"[cliente_transfere_gov.py] Falha na requisição. Status: {status}" + ) + return None + + def get_all_executores_especiais(self, limit: int = 1000) -> list: + """ + Extração completa com logs de progresso detalhados. + """ + all_data = [] + offset = 0 + page = 1 + + logging.info( + "[cliente_transfere_gov.py] Iniciando extração GLOBAL de executores especiais" + ) + + while True: + data = self.get_executores_especiais(limit=limit, offset=offset) + + if not data or len(data) == 0: + logging.info("[cliente_transfere_gov.py] Fim dos dados alcançado.") + break + + all_data.extend(data) + + logging.info( + f"[cliente_transfere_gov.py] Página {page} processada. " + f"Total acumulado: {len(all_data)}" + ) + + if len(data) < limit: + logging.info("[cliente_transfere_gov.py] Última página identificada.") + break + + offset += limit + page += 1 + + logging.info( + f"[cliente_transfere_gov.py] Extração finalizada. Total: {len(all_data)}" + ) + return all_data + + def get_empenhos_especiais_by_plano_acao( + self, id_plano_acao: int, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """ + Obter empenhos especiais por ID do plano de ação com paginação. + + Args: + id_plano_acao (int): ID do plano de ação + limit (int): Quantidade de registros por página (padrão: 1000) + offset (int): Deslocamento inicial (padrão: 0) + + Returns: + list: lista de empenhos especiais ou None se falhar + """ + endpoint = f"empenho_especial?id_plano_acao=eq.{id_plano_acao}" + params = {"select": "*", "limit": limit, "offset": offset} + + logging.info( + f"[cliente_transfere_gov.py] Fetching empenhos especiais for " + f"id_plano_acao={id_plano_acao}, limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} " + f"empenhos especiais for plano de ação {id_plano_acao}" + ) + return data + else: + logging.warning( + f"[cliente_transfere_gov.py] Failed to fetch empenhos especiais for " + f"plano de ação {id_plano_acao} with status: {status}" + ) + return None + + def get_empenhos_especiais( + self, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """ + Obter empenhos especiais com paginação. + Args: + limit (int): Quantidade de registros por página (padrão: 1000) + offset (int): Deslocamento inicial (padrão: 0) + Returns: + list: lista de empenhos especiais ou None se falhar + """ + endpoint = "empenho_especial" + params = { + "select": "*", + "order": "id_empenho.asc", + "limit": limit, + "offset": offset, + } + + logging.info( + f"[cliente_transfere_gov.py] Fetching empenhos especiais with " + f"limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} " + "empenhos especiais" + ) + return data + else: + logging.warning( + f"[cliente_transfere_gov.py] Failed to fetch empenhos especiais " + f"with status: {status}" + ) + return None + + def get_all_empenhos_especiais(self, page_size: int = 1000) -> list: + """ + Obter todos os empenhos especiais com paginação automática. + Args: + page_size (int): Quantidade de registros por requisição (padrão: 1000) + Returns: + list: lista completa de empenhos especiais + """ + all_data = [] + offset = 0 + page = 1 + + logging.info( + "[cliente_transfere_gov.py] Starting full extraction of empenhos especiais" + ) + + while True: + logging.info( + f"[cliente_transfere_gov.py] Fetching page {page} " f"(offset: {offset})" + ) + + data = self.get_empenhos_especiais(limit=page_size, offset=offset) + + if not data or len(data) == 0: + logging.info( + "[cliente_transfere_gov.py] No more data received. " + "Extraction complete." + ) + break + + all_data.extend(data) + logging.info( + f"[cliente_transfere_gov.py] Page {page} fetched: {len(data)} records. " + f"Total so far: {len(all_data)}" + ) + + # Se recebemos menos registros que o limite, é a última página + if len(data) < page_size: + logging.info("[cliente_transfere_gov.py] Last page reached.") + break + + offset += page_size + page += 1 + + logging.info( + f"[cliente_transfere_gov.py] Extraction completed. " + f"Total records: {len(all_data)}" + ) + return all_data + + def get_all_empenhos_especiais_by_plano_acao( + self, id_plano_acao: int, page_size: int = 1000 + ) -> list: + """ + Obter todos os empenhos especiais de um plano de ação com paginação automática. + + Args: + id_plano_acao (int): ID do plano de ação + page_size (int): Quantidade de registros por requisição (padrão: 1000) + + Returns: + list: lista completa de empenhos especiais + """ + all_data = [] + offset = 0 + + while True: + data = self.get_empenhos_especiais_by_plano_acao( + id_plano_acao, limit=page_size, offset=offset + ) + + if not data or len(data) == 0: + break + + all_data.extend(data) + + if len(data) < page_size: + break + + offset += page_size + + logging.info( + f"[cliente_transfere_gov.py] Total empenhos especiais for plano de ação " + f"{id_plano_acao}: {len(all_data)}" + ) + return all_data + + def get_relatorio_gestao_especial( + self, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """Busca relatórios de gestão de forma global com paginação.""" + endpoint = "relatorio_gestao_especial" + params = { + "select": "*", + "order": "id_relatorio_gestao.asc", + "limit": limit, + "offset": offset, + } + + logging.info( + f"[cliente_transfere_gov.py] Fetching relatorios with limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} records" + ) + return data + + logging.warning(f"[cliente_transfere_gov.py] Failed with status: {status}") + return None + + def get_all_relatorio_gestao_especial(self, page_size: int = 1000) -> list: + """Obter todos os relatórios com paginação automática global.""" + all_data = [] + offset = 0 + page = 1 + + logging.info( + "[cliente_transfere_gov.py] Starting full extraction of relatorio_gestao_especial" + ) + + while True: + logging.info( + f"[cliente_transfere_gov.py] Fetching page {page} (offset: {offset})" + ) + + data = self.get_relatorio_gestao_especial(limit=page_size, offset=offset) + + if not data or len(data) == 0: + logging.info("[cliente_transfere_gov.py] No more data received.") + break + + all_data.extend(data) + logging.info( + f"[cliente_transfere_gov.py] Page {page} fetched. Total so far: {len(all_data)}" + ) + + if len(data) < page_size: + logging.info("[cliente_transfere_gov.py] Last page reached.") + break + + offset += page_size + page += 1 + + return all_data + + def get_documentos_habeis_especiais( + self, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """ + Obter documentos hábeis especiais com paginação. + + Args: + limit (int): Quantidade de registros por página (padrão: 1000) + offset (int): Deslocamento inicial (padrão: 0) + + Returns: + list: lista de documentos hábeis especiais ou None se falhar + """ + endpoint = "documento_habil_especial" + params = { + "select": "*", + "order": "id_dh.asc", + "limit": limit, + "offset": offset, + } + + logging.info( + f"[cliente_transfere_gov.py] Fetching documentos hábeis especiais with " + f"limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} " + "documentos hábeis especiais" + ) + return data + else: + logging.warning( + f"[cliente_transfere_gov.py] Failed to fetch documentos hábeis especiais " + f"with status: {status}" + ) + return None + + def get_all_documentos_habeis_especiais(self, page_size: int = 1000) -> list: + """ + Obter todos os documentos hábeis especiais com paginação automática. + + Args: + page_size (int): Quantidade de registros por requisição (padrão: 1000) + + Returns: + list: lista completa de documentos hábeis especiais + """ + all_data = [] + offset = 0 + page = 1 + + logging.info( + "[cliente_transfere_gov.py] Starting full extraction of " + "documentos hábeis especiais" + ) + + while True: + logging.info( + f"[cliente_transfere_gov.py] Fetching page {page} " f"(offset: {offset})" + ) + + data = self.get_documentos_habeis_especiais(limit=page_size, offset=offset) + + if not data or len(data) == 0: + logging.info( + "[cliente_transfere_gov.py] No more data received. " + "Extraction complete." + ) + break + + all_data.extend(data) + logging.info( + f"[cliente_transfere_gov.py] Page {page} fetched: {len(data)} records. " + f"Total so far: {len(all_data)}" + ) + + # Se recebemos menos registros que o limite, é a última página + if len(data) < page_size: + logging.info("[cliente_transfere_gov.py] Last page reached.") + break + + offset += page_size + page += 1 + + logging.info( + f"[cliente_transfere_gov.py] Extraction completed. " + f"Total records: {len(all_data)}" + ) + return all_data + + def get_documentos_habeis_especiais_by_empenho( + self, id_empenho: int, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """ + Obter documentos hábeis especiais por ID do empenho com paginação. + + Args: + id_empenho (int): ID do empenho + limit (int): Quantidade de registros por página (padrão: 1000) + offset (int): Deslocamento inicial (padrão: 0) + + Returns: + list: lista de documentos hábeis especiais ou None se falhar + """ + endpoint = f"documento_habil_especial?id_empenho=eq.{id_empenho}" + params = {"select": "*", "limit": limit, "offset": offset} + + logging.info( + f"[cliente_transfere_gov.py] Fetching documentos hábeis especiais for " + f"id_empenho={id_empenho}, limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} " + f"documentos hábeis especiais for empenho {id_empenho}" + ) + return data + else: + logging.warning( + f"[cliente_transfere_gov.py] Failed to fetch documentos " + f"hábeis especiais for empenho {id_empenho} with status: " + f"{status}" + ) + return None + + def get_all_documentos_habeis_especiais_by_empenho( + self, id_empenho: int, page_size: int = 1000 + ) -> list: + """ + Obter todos os documentos hábeis especiais de um empenho com paginação automática. + + Args: + id_empenho (int): ID do empenho + page_size (int): Quantidade de registros por requisição (padrão: 1000) + + Returns: + list: lista completa de documentos hábeis especiais + """ + all_data = [] + offset = 0 + + while True: + data = self.get_documentos_habeis_especiais_by_empenho( + id_empenho, limit=page_size, offset=offset + ) + + if not data or len(data) == 0: + break + + all_data.extend(data) + + if len(data) < page_size: + break + + offset += page_size + + logging.info( + f"[cliente_transfere_gov.py] Total documentos hábeis especiais for empenho " + f"{id_empenho}: {len(all_data)}" + ) + return all_data + + def get_metas_especiais(self, limit: int = 1000, offset: int = 0) -> Optional[list]: + """ + Obter metas especiais com paginação. + + Args: + limit (int): Quantidade de registros por página (padrão: 1000) + offset (int): Deslocamento inicial (padrão: 0) + + Returns: + list: lista de metas especiais ou None se falhar + """ + endpoint = "meta_especial" + params = { + "select": "*", + "order": "id_meta.asc", + "limit": limit, + "offset": offset, + } + + logging.info( + f"[cliente_transfere_gov.py] Fetching metas especiais with " + f"limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} " + "metas especiais" + ) + return data + else: + logging.warning( + f"[cliente_transfere_gov.py] Failed to fetch metas especiais " + f"with status: {status}" + ) + return None + + def get_all_metas_especiais(self, page_size: int = 1000) -> list: + """ + Obter todas as metas especiais com paginação automática. + + Args: + page_size (int): Quantidade de registros por requisição (padrão: 1000) + + Returns: + list: lista completa de metas especiais + """ + all_data = [] + offset = 0 + page = 1 + + logging.info( + "[cliente_transfere_gov.py] Starting full extraction of metas especiais" + ) + + while True: + logging.info( + f"[cliente_transfere_gov.py] Fetching page {page} (offset: {offset})" + ) + + data = self.get_metas_especiais(limit=page_size, offset=offset) + + if not data or len(data) == 0: + logging.info( + "[cliente_transfere_gov.py] No more data received. " + "Extraction complete." + ) + break + + all_data.extend(data) + logging.info( + f"[cliente_transfere_gov.py] Page {page} fetched: {len(data)} records. " + f"Total so far: {len(all_data)}" + ) + + if len(data) < page_size: + logging.info("[cliente_transfere_gov.py] Last page reached.") + break + + offset += page_size + page += 1 + + logging.info( + f"[cliente_transfere_gov.py] Extraction completed. " + f"Total metas especiais: {len(all_data)}" + ) + return all_data + + def get_finalidades_especiais( + self, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """ + Obter finalidades especiais com paginação. + + Args: + limit (int): Quantidade de registros por página (padrão: 1000) + offset (int): Deslocamento inicial (padrão: 0) + + Returns: + list: lista de finalidades especiais ou None se falhar + """ + endpoint = "finalidade_especial" + params = { + "select": "*", + "order": "id_executor.asc", + "limit": limit, + "offset": offset, + } + + logging.info( + f"[cliente_transfere_gov.py] Fetching finalidades especiais with " + f"limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} " + "finalidades especiais" + ) + return data + else: + logging.warning( + f"[cliente_transfere_gov.py] Failed to fetch finalidades especiais " + f"with status: {status}" + ) + return None + + def get_all_finalidades_especiais(self, page_size: int = 1000) -> list: + """ + Obter todas as finalidades especiais com paginação automática. + + Args: + page_size (int): Quantidade de registros por requisição (padrão: 1000) + + Returns: + list: lista completa de finalidades especiais + """ + all_data = [] + offset = 0 + page = 1 + + logging.info( + "[cliente_transfere_gov.py] Starting full extraction of " + "finalidades especiais" + ) + + while True: + logging.info( + f"[cliente_transfere_gov.py] Fetching page {page} " f"(offset: {offset})" + ) + + data = self.get_finalidades_especiais(limit=page_size, offset=offset) + + if not data or len(data) == 0: + logging.info( + "[cliente_transfere_gov.py] No more data received. " + "Extraction complete." + ) + break + + all_data.extend(data) + logging.info( + f"[cliente_transfere_gov.py] Page {page} fetched: {len(data)} records. " + f"Total so far: {len(all_data)}" + ) + + # Se recebemos menos registros que o limite, é a última página + if len(data) < page_size: + logging.info("[cliente_transfere_gov.py] Last page reached.") + break + + offset += page_size + page += 1 + + logging.info( + f"[cliente_transfere_gov.py] Extraction completed. " + f"Total records: {len(all_data)}" + ) + return all_data + + def get_ordens_bancarias_especiais( + self, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """ + Obter ordens bancárias especiais com paginação. + + Args: + limit (int): Quantidade de registros por página (padrão: 1000) + offset (int): Deslocamento inicial (padrão: 0) + + Returns: + list: lista de ordens bancárias especiais ou None se falhar + """ + endpoint = "ordem_pagamento_ordem_bancaria_especial" + params = { + "select": "*", + "order": "id_op_ob.asc", + "limit": limit, + "offset": offset, + } + + logging.info( + f"[cliente_transfere_gov.py] Fetching ordens bancárias especiais with " + f"limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} " + "ordens bancárias especiais" + ) + return data + else: + logging.warning( + f"[cliente_transfere_gov.py] Failed to fetch ordens bancárias especiais " + f"with status: {status}" + ) + return None + + def get_all_ordens_bancarias_especiais(self, page_size: int = 1000) -> list: + """ + Obter todas as ordens bancárias especiais com paginação automática. + + Args: + page_size (int): Quantidade de registros por requisição (padrão: 1000) + + Returns: + list: lista completa de ordens bancárias especiais + """ + all_data = [] + offset = 0 + page = 1 + + logging.info( + "[cliente_transfere_gov.py] Starting full extraction of " + "ordens bancárias especiais" + ) + + while True: + logging.info( + f"[cliente_transfere_gov.py] Fetching page {page} " f"(offset: {offset})" + ) + + data = self.get_ordens_bancarias_especiais(limit=page_size, offset=offset) + + if not data or len(data) == 0: + logging.info( + "[cliente_transfere_gov.py] No more data received. " + "Extraction complete." + ) + break + + all_data.extend(data) + logging.info( + f"[cliente_transfere_gov.py] Page {page} fetched: {len(data)} records. " + f"Total so far: {len(all_data)}" + ) + + # Se recebemos menos registros que o limite, é a última página + if len(data) < page_size: + logging.info("[cliente_transfere_gov.py] Last page reached.") + break + + offset += page_size + page += 1 + + logging.info( + f"[cliente_transfere_gov.py] Extraction completed. " + f"Total records: {len(all_data)}" + ) + return all_data + + def get_relatorio_gestao_novo_especial( + self, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """ + Obter relatórios de gestão novo especial com paginação. + + Args: + limit (int): Quantidade de registros por página (padrão: 1000) + offset (int): Deslocamento inicial (padrão: 0) + + Returns: + list: lista de relatórios de gestão novo especial ou None se falhar + """ + endpoint = "relatorio_gestao_novo_especial" + params = { + "select": "*", + "order": "id_relatorio_gestao_novo.asc", + "limit": limit, + "offset": offset, + } + + logging.info( + f"[cliente_transfere_gov.py] Fetching relatórios de gestão novo with " + f"limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} " + "relatórios de gestão novo" + ) + return data + else: + logging.warning( + f"[cliente_transfere_gov.py] Failed to fetch relatórios de gestão novo " + f"with status: {status}" + ) + return None + + def get_all_relatorios_gestao_novo_especial(self, page_size: int = 1000) -> list: + """ + Obter todos os relatórios de gestão novo especial com paginação automática. + + Args: + page_size (int): Quantidade de registros por requisição (padrão: 1000) + + Returns: + list: lista completa de relatórios de gestão novo especial + """ + all_data = [] + offset = 0 + page = 1 + + logging.info( + "[cliente_transfere_gov.py] Starting full extraction of " + "relatórios de gestão novo" + ) + + while True: + logging.info( + f"[cliente_transfere_gov.py] Fetching page {page} " f"(offset: {offset})" + ) + + data = self.get_relatorio_gestao_novo_especial(limit=page_size, offset=offset) + + if not data or len(data) == 0: + logging.info( + "[cliente_transfere_gov.py] No more data received. " + "Extraction complete." + ) + break + + all_data.extend(data) + logging.info( + f"[cliente_transfere_gov.py] Page {page} fetched: {len(data)} records. " + f"Total so far: {len(all_data)}" + ) + + # Se recebemos menos registros que o limite, é a última página + if len(data) < page_size: + logging.info("[cliente_transfere_gov.py] Last page reached.") + break + + offset += page_size + page += 1 + + logging.info( + f"[cliente_transfere_gov.py] Extraction completed. " + f"Total records: {len(all_data)}" + ) + return all_data + + def get_plano_trabalho_especial( + self, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """ + Obter planos de trabalho especiais com paginação. + + Args: + limit (int): Quantidade de registros por página (padrão: 1000) + offset (int): Deslocamento inicial (padrão: 0) + + Returns: + list: lista de planos de trabalho ou None se falhar + """ + endpoint = "plano_trabalho_especial" + params = { + "select": "*", + "order": "id_plano_trabalho.asc", + "limit": limit, + "offset": offset, + } + + logging.info( + f"[cliente_transfere_gov.py] Fetching plano_trabalho_especial with " + f"limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} " + "planos de trabalho especiais" + ) + return data + else: + logging.warning( + f"[cliente_transfere_gov.py] Failed to fetch plano_trabalho_especial " + f"with status: {status}" + ) + return None + + def get_all_plano_trabalho_especial(self, page_size: int = 1000) -> list: + """ + Obter todos os planos de trabalho especiais com paginação automática. + + Args: + page_size (int): Quantidade de registros por requisição (padrão: 1000) + + Returns: + list: lista completa de planos de trabalho especiais + """ + all_data = [] + offset = 0 + page = 1 + + logging.info( + "[cliente_transfere_gov.py] Starting full extraction of " + "plano_trabalho_especial" + ) + + while True: + logging.info( + f"[cliente_transfere_gov.py] Fetching page {page} (offset: {offset})" + ) + + data = self.get_plano_trabalho_especial(limit=page_size, offset=offset) + + if not data or len(data) == 0: + logging.info( + "[cliente_transfere_gov.py] No more data received. " + "Extraction complete." + ) + break + + all_data.extend(data) + logging.info( + f"[cliente_transfere_gov.py] Page {page} fetched: {len(data)} records. " + f"Total so far: {len(all_data)}" + ) + + if len(data) < page_size: + logging.info("[cliente_transfere_gov.py] Last page reached.") + break + + offset += page_size + page += 1 + + logging.info( + f"[cliente_transfere_gov.py] Extraction completed. " + f"Total records: {len(all_data)}" + ) + return all_data + + def get_historico_pagamentos_especiais( + self, limit: int = 1000, offset: int = 0 + ) -> Optional[list]: + """ + Obter histórico de pagamentos especiais com paginação. + + Args: + limit (int): Quantidade de registros por página (padrão: 1000) + offset (int): Deslocamento inicial (padrão: 0) + + Returns: + list: lista do histórico de pagamentos especiais ou None se falhar + """ + endpoint = "historico_pagamento_especial" + params = { + "select": "*", + "order": "id_historico_op_ob.asc", + "limit": limit, + "offset": offset, + } + + logging.info( + f"[cliente_transfere_gov.py] Fetching histórico de pagamentos especiais with " + f"limit={limit}, offset={offset}" + ) + + status, data = self.request( + http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER, params=params + ) + + if status == http.HTTPStatus.OK and isinstance(data, list): + logging.info( + f"[cliente_transfere_gov.py] Successfully fetched {len(data)} " + "histórico de pagamentos especiais" + ) + return data + else: + logging.warning( + f"[cliente_transfere_gov.py] Failed to fetch histórico de pagamentos" + " especiais " + f"with status: {status}" + ) + return None + + def get_all_historico_pagamentos_especiais(self, page_size: int = 1000) -> list: + """ + Obter todo o historico de pagamentos especiais com paginação automática. + + Args: + page_size (int): Quantidade de registros por requisição (padrão: 1000) + + Returns: + list: lista completa do histórico de pagamentos especiais + """ + all_data = [] + offset = 0 + page = 1 + + logging.info( + "[cliente_transfere_gov.py] Starting full extraction of histórico de" + " pagamentos especiais" + ) + + while True: + logging.info( + f"[cliente_transfere_gov.py] Fetching page {page} (offset: {offset})" + ) + + data = self.get_historico_pagamentos_especiais(limit=page_size, offset=offset) + + if not data or len(data) == 0: + logging.info( + "[cliente_transfere_gov.py] No more data received. " + "Extraction complete." + ) + break + + all_data.extend(data) + logging.info( + f"[cliente_transfere_gov.py] Page {page} fetched: {len(data)} records. " + f"Total so far: {len(all_data)}" + ) + + if len(data) < page_size: + logging.info("[cliente_transfere_gov.py] Last page reached.") + break + + offset += page_size + page += 1 + + logging.info( + f"[cliente_transfere_gov.py] Extraction completed. " + f"Total histórico de pagamentos especiais: {len(all_data)}" + ) + return all_data diff --git a/airflow_lappis/plugins/schedule_loader.py b/airflow_lappis/plugins/schedule_loader.py new file mode 100644 index 00000000..610afa03 --- /dev/null +++ b/airflow_lappis/plugins/schedule_loader.py @@ -0,0 +1,28 @@ +from airflow.models import Variable +from datetime import timedelta + + +def get_dynamic_schedule(dag_id: str, default: str = "@daily") -> str | timedelta: + """ + Retorna o schedule da Variable 'dynamic_schedules' para a DAG. + Suporta: 'preset'/'cron' (retorna str) e 'timedelta' (retorna timedelta). + Se não houver schedule configurado, retorna o valor default (@daily). + """ + + schedules = Variable.get("dynamic_schedules", default_var={}, deserialize_json=True) + + dag_schedule = schedules.get(dag_id) + + if not dag_schedule: + return default + + dag_type = dag_schedule.get("type") + dag_value = dag_schedule.get("value") + + if dag_type in ["preset", "cron"]: + return str(dag_value) + + if dag_type == "timedelta": + return timedelta(**dag_value) + + raise ValueError(f"Tipo de schedule inválido: {dag_type}") diff --git a/airflow_lappis/plugins/tabelas_siconv.py b/airflow_lappis/plugins/tabelas_siconv.py new file mode 100644 index 00000000..985afa5a --- /dev/null +++ b/airflow_lappis/plugins/tabelas_siconv.py @@ -0,0 +1,224 @@ +TABELAS_SICONV = [ + { + "nome_tabela": "proposta", + "nome_csv": "siconv_proposta.csv", + "conflict_fields": ["id_proposta"], + "primary_key": ["id_proposta"], + "truncate_before_insert": False, + "skip_rows": 0, + "colunas": [ + "ID_PROPOSTA", "UF_PROPONENTE", "MUNIC_PROPONENTE", "COD_MUNIC_IBGE", + "COD_ORGAO_SUP", "DESC_ORGAO_SUP", "NATUREZA_JURIDICA", "NR_PROPOSTA", + "DIA_PROP", "MES_PROP", "ANO_PROP", "DIA_PROPOSTA", "COD_ORGAO", + "DESC_ORGAO", "MODALIDADE", "IDENTIF_PROPONENTE", "NM_PROPONENTE", + "CEP_PROPONENTE", "ENDERECO_PROPONENTE", "BAIRRO_PROPONENTE", "NM_BANCO", + "SITUACAO_CONTA", "SITUACAO_PROJETO_BASICO", "SIT_PROPOSTA", + "DIA_INIC_VIGENCIA_PROPOSTA", "DIA_FIM_VIGENCIA_PROPOSTA", + "OBJETO_PROPOSTA", "ITEM_INVESTIMENTO", "ENVIADA_MANDATARIA", + "NOME_SUBTIPO_PROPOSTA", "DESCRICAO_SUBTIPO_PROPOSTA", "VL_GLOBAL_PROP", + "VL_REPASSE_PROP", "VL_CONTRAPARTIDA_PROP", "CD_AGENCIA", "CD_CONTA", + ], + }, + { + "nome_tabela": "convenio", + "nome_csv": "siconv_convenio.csv", + "conflict_fields": ["nr_convenio"], + "primary_key": ["nr_convenio"], + "truncate_before_insert": False, + "skip_rows": 0, + "colunas": [ + "NR_CONVENIO", "ID_PROPOSTA", "DIA", "MES", "ANO", "DIA_ASSIN_CONV", + "SIT_CONVENIO", "SUBSITUACAO_CONV", "SITUACAO_PUBLICACAO", "INSTRUMENTO_ATIVO", + "IND_OPERA_OBTV", "NR_PROCESSO", "UG_EMITENTE", "DIA_PUBL_CONV", + "DIA_INIC_VIGENC_CONV", "DIA_FIM_VIGENC_CONV", "DIA_FIM_VIGENC_ORIGINAL_CONV", + "DIAS_PREST_CONTAS", "DIA_LIMITE_PREST_CONTAS", "DATA_SUSPENSIVA", + "DATA_RETIRADA_SUSPENSIVA", "DIAS_CLAUSULA_SUSPENSIVA", "SITUACAO_CONTRATACAO", + "IND_ASSINADO", "MOTIVO_SUSPENSAO", "IND_FOTO", "QTDE_CONVENIOS", "QTD_TA", + "QTD_PRORROGA", "VL_GLOBAL_CONV", "VL_REPASSE_CONV", "VL_CONTRAPARTIDA_CONV", + "VL_EMPENHADO_CONV", "VL_DESEMBOLSADO_CONV", "VL_SALDO_REMAN_TESOURO", + "VL_SALDO_REMAN_CONVENENTE", "VL_RENDIMENTO_APLICACAO", "VL_INGRESSO_CONTRAPARTIDA", + "VL_SALDO_CONTA", "VALOR_GLOBAL_ORIGINAL_CONV", + ], + }, + { + "nome_tabela": "desembolso", + "nome_csv": "siconv_desembolso.csv", + "conflict_fields": ["id_desembolso"], + "primary_key": ["id_desembolso"], + "truncate_before_insert": False, + "skip_rows": 0, + "colunas": [ + "ID_DESEMBOLSO", "NR_CONVENIO", "DT_ULT_DESEMBOLSO", "QTD_DIAS_SEM_DESEMBOLSO", + "DATA_DESEMBOLSO", "ANO_DESEMBOLSO", "MES_DESEMBOLSO", "NR_SIAFI", + "UG_EMITENTE_DH", "OBSERVACAO_DH", "VL_DESEMBOLSADO", + ], + }, + { + "nome_tabela": "desbloqueio", + "nome_csv": "siconv_desbloqueio_cr.csv", + "conflict_fields": [], + "primary_key": [], + "truncate_before_insert": True, + "skip_rows": 0, + "colunas": [ + "NR_CONVENIO", "NR_OB", "DATA_CADASTRO", "DATA_ENVIO", + "TIPO_RECURSO_DESBLOQUEIO", "VL_TOTAL_DESBLOQUEIO", + "VL_DESBLOQUEADO", "VL_BLOQUEADO", + ], + }, + { + "nome_tabela": "solicitacao_alteracao", + "nome_csv": "siconv_solicitacao_alteracao.csv", + "conflict_fields": ["id_solicitacao"], + "primary_key": ["id_solicitacao"], + "truncate_before_insert": False, + "skip_rows": 0, + "colunas": [ + "ID_SOLICITACAO", "NR_CONVENIO", "NR_SOLICITACAO", "SITUACAO_SOLICITACAO", + "OBJETO_SOLICITACAO", "DATA_SOLICITACAO", + ], + }, + { + "nome_tabela": "termo_aditivo", + "nome_csv": "siconv_termo_aditivo.csv", + "conflict_fields": ["nr_convenio", "numero_ta"], + "primary_key": ["nr_convenio", "numero_ta"], + "truncate_before_insert": False, + "skip_rows": 0, + "colunas": [ + "NR_CONVENIO", "ID_SOLICITACAO", "NUMERO_TA", "TIPO_TA", + "VL_GLOBAL_TA", "VL_REPASSE_TA", "VL_CONTRAPARTIDA_TA", + "DT_ASSINATURA_TA", "DT_INICIO_TA", "DT_FIM_TA", "JUSTIFICATIVA_TA", + ], + }, + { + "nome_tabela": "solicitacao_rendimento_aplicacao", + "nome_csv": "siconv_solicitacao_rendimento_aplicacao.csv", + "conflict_fields": ["id_solicitacao_rend_aplicacao"], + "primary_key": ["id_solicitacao_rend_aplicacao"], + "truncate_before_insert": False, + "skip_rows": 0, + "colunas": [ + "ID_SOLICITACAO_REND_APLICACAO", "NR_CONVENIO", "NR_SOLICITACAO_REND_APLICACAO", + "STATUS_SOLICITACAO_REND_APLICACAO", "DATA_SOLICITACAO_REND_APLICACAO", + "VALOR_SOLICITACAO_REND_APLICACAO", "VALOR_APROVADO_SOLICITACAO_REND_APLICACAO", + ], + }, + { + "nome_tabela": "prorroga_oficio", + "nome_csv": "siconv_prorroga_oficio.csv", + "conflict_fields": [], + "primary_key": [], + "truncate_before_insert": True, + "skip_rows": 0, + "colunas": [ + "NR_CONVENIO", "NR_PRORROGA", "DT_INICIO_PRORROGA", "DT_FIM_PRORROGA", + "DIAS_PRORROGA", "DT_ASSINATURA_PRORROGA", "SIT_PRORROGA", + ], + }, + { + "nome_tabela": "pagamento_tributo", + "nome_csv": "siconv_pagamento_tributo.csv", + "conflict_fields": ["nr_convenio", "data_tributo"], + "primary_key": ["nr_convenio", "data_tributo"], + "truncate_before_insert": False, + "skip_rows": 0, + "colunas": [ + "NR_CONVENIO", "DATA_TRIBUTO", "VL_PAG_TRIBUTOS", + ], + }, + { + "nome_tabela": "pagamento", + "nome_csv": "siconv_pagamento.csv", + "conflict_fields": ["nr_mov_fin"], + "primary_key": ["nr_mov_fin"], + "truncate_before_insert": False, + "skip_rows": 0, + "colunas": [ + "NR_MOV_FIN", "NR_CONVENIO", "IDENTIF_FORNECEDOR", "NOME_FORNECEDOR", + "TP_MOV_FINANCEIRA", "DATA_PAG", "NR_DL", "DESC_DL", + "VL_PAGO", "ID_DL", "DATA_EMISSAO_DL", + ], + }, + { + "nome_tabela": "licitacao", + "nome_csv": "siconv_licitacao.csv", + "conflict_fields": ["id_licitacao"], + "primary_key": ["id_licitacao"], + "truncate_before_insert": False, + "skip_rows": 0, + "colunas": [ + "ID_LICITACAO", "NR_CONVENIO", "NR_LICITACAO", "MODALIDADE_LICITACAO", + "TP_PROCESSO_COMPRA", "TIPO_LICITACAO", "NR_PROCESSO_LICITACAO", + "DATA_PUBLICACAO_LICITACAO", "DATA_ABERTURA_LICITACAO", "DATA_ENCERRAMENTO_LICITACAO", + "DATA_HOMOLOGACAO_LICITACAO", "STATUS_LICITACAO", "SITUACAO_ACEITE_PROCESSO_EXECU", + "SISTEMA_ORIGEM", "SITUACAO_SISTEMA", "VALOR_LICITACAO", + "DATA_ANALISE_ACEITE", "DATA_ENVIO_ANALISE", + ], + }, + { + "nome_tabela": "ingresso_contrapartida", + "nome_csv": "siconv_ingresso_contrapartida.csv", + "conflict_fields": ["nr_convenio", "dt_ingresso_contrapartida"], + "primary_key": ["nr_convenio", "dt_ingresso_contrapartida"], + "truncate_before_insert": False, + "skip_rows": 0, + "colunas": [ + "NR_CONVENIO", "DT_INGRESSO_CONTRAPARTIDA", "VL_INGRESSO_CONTRAPARTIDA", + ], + }, + { + "nome_tabela": "empenho", + "nome_csv": "siconv_empenho.csv", + "conflict_fields": [], + "primary_key": [], + "truncate_before_insert": True, + "skip_rows": 0, + "colunas": [ + "ID_EMPENHO", "NR_CONVENIO", "NR_EMPENHO", "TIPO_NOTA", "DESC_TIPO_NOTA", + "DATA_EMISSAO", "COD_SITUACAO_EMPENHO", "DESC_SITUACAO_EMPENHO", + "UG_EMITENTE", "UG_RESPONSAVEL", "FONTE_RECURSO", "NATUREZA_DESPESA", + "PLANO_INTERNO", "PTRES", "VALOR_EMPENHO", "RESULTADO_PRIMARIO", + "OBSERVACAO_EMPENHO", "DESCRICAO_EMENDA_SIAFI", + ], + }, + { + "nome_tabela": "historico_situacao", + "nome_csv": "siconv_historico_situacao.csv", + "conflict_fields": [], + "primary_key": [], + "truncate_before_insert": True, + "skip_rows": 0, + "colunas": [ + "ID_PROPOSTA", "NR_CONVENIO", "DIA_HISTORICO_SIT", "HISTORICO_SIT", + "DIAS_HISTORICO_SIT", "COD_HISTORICO_SIT", + ], + }, + { + "nome_tabela": "cronograma_desembolso", + "nome_csv": "siconv_cronograma_desembolso.csv", + "conflict_fields": ["id_proposta", "nr_convenio", "nr_parcela_crono_desembolso"], + "primary_key": ["id_proposta", "nr_convenio", "nr_parcela_crono_desembolso"], + "truncate_before_insert": False, + "skip_rows": 0, + "colunas": [ + "ID_PROPOSTA", "NR_CONVENIO", "NR_PARCELA_CRONO_DESEMBOLSO", + "MES_CRONO_DESEMBOLSO", "ANO_CRONO_DESEMBOLSO", "TIPO_RESP_CRONO_DESEMBOLSO", + "VALOR_PARCELA_CRONO_DESEMBOLSO", + ], + }, + { + "nome_tabela": "meta_crono_fisico", + "nome_csv": "siconv_meta_crono_fisico.csv", + "conflict_fields": ["id_meta"], + "primary_key": ["id_meta"], + "truncate_before_insert": False, + "skip_rows": 0, + "colunas": [ + "ID_META", "ID_PROPOSTA", "NR_CONVENIO", "COD_PROGRAMA", "NOME_PROGRAMA", + "NR_META", "TIPO_META", "DESC_META", "DATA_INICIO_META", "DATA_FIM_META", + "UF_META", "MUNICIPIO_META", "ENDERECO_META", "CEP_META", + "QTD_META", "UND_FORNECIMENTO_META", "VL_META", + ], + }, +] \ No newline at end of file diff --git a/airflow_lappis/templates/siape/consultaDadosAfastamento.xml.j2 b/airflow_lappis/templates/siape/consultaDadosAfastamento.xml.j2 new file mode 100644 index 00000000..03ef212c --- /dev/null +++ b/airflow_lappis/templates/siape/consultaDadosAfastamento.xml.j2 @@ -0,0 +1,15 @@ + + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + {{ parmExistPag }} + {{ parmTipoVinculo }} + + + diff --git a/airflow_lappis/templates/siape/consultaDadosAfastamentoHistorico.xml.j2 b/airflow_lappis/templates/siape/consultaDadosAfastamentoHistorico.xml.j2 new file mode 100644 index 00000000..2552e80f --- /dev/null +++ b/airflow_lappis/templates/siape/consultaDadosAfastamentoHistorico.xml.j2 @@ -0,0 +1,19 @@ + + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + {{ parmExistPag }} + {{ parmTipoVinculo }} + {{ anoInicial }} + {{ mesInicial }} + {{ anoFinal }} + {{ mesFinal }} + + + \ No newline at end of file diff --git a/airflow_lappis/templates/siape/consultaDadosCurriculo.xml.j2 b/airflow_lappis/templates/siape/consultaDadosCurriculo.xml.j2 new file mode 100644 index 00000000..74937137 --- /dev/null +++ b/airflow_lappis/templates/siape/consultaDadosCurriculo.xml.j2 @@ -0,0 +1,15 @@ + + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + {{ parmExistPag }} + {{ parmTipoVinculo }} + + + diff --git a/airflow_lappis/templates/siape/consultaDadosDependentes.xml.j2 b/airflow_lappis/templates/siape/consultaDadosDependentes.xml.j2 new file mode 100644 index 00000000..486ad749 --- /dev/null +++ b/airflow_lappis/templates/siape/consultaDadosDependentes.xml.j2 @@ -0,0 +1,15 @@ + + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + {{ parmExistPag }} + {{ parmTipoVinculo }} + + + diff --git a/airflow_lappis/templates/siape/consultaDadosEscolares.xml.j2 b/airflow_lappis/templates/siape/consultaDadosEscolares.xml.j2 new file mode 100644 index 00000000..6b4409cd --- /dev/null +++ b/airflow_lappis/templates/siape/consultaDadosEscolares.xml.j2 @@ -0,0 +1,15 @@ + + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + {{ parmExistPag }} + {{ parmTipoVinculo }} + + + diff --git a/airflow_lappis/templates/siape/consultaDadosFinanceiros.xml.j2 b/airflow_lappis/templates/siape/consultaDadosFinanceiros.xml.j2 new file mode 100644 index 00000000..39d3ee25 --- /dev/null +++ b/airflow_lappis/templates/siape/consultaDadosFinanceiros.xml.j2 @@ -0,0 +1,15 @@ + + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + {{ parmExistPag }} + {{ parmTipoVinculo }} + + + diff --git a/airflow_lappis/templates/siape/consultaDadosFuncionais.xml.j2 b/airflow_lappis/templates/siape/consultaDadosFuncionais.xml.j2 new file mode 100644 index 00000000..6d66114b --- /dev/null +++ b/airflow_lappis/templates/siape/consultaDadosFuncionais.xml.j2 @@ -0,0 +1,14 @@ + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + {{ parmExistPag }} + {{ parmTipoVinculo }} + + + diff --git a/airflow_lappis/templates/siape/consultaDadosPA.xml.j2 b/airflow_lappis/templates/siape/consultaDadosPA.xml.j2 new file mode 100644 index 00000000..9c8e5983 --- /dev/null +++ b/airflow_lappis/templates/siape/consultaDadosPA.xml.j2 @@ -0,0 +1,15 @@ + + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + {{ parmExistPag }} + {{ parmTipoVinculo }} + + + diff --git a/airflow_lappis/templates/siape/consultaDadosPessoais.xml.j2 b/airflow_lappis/templates/siape/consultaDadosPessoais.xml.j2 new file mode 100644 index 00000000..d0e8c0e5 --- /dev/null +++ b/airflow_lappis/templates/siape/consultaDadosPessoais.xml.j2 @@ -0,0 +1,15 @@ + + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + {{ parmExistPag }} + {{ parmTipoVinculo }} + + + diff --git a/airflow_lappis/templates/siape/consultaDadosUorg.xml.j2 b/airflow_lappis/templates/siape/consultaDadosUorg.xml.j2 new file mode 100644 index 00000000..a9854c08 --- /dev/null +++ b/airflow_lappis/templates/siape/consultaDadosUorg.xml.j2 @@ -0,0 +1,15 @@ + + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + {{ parmExistPag }} + {{ parmTipoVinculo }} + + + diff --git a/airflow_lappis/templates/siape/consultaPensoesInstituidas.xml.j2 b/airflow_lappis/templates/siape/consultaPensoesInstituidas.xml.j2 new file mode 100644 index 00000000..d138d88c --- /dev/null +++ b/airflow_lappis/templates/siape/consultaPensoesInstituidas.xml.j2 @@ -0,0 +1,15 @@ + + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + {{ parmExistPag }} + {{ parmTipoVinculo }} + + + diff --git a/airflow_lappis/templates/siape/listaInformacoesAposentadoria.xml.j2 b/airflow_lappis/templates/siape/listaInformacoesAposentadoria.xml.j2 new file mode 100644 index 00000000..720dfbd1 --- /dev/null +++ b/airflow_lappis/templates/siape/listaInformacoesAposentadoria.xml.j2 @@ -0,0 +1,14 @@ + + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ orgao }} + {{ matricula }} + + + diff --git a/airflow_lappis/templates/siape/listaServidores.xml.j2 b/airflow_lappis/templates/siape/listaServidores.xml.j2 new file mode 100644 index 00000000..0388e724 --- /dev/null +++ b/airflow_lappis/templates/siape/listaServidores.xml.j2 @@ -0,0 +1,14 @@ + + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + {{ codUorg }} + + + diff --git a/airflow_lappis/templates/siape/listaUorgs.xml.j2 b/airflow_lappis/templates/siape/listaUorgs.xml.j2 new file mode 100644 index 00000000..d17ea764 --- /dev/null +++ b/airflow_lappis/templates/siape/listaUorgs.xml.j2 @@ -0,0 +1,13 @@ + + + + + {{ siglaSistema }} + {{ nomeSistema }} + {{ senha }} + {{ cpf }} + {{ codOrgao }} + 00000000 + + + diff --git a/dbt_project.yml b/dbt_project.yml deleted file mode 100644 index c957d047..00000000 --- a/dbt_project.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: 'airflow_dbt_project' -version: '1.0.0' - -profile: 'airflow_dbt_project' - -model-paths: ["models"] -analysis-paths: ["analyses"] -test-paths: ["tests"] -seed-paths: ["seeds"] -macro-paths: ["macros"] -snapshot-paths: ["snapshots"] - -clean-targets: - - "target" - - "dbt_packages" - - -models: - airflow_dbt_project: - silver: - +schema: silver - +materialized: table - gold: - +schema: gold - +materialized: table diff --git a/docker-compose.yml b/docker-compose.yml index 45e14fb2..b9249648 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - # Base configuration for Airflow services x-airflow-common: &airflow-common build: . @@ -7,8 +5,11 @@ x-airflow-common: &airflow-common env_file: - .env volumes: - - ./dags:${AIRFLOW_HOME}/dags/ - - ./plugins:${AIRFLOW_HOME}/plugins/ + - ./airflow_lappis/airflow.cfg:${AIRFLOW_HOME}/airflow.cfg + - ./airflow_lappis/dags:${AIRFLOW_HOME}/dags/ + - ./airflow_lappis/plugins:${AIRFLOW_HOME}/plugins/ + - ./airflow_lappis/helpers:${AIRFLOW_HOME}/helpers/ + - ./airflow_lappis/dags/dbt/ipea/profiles.yml:${AIRFLOW_HOME}/.dbt/profiles.yml depends_on: &airflow-common-depends-on postgres: condition: service_healthy @@ -30,13 +31,39 @@ x-airflow-environment: &airflow-common-env AIRFLOW__WEBSERVER__NAVBAR_COLOR: '#98DFFF' AIRFLOW__WEBSERVER__RELOAD_ON_PLUGIN_CHANGE: 'true' AIRFLOW__WEBSERVER__SECRET_KEY: '42' - PYTHONPATH: '${AIRFLOW_HOME}/dags:${AIRFLOW_HOME}/plugins' + PYTHONPATH: "/opt/airflow/dags:/opt/airflow/plugins:/opt/airflow/helpers:$PYTHONPATH" _AIRFLOW_DB_MIGRATE: 'true' _AIRFLOW_WWW_USER_CREATE: 'true' _AIRFLOW_WWW_USER_USERNAME: ${_AIRFLOW_WWW_USER_USERNAME:-airflow} _AIRFLOW_WWW_USER_PASSWORD: ${_AIRFLOW_WWW_USER_PASSWORD:-airflow} - + AIRFLOW__CORE__PLUGINS_FOLDER: ${AIRFLOW_HOME}/plugins + AIRFLOW__CORE__DAGS_FOLDER: ${AIRFLOW_HOME}/dags + AIRFLOW_REPO_BASE: ${AIRFLOW_HOME} + # A ENV AIRFLOW_REPO_BASE É IMPORTANTE PARA SINCRONIZAR COM O SISTEMA DE PASTAS + # DO AIRFLOW EM HOMOLOG E PROD, ELES POSSUEM UMA ESTRUTURA DE PASTAS DIFERENTE + # USAR ESSA ENV NOS CÓDIGOS PARA NÃO HAVER CONFLITOS + services: + # Airflow Services + airflow: + <<: *airflow-common + # Usamos o comando de standalone aqui para não rodar múltiplos containers + # do airflow localmente e assim melhorar a velocidade inicialização + # doc do standalone: https://airflow.apache.org/docs/apache-airflow/2.8.1/start.html + command: standalone + ports: + - "8080:8080" + healthcheck: + test: [ "CMD", "curl", "--fail", "http://localhost:8080/health" ] + interval: 10s + timeout: 60s + start_period: 60s + retries: 5 + restart: always + environment: + <<: *airflow-common-env + + # Postgres database postgres: image: postgres:15-alpine env_file: @@ -57,28 +84,11 @@ services: retries: 5 restart: always - # Airflow Services - airflow: - <<: *airflow-common - # Usamos o comando de standalone aqui para não rodar múltiplos containers - # do airflow localmente e assim melhorar a velocidade inicialização - # doc do standalone: https://airflow.apache.org/docs/apache-airflow/2.8.1/start.html - command: standalone - ports: - - "8080:8080" - healthcheck: - test: ["CMD", "curl", "--fail", "http://localhost:8080/health"] - interval: 10s - timeout: 60s - start_period: 60s - retries: 5 - restart: always - environment: - <<: *airflow-common-env - # Analytics Tools superset: - image: apache/superset:latest + build: + context: . + dockerfile: Dockerfile.superset # Custom Dockerfile with PostgreSQL drivers environment: SUPERSET_SECRET_KEY: 'supersetadmin' SUPERSET_ENV: development @@ -87,6 +97,12 @@ services: - "8088:8088" depends_on: - postgres + restart: always + healthcheck: + test: ["CMD", "curl", "--fail", "http://localhost:8088/health"] + interval: 30s + timeout: 10s + retries: 3 command: > /bin/sh -c " superset fab create-admin --username admin --firstname Admin --lastname User --email admin@superset.com --password admin && @@ -114,4 +130,4 @@ services: volumes: postgres-db: - airflow_logs: + airflow_logs: \ No newline at end of file diff --git a/.env b/local.env old mode 100755 new mode 100644 similarity index 100% rename from .env rename to local.env diff --git a/profiles.yml b/profiles.yml deleted file mode 100644 index 23dea3f0..00000000 --- a/profiles.yml +++ /dev/null @@ -1,11 +0,0 @@ -default: - outputs: - dev: - type: postgres - host: 10.0.0.73 - user: analytics - password: xQ3hxNJThsVx3WqEp6Yr0hhtptSMbmbFWyL2 - port: 5432 - dbname: analytics - schema: public - target: dev diff --git a/pyproject.toml b/pyproject.toml index 025e8920..2dca6ad5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [tool.poetry] -name = "app_lappis_ipea" +package-mode = false +name = "lappis" version = "0.1.0" description = "Gestão de Dados do IPEA - Aplicações Python" authors = ["Lappis UNB"] @@ -8,11 +9,20 @@ readme = "README.md" [tool.poetry.dependencies] python = "~3.11" apache-airflow = "2.8.1" +apache-airflow-providers-postgres = "5.14.0" +apache-airflow-providers-microsoft-mssql = "3.7.2" +flask-session = "0.5.0" +numpy = "1.26.4" +flask = "*" dbt-core = "*" dbt-postgres = "*" pandas = "*" requests = "*" sqlalchemy = "*" +zeep = "*" +imap-tools = "*" +astronomer-cosmos = "*" +openpyxl = "^3.1.5" [tool.poetry.group.dev.dependencies] black = "*" @@ -22,10 +32,28 @@ pytest-cov = "*" mypy = "*" types-psycopg2 = "*" pandas-stubs = "*" +sqlalchemy-stubs = "*" jupyter = "*" shandy-sqlfmt = "*" sqlfluff = "*" sqlfluff-templater-dbt = "*" +types-Authlib = "*" +types-Deprecated = "*" +types-Pygments = "*" +types-WTForms = "*" +types-croniter = "*" +types-gevent = "*" +types-jmespath = "*" +types-jsonschema = "*" +types-openpyxl = "*" +types-psutil = "*" +types-tabulate = "*" +types-colorama = "*" +types-decorator = "*" +types-passlib = "*" +types-pycurl = "*" +types-simplejson = "*" +types-uWSGI = "*" [build-system] requires = ["poetry-core"] @@ -43,6 +71,7 @@ exclude = ''' | \.mypy_cache | \.tox | \.venv + | \.conda | _build | buck-out | build @@ -70,6 +99,7 @@ ignore = [] [tool.mypy] python_version = "3.11" +files = ["**/*.py"] warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true @@ -81,10 +111,13 @@ warn_redundant_casts = true warn_unused_ignores = true warn_no_return = true warn_unreachable = true +follow_untyped_imports = true +disable_error_code = ["arg-type", "attr-defined"] [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] +pythonpath = ["airflow_lappis", "airflow_lappis/helpers", "airflow_lappis/plugins"] [tool.sqlfmt] line_length = 90 @@ -94,18 +127,23 @@ exclude = [ ".venv/", "build/", "dist/", + "**/target/**", + "**/compiled/**" ] [tool.sqlfluff.core] dialect = "postgres" templater = "dbt" max_line_length = 90 -exclude_rules = "L016" +exclude_rules = "L016,LT02,LT09,LT14,RF02,AL04,CV08" +verbose = 1 [tool.sqlfluff.indentation] indented_joins = true indented_using_on = true template_blocks_indent = true +indent_unit = "space" +tab_space_size = 2 [tool.sqlfluff.rules.capitalisation.keywords] capitalisation_policy = "lower" @@ -123,7 +161,10 @@ capitalisation_policy = "lower" unwrap_wrapped_queries = true [tool.sqlfluff.templater.dbt] -project_dir = "." -profiles_dir = "." -profile = "default" -target = "dev" +project_dir = "./airflow_lappis/dags/dbt/ipea" +profiles_dir = "./airflow_lappis/dags/dbt/ipea" +profile = "ipea" +target = "prod" +defer_mode = true +static_analysis = true +disable_database_connection = true diff --git a/requirements.txt b/requirements.txt index f7ebb449..5f417239 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,157 +1,8 @@ -agate==1.9.1 ; python_version >= "3.11" and python_version < "3.12" -aiohappyeyeballs==2.4.4 ; python_version >= "3.11" and python_version < "3.12" -aiohttp==3.10.11 ; python_version >= "3.11" and python_version < "3.12" -aiosignal==1.3.2 ; python_version >= "3.11" and python_version < "3.12" -alembic==1.14.0 ; python_version >= "3.11" and python_version < "3.12" -annotated-types==0.7.0 ; python_version >= "3.11" and python_version < "3.12" -anyio==4.8.0 ; python_version >= "3.11" and python_version < "3.12" -apache-airflow-providers-common-io==1.4.2 ; python_version >= "3.11" and python_version < "3.12" -apache-airflow-providers-common-sql==1.20.0 ; python_version >= "3.11" and python_version < "3.12" -apache-airflow-providers-ftp==3.11.1 ; python_version >= "3.11" and python_version < "3.12" -apache-airflow-providers-http==4.13.3 ; python_version >= "3.11" and python_version < "3.12" -apache-airflow-providers-imap==3.7.0 ; python_version >= "3.11" and python_version < "3.12" -apache-airflow-providers-sqlite==3.9.1 ; python_version >= "3.11" and python_version < "3.12" -apache-airflow==2.8.1 ; python_version >= "3.11" and python_version < "3.12" -apispec[yaml]==6.8.1 ; python_version >= "3.11" and python_version < "3.12" -argcomplete==3.5.3 ; python_version >= "3.11" and python_version < "3.12" -asgiref==3.8.1 ; python_version >= "3.11" and python_version < "3.12" -attrs==24.3.0 ; python_version >= "3.11" and python_version < "3.12" -babel==2.16.0 ; python_version >= "3.11" and python_version < "3.12" -blinker==1.9.0 ; python_version >= "3.11" and python_version < "3.12" -cachelib==0.9.0 ; python_version >= "3.11" and python_version < "3.12" -certifi==2024.12.14 ; python_version >= "3.11" and python_version < "3.12" -cffi==1.17.1 ; python_version >= "3.11" and python_version < "3.12" and platform_python_implementation != "PyPy" -charset-normalizer==3.4.1 ; python_version >= "3.11" and python_version < "3.12" -click==8.1.8 ; python_version >= "3.11" and python_version < "3.12" -clickclick==20.10.2 ; python_version >= "3.11" and python_version < "3.12" -colorama==0.4.6 ; python_version >= "3.11" and python_version < "3.12" -colorlog==4.8.0 ; python_version >= "3.11" and python_version < "3.12" -configupdater==3.2 ; python_version >= "3.11" and python_version < "3.12" -connexion[flask]==2.14.1 ; python_version >= "3.11" and python_version < "3.12" -cron-descriptor==1.4.5 ; python_version >= "3.11" and python_version < "3.12" -croniter==6.0.0 ; python_version >= "3.11" and python_version < "3.12" -cryptography==44.0.0 ; python_version >= "3.11" and python_version < "3.12" -daff==1.3.46 ; python_version >= "3.11" and python_version < "3.12" -dbt-adapters==1.13.0 ; python_version >= "3.11" and python_version < "3.12" -dbt-common==1.14.0 ; python_version >= "3.11" and python_version < "3.12" -dbt-core==1.9.1 ; python_version >= "3.11" and python_version < "3.12" -dbt-extractor==0.5.1 ; python_version >= "3.11" and python_version < "3.12" -dbt-postgres==1.9.0 ; python_version >= "3.11" and python_version < "3.12" -dbt-semantic-interfaces==0.7.4 ; python_version >= "3.11" and python_version < "3.12" -deepdiff==7.0.1 ; python_version >= "3.11" and python_version < "3.12" -deprecated==1.2.15 ; python_version >= "3.11" and python_version < "3.12" -dill==0.3.9 ; python_version >= "3.11" and python_version < "3.12" -dnspython==2.7.0 ; python_version >= "3.11" and python_version < "3.12" -email-validator==1.3.1 ; python_version >= "3.11" and python_version < "3.12" -flask-appbuilder==4.3.10 ; python_version >= "3.11" and python_version < "3.12" -flask-babel==2.0.0 ; python_version >= "3.11" and python_version < "3.12" -flask-caching==2.3.0 ; python_version >= "3.11" and python_version < "3.12" -flask-jwt-extended==4.7.1 ; python_version >= "3.11" and python_version < "3.12" -flask-limiter==3.10.0 ; python_version >= "3.11" and python_version < "3.12" -flask-login==0.6.3 ; python_version >= "3.11" and python_version < "3.12" -flask-session==0.8.0 ; python_version >= "3.11" and python_version < "3.12" -flask-sqlalchemy==2.5.1 ; python_version >= "3.11" and python_version < "3.12" -flask-wtf==1.2.2 ; python_version >= "3.11" and python_version < "3.12" -flask==2.2.5 ; python_version >= "3.11" and python_version < "3.12" -frozenlist==1.5.0 ; python_version >= "3.11" and python_version < "3.12" -fsspec==2024.12.0 ; python_version >= "3.11" and python_version < "3.12" -google-re2==1.1.20240702 ; python_version >= "3.11" and python_version < "3.12" -googleapis-common-protos==1.66.0 ; python_version >= "3.11" and python_version < "3.12" -greenlet==3.1.1 ; python_version >= "3.11" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and python_version < "3.12" -grpcio==1.69.0 ; python_version >= "3.11" and python_version < "3.12" -gunicorn==23.0.0 ; python_version >= "3.11" and python_version < "3.12" -h11==0.14.0 ; python_version >= "3.11" and python_version < "3.12" -httpcore==1.0.7 ; python_version >= "3.11" and python_version < "3.12" -httpx==0.28.1 ; python_version >= "3.11" and python_version < "3.12" -idna==3.10 ; python_version >= "3.11" and python_version < "3.12" -importlib-metadata==6.11.0 ; python_version >= "3.11" and python_version < "3.12" -inflection==0.5.1 ; python_version >= "3.11" and python_version < "3.12" -isodate==0.6.1 ; python_version >= "3.11" and python_version < "3.12" -itsdangerous==2.2.0 ; python_version >= "3.11" and python_version < "3.12" -jinja2==3.1.5 ; python_version >= "3.11" and python_version < "3.12" -jsonschema-specifications==2024.10.1 ; python_version >= "3.11" and python_version < "3.12" -jsonschema==4.23.0 ; python_version >= "3.11" and python_version < "3.12" -lazy-object-proxy==1.10.0 ; python_version >= "3.11" and python_version < "3.12" -leather==0.4.0 ; python_version >= "3.11" and python_version < "3.12" -limits==4.0.0 ; python_version >= "3.11" and python_version < "3.12" -linkify-it-py==2.0.3 ; python_version >= "3.11" and python_version < "3.12" -lockfile==0.12.2 ; python_version >= "3.11" and python_version < "3.12" -mako==1.3.8 ; python_version >= "3.11" and python_version < "3.12" -markdown-it-py==3.0.0 ; python_version >= "3.11" and python_version < "3.12" -markdown==3.7 ; python_version >= "3.11" and python_version < "3.12" -markupsafe==3.0.2 ; python_version >= "3.11" and python_version < "3.12" -marshmallow-oneofschema==3.1.1 ; python_version >= "3.11" and python_version < "3.12" -marshmallow-sqlalchemy==0.26.1 ; python_version >= "3.11" and python_version < "3.12" -marshmallow==3.25.0 ; python_version >= "3.11" and python_version < "3.12" -mashumaro[msgpack]==3.14 ; python_version >= "3.11" and python_version < "3.12" -mdit-py-plugins==0.4.2 ; python_version >= "3.11" and python_version < "3.12" -mdurl==0.1.2 ; python_version >= "3.11" and python_version < "3.12" -more-itertools==10.5.0 ; python_version >= "3.11" and python_version < "3.12" -msgpack==1.1.0 ; python_version >= "3.11" and python_version < "3.12" -msgspec==0.19.0 ; python_version >= "3.11" and python_version < "3.12" -multidict==6.1.0 ; python_version >= "3.11" and python_version < "3.12" -networkx==3.4.2 ; python_version >= "3.11" and python_version < "3.12" -numpy==2.2.1 ; python_version == "3.11" -opentelemetry-api==1.29.0 ; python_version >= "3.11" and python_version < "3.12" -opentelemetry-exporter-otlp-proto-common==1.29.0 ; python_version >= "3.11" and python_version < "3.12" -opentelemetry-exporter-otlp-proto-grpc==1.29.0 ; python_version >= "3.11" and python_version < "3.12" -opentelemetry-exporter-otlp-proto-http==1.29.0 ; python_version >= "3.11" and python_version < "3.12" -opentelemetry-exporter-otlp==1.29.0 ; python_version >= "3.11" and python_version < "3.12" -opentelemetry-proto==1.29.0 ; python_version >= "3.11" and python_version < "3.12" -opentelemetry-sdk==1.29.0 ; python_version >= "3.11" and python_version < "3.12" -opentelemetry-semantic-conventions==0.50b0 ; python_version >= "3.11" and python_version < "3.12" -ordered-set==4.1.0 ; python_version >= "3.11" and python_version < "3.12" -packaging==24.2 ; python_version >= "3.11" and python_version < "3.12" -pandas==2.2.3 ; python_version >= "3.11" and python_version < "3.12" -parsedatetime==2.6 ; python_version >= "3.11" and python_version < "3.12" -pathspec==0.12.1 ; python_version >= "3.11" and python_version < "3.12" -pendulum==3.0.0 ; python_version >= "3.11" and python_version < "3.12" -pluggy==1.5.0 ; python_version >= "3.11" and python_version < "3.12" -prison==0.2.1 ; python_version >= "3.11" and python_version < "3.12" -propcache==0.2.1 ; python_version >= "3.11" and python_version < "3.12" -protobuf==5.29.3 ; python_version >= "3.11" and python_version < "3.12" -psutil==6.1.1 ; python_version >= "3.11" and python_version < "3.12" -psycopg2-binary==2.9.10 ; python_version >= "3.11" and python_version < "3.12" -pycparser==2.22 ; python_version >= "3.11" and python_version < "3.12" and platform_python_implementation != "PyPy" -pydantic-core==2.27.2 ; python_version >= "3.11" and python_version < "3.12" -pydantic==2.10.5 ; python_version >= "3.11" and python_version < "3.12" -pygments==2.19.1 ; python_version >= "3.11" and python_version < "3.12" -pyjwt==2.10.1 ; python_version >= "3.11" and python_version < "3.12" -python-daemon==3.1.2 ; python_version >= "3.11" and python_version < "3.12" -python-dateutil==2.9.0.post0 ; python_version >= "3.11" and python_version < "3.12" -python-nvd3==0.16.0 ; python_version >= "3.11" and python_version < "3.12" -python-slugify==8.0.4 ; python_version >= "3.11" and python_version < "3.12" -pytimeparse==1.1.8 ; python_version >= "3.11" and python_version < "3.12" -pytz==2024.2 ; python_version >= "3.11" and python_version < "3.12" -pyyaml==6.0.2 ; python_version >= "3.11" and python_version < "3.12" -referencing==0.35.1 ; python_version >= "3.11" and python_version < "3.12" -requests-toolbelt==1.0.0 ; python_version >= "3.11" and python_version < "3.12" -requests==2.32.3 ; python_version >= "3.11" and python_version < "3.12" -rfc3339-validator==0.1.4 ; python_version >= "3.11" and python_version < "3.12" -rich-argparse==1.6.0 ; python_version >= "3.11" and python_version < "3.12" -rich==13.9.4 ; python_version >= "3.11" and python_version < "3.12" -rpds-py==0.22.3 ; python_version >= "3.11" and python_version < "3.12" -setproctitle==1.3.4 ; python_version >= "3.11" and python_version < "3.12" -six==1.17.0 ; python_version >= "3.11" and python_version < "3.12" -sniffio==1.3.1 ; python_version >= "3.11" and python_version < "3.12" -snowplow-tracker==1.0.4 ; python_version >= "3.11" and python_version < "3.12" -sqlalchemy-jsonfield==1.0.2 ; python_version >= "3.11" and python_version < "3.12" -sqlalchemy-utils==0.41.2 ; python_version >= "3.11" and python_version < "3.12" -sqlalchemy==1.4.54 ; python_version >= "3.11" and python_version < "3.12" -sqlparse==0.5.3 ; python_version >= "3.11" and python_version < "3.12" -tabulate==0.9.0 ; python_version >= "3.11" and python_version < "3.12" -tenacity==9.0.0 ; python_version >= "3.11" and python_version < "3.12" -termcolor==2.5.0 ; python_version >= "3.11" and python_version < "3.12" -text-unidecode==1.3 ; python_version >= "3.11" and python_version < "3.12" -types-requests==2.32.0.20241016 ; python_version >= "3.11" and python_version < "3.12" -typing-extensions==4.12.2 ; python_version >= "3.11" and python_version < "3.12" -tzdata==2024.2 ; python_version >= "3.11" and python_version < "3.12" -uc-micro-py==1.0.3 ; python_version >= "3.11" and python_version < "3.12" -unicodecsv==0.14.1 ; python_version >= "3.11" and python_version < "3.12" -universal-pathlib==0.2.6 ; python_version >= "3.11" and python_version < "3.12" -urllib3==2.3.0 ; python_version >= "3.11" and python_version < "3.12" -werkzeug==2.3.8 ; python_version >= "3.11" and python_version < "3.12" -wrapt==1.17.0 ; python_version >= "3.11" and python_version < "3.12" -wtforms==3.2.1 ; python_version >= "3.11" and python_version < "3.12" -yarl==1.18.3 ; python_version >= "3.11" and python_version < "3.12" -zipp==3.21.0 ; python_version >= "3.11" and python_version < "3.12" +collate-sqllineage==1.6.0 +sqlparse==0.5 +astronomer-cosmos==1.9.0 +dbt-postgres==1.7.13 +imap-tools==1.10.0 +zeep==4.3.1 +apache-airflow-providers-microsoft-mssql==3.7.2 +openpyxl \ No newline at end of file diff --git a/setup-git-hooks.sh b/setup-git-hooks.sh index cb9dbcf7..21c1234c 100644 --- a/setup-git-hooks.sh +++ b/setup-git-hooks.sh @@ -17,7 +17,7 @@ cat > .git/hooks/pre-push << 'EOF' #!/bin/bash set -e echo "Running pre-push checks..." -make lint +make lint -e GITLAB_CI=TRUE make test echo -e "\033[0;32mPre-push checks passed!\033[0m" exit 0 diff --git a/src/airflow/dags/__init__.py b/src/airflow/dags/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/airflow/plugins/__init__.py b/src/airflow/plugins/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/airflow/plugins/cliente_base.py b/src/airflow/plugins/cliente_base.py deleted file mode 100644 index a0892050..00000000 --- a/src/airflow/plugins/cliente_base.py +++ /dev/null @@ -1,51 +0,0 @@ -import http -import logging -import time -from http import HTTPStatus -from typing import Tuple, Optional, Any - -import httpx - - -class ClienteBase(object): - - DEFAULT_MAX_RETRIES = 5 - DEFAULT_SLEEP_SECONDS = 1 - DEFAULT_TIMEOUT = 30 - - def __init__(self, base_url: str) -> None: - self.base_url = base_url - self.client = httpx.Client(base_url=base_url) - - def request( - self, method: http.HTTPMethod, path: str, **kwargs: Any - ) -> Tuple[http.HTTPStatus, Optional[dict | list]]: - """ - Faz uma requisição HTTP em até DEFAULT_MAX_RETRIES+1 tentativas. - - Args: - method (HTTPMethod): HTTP Method. - path (str): URL path. - - Returns: - Tuple[http.HTTPStatus, dict]: status e resposta da requisição HTTP. - """ - kwargs["timeout"] = kwargs.get("timeout", self.DEFAULT_TIMEOUT) - response = None - - for attempt in range(self.DEFAULT_MAX_RETRIES): - try: - response = self.client.request(method, path, **kwargs) - response.raise_for_status() - return HTTPStatus(response.status_code), response.json() - except httpx.HTTPError as e: - if attempt < self.DEFAULT_MAX_RETRIES: - status = response.status_code if response else "Unknown" - logging.warning(f"API falhou com status {status}") - time.sleep(attempt**2 * self.DEFAULT_SLEEP_SECONDS) - else: - raise Exception( - "API falhou após o número máximo de tentativas!" - ) from e - - return HTTPStatus.INTERNAL_SERVER_ERROR, None diff --git a/src/airflow/plugins/cliente_contratos.py b/src/airflow/plugins/cliente_contratos.py deleted file mode 100644 index 5f01a619..00000000 --- a/src/airflow/plugins/cliente_contratos.py +++ /dev/null @@ -1,76 +0,0 @@ -import http - -from .cliente_base import ClienteBase - - -class ClienteContratos(ClienteBase): - - BASE_URL = "https://contratos.comprasnet.gov.br/api" - BASE_HEADER = {"accept": "application/json"} - - def __init__(self) -> None: - super().__init__(base_url=ClienteContratos.BASE_URL) - - def get_contratos_by_ug(self, ug_code: str) -> list | None: - """ - Obter todos os contratos ativos de uma UG específica. - - Args: - ug_code (str): UG code - - Returns: - list: lista de contratos por ug - """ - endpoint = f"/contrato/ug/{ug_code}" - status, data = self.request( - http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER - ) - return data if status == http.HTTPStatus.OK and type(data) is list else None - - def get_faturas_by_contrato_id(self, contrato_id: str) -> list | None: - """ - Obter todas as faturas de um contrato específico. - - Args: - contrato_id (str): id do contrato - - Returns: - list: as faturas de um contrato específico. - """ - endpoint = f"/contrato/{contrato_id}/faturas" - status, data = self.request( - http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER - ) - return data if status == http.HTTPStatus.OK and type(data) is list else None - - def get_empenhos_by_contrato_id(self, contrato_id: str) -> list | None: - """ - Obter todos os empenhos de um contrato específico. - - Args: - contrato_id (str): id do contrato - - Returns: - list: os empenhos de um contrato específico. - """ - endpoint = f"/contrato/{contrato_id}/empenhos" - status, data = self.request( - http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER - ) - return data if status == http.HTTPStatus.OK and type(data) is list else None - - def get_cronograma_by_contrato_id(self, contrato_id: str) -> list | None: - """ - Obter todos os cronogramas de um contrato específico. - - Args: - contrato_id (str): id do contrato - - Returns: - list: cronogramas de um contrato específico. - """ - endpoint = f"/contrato/{contrato_id}/cronograma" - status, data = self.request( - http.HTTPMethod.GET, endpoint, headers=self.BASE_HEADER - ) - return data if status == http.HTTPStatus.OK and type(data) is list else None diff --git a/src/airflow/plugins/cliente_estrutura.py b/src/airflow/plugins/cliente_estrutura.py deleted file mode 100644 index f0f19b67..00000000 --- a/src/airflow/plugins/cliente_estrutura.py +++ /dev/null @@ -1,45 +0,0 @@ -import http -from typing import Optional - -from .cliente_base import ClienteBase - - -class ClienteEstrutura(ClienteBase): - - BASE_URL = "https://estruturaorganizacional.dados.gov.br/doc" - - def __init__(self) -> None: - super().__init__(base_url=ClienteEstrutura.BASE_URL) - - def get_estrutura_organizacional_resumida( - self, - codigo_poder: Optional[str] = None, - codigo_esfera: Optional[str] = None, - codigo_unidade: Optional[str] = None, - ) -> Optional[list]: - """ - Consultar Estrutura Organizacional Resumida. - - Args: - codigo_poder (Optional[str]): código do poder - codigo_esfera (Optional[str]): código da esfera - codigo_unidade (Optional[str]): código da unidade - - Returns: - dict: Estrutura Organizacional Resumida. - """ - endpoint = "/estrutura-organizacional/resumida" - params = {} - if codigo_poder: - params["codigoPoder"] = codigo_poder - if codigo_esfera: - params["codigoEsfera"] = codigo_esfera - if codigo_unidade: - params["codigoUnidade"] = codigo_unidade - - status, data = self.request(http.HTTPMethod.GET, endpoint, params=params) - return ( - data.get("unidades", []) - if status == http.HTTPStatus.OK and type(data) is dict - else None - ) diff --git a/src/airflow/plugins/cliente_postgres.py b/src/airflow/plugins/cliente_postgres.py deleted file mode 100644 index 10b766a0..00000000 --- a/src/airflow/plugins/cliente_postgres.py +++ /dev/null @@ -1,145 +0,0 @@ -from typing import Any, Dict, List, Optional, Tuple -import psycopg2 -from psycopg2.extras import execute_values -from pandas import json_normalize - - -class ClientPostgresDB: - """Client for interacting with PostgreSQL database.""" - - SEPARATOR = "_" - - TYPE_MAP = {int: "BIGINT", float: "NUMERIC", bool: "BOOLEAN"} - - @staticmethod - def _get_column_type(value: Any) -> str: - """ - Determine PostgreSQL column type from Python value. - - Args: - value: Python value to analyze - - Returns: - PostgreSQL column type as string - """ - return ClientPostgresDB.TYPE_MAP.get(type(value), "TEXT") - - @staticmethod - def _flatten_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """ - Flattens a list of deep dictionaries into a list of flat dictionaries. - - Args: - data (List[Dict[str, Any]]): List of dictionaries to flatten. - - Returns: - List[Dict[str, Any]]: List of flat dictionaries. - """ - return list( - map( - lambda d: { - str(k): v if type(v) is not list else str(v) for k, v in d.items() - }, - json_normalize(data, sep=ClientPostgresDB.SEPARATOR).to_dict( - orient="records" - ), - ) - ) - - def __init__(self, conn_str: str) -> None: - self.conn_str = conn_str - - def create_table_if_not_exists( - self, - sample_data: Dict[str, Any], - table_name: str, - primary_key: Optional[str] = None, - schema: str = "raw", - ) -> None: - """Create table dynamically based on data structure. - - Args: - sample_data: Sample data to determine schema - table_name: Name of table to create - primary_key: Primary key column name - schema: Database schema name - """ - with psycopg2.connect(self.conn_str) as conn: - with conn.cursor() as cursor: - cursor.execute(f"CREATE SCHEMA IF NOT EXISTS {schema};") - - flattened_sample = self._flatten_data([sample_data])[0] - column_definitions: List[str] = [] - - for column, value in flattened_sample.items(): - col_type = self._get_column_type(value) - column_definitions.append(f"{column} {col_type}") - - if primary_key and primary_key in flattened_sample: - column_definitions.append(f"PRIMARY KEY ({primary_key})") - - create_table_query = f""" - CREATE TABLE IF NOT EXISTS {schema}.{table_name} ( - {', '.join(column_definitions)} - );""" - - try: - cursor.execute(create_table_query) - except psycopg2.Error as err: - raise RuntimeError( - f"Failed to create table {schema}.{table_name}" - ) from err - - def insert_data( - self, - data: List[Dict[str, Any]], - table_name: str, - conflict_field: Optional[str] = None, - primary_key: Optional[str] = None, - schema: str = "raw", - ) -> None: - """Insert data into database table. - - Args: - data: List of dictionaries to insert - table_name: Target table name - conflict_field: Column name for conflict resolution - primary_key: Primary key column name - schema: Database schema name - """ - if not data: - return - - self.create_table_if_not_exists(data[0], table_name, primary_key=primary_key) - - flattened_data = self._flatten_data(data) - columns = list(flattened_data[0].keys()) - values = [tuple(item.values()) for item in flattened_data] - - sql = f""" - INSERT INTO {schema}.{table_name} ({', '.join(columns)}) - VALUES %s - """ - - if conflict_field: - sql += f" ON CONFLICT ({conflict_field}) DO NOTHING" - - with psycopg2.connect(self.conn_str) as conn: - with conn.cursor() as cursor: - try: - execute_values(cursor, sql, values) - except psycopg2.Error as err: - raise RuntimeError( - f"Failed to insert data into {table_name}" - ) from err - - def execute_query(self, query: str) -> List[Tuple[Any, ...]]: - """Get all contract IDs from contratos table. - - Returns: - List of contract IDs - """ - with psycopg2.connect(self.conn_str) as conn: - with conn.cursor() as cursor: - cursor.execute(query) - return cursor.fetchall() diff --git a/src/dbt/__init__.py b/src/dbt/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/jupyter/__init__.py b/src/jupyter/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/superset/__init__.py b/src/superset/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/airflow/__init__.py b/superset/__init__.py similarity index 100% rename from src/airflow/__init__.py rename to superset/__init__.py diff --git a/tests/test_cliente_mrv.py b/tests/test_cliente_mrv.py new file mode 100644 index 00000000..761dcc31 --- /dev/null +++ b/tests/test_cliente_mrv.py @@ -0,0 +1,54 @@ +from unittest.mock import patch, MagicMock +from http import HTTPStatus +import httpx +import pytest + +from airflow_lappis.plugins.cliente_mrv import ClienteMRV + +class TestClienteMRV: + @pytest.fixture + def cliente_mrv(self): + return ClienteMRV() + + @patch("httpx.Client.request") + def test_consultar_empreendimentos_sucesso(self, mock_request, cliente_mrv): + # Configura o mock para retornar uma resposta de sucesso + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = HTTPStatus.OK + mock_response.json.return_value = [{"id": 1, "nome": "Residencial Arvoredo", "cidade": "São Paulo"}] + mock_request.return_value = mock_response + + status, dados = cliente_mrv.consultar_empreendimentos(params={"cidade": "São Paulo"}) + + # Verifica se a chamada foi feita corretamente + mock_request.assert_called_once_with( + "GET", + "/empreendimentos", + params={"cidade": "São Paulo"}, + timeout=cliente_mrv.DEFAULT_TIMEOUT + ) + + assert status == HTTPStatus.OK + assert isinstance(dados, list) + assert len(dados) == 1 + assert dados[0]["nome"] == "Residencial Arvoredo" + + @patch("httpx.Client.request") + def test_consultar_empreendimentos_falha(self, mock_request, cliente_mrv): + # Simula uma falha na requisição (ex: erro 404) + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = HTTPStatus.NOT_FOUND + + # raise_for_status deve levantar exceção para simular o comportamento real do httpx + def raise_for_status_mock(): + raise httpx.HTTPStatusError("Not Found", request=MagicMock(), response=mock_response) + + mock_response.raise_for_status.side_effect = raise_for_status_mock + mock_request.return_value = mock_response + + # Executa e espera que levante a exceção mapeada no ClienteBase + with pytest.raises(Exception, match="API failed after the maximum number of attempts!"): + cliente_mrv.consultar_empreendimentos() + + # Verifica que ocorreu as tentativas de retentativa definidas em ClienteBase + assert mock_request.call_count == cliente_mrv.DEFAULT_MAX_RETRIES diff --git a/tests/test_quilombolas_parser.py b/tests/test_quilombolas_parser.py new file mode 100644 index 00000000..095642f1 --- /dev/null +++ b/tests/test_quilombolas_parser.py @@ -0,0 +1,160 @@ +import pandas as pd + +from quilombolas_parser import ( + PREFIXO_TABELA, + construir_nome_tabela, + construir_nome_tabela_indice, + deduplicar_colunas, + normalizar_nome_coluna, + parsear_arquivo_indice, + preparar_registros_insercao, + processar_chunk_excel, + resolver_chave_primaria, +) + + +def test_construir_nome_tabela_prefixo() -> None: + assert construir_nome_tabela("Tabela_de_resultado_02.xlsx") == ( + f"{PREFIXO_TABELA}tabela_de_resultado_02" + ) + + +def test_construir_nome_tabela_indice() -> None: + assert construir_nome_tabela_indice("Tabelas_de_resultados") == ( + f"{PREFIXO_TABELA}indice_tabelas_de_resultados" + ) + + +def test_normalizar_nome_coluna_remove_acentos() -> None: + nome = normalizar_nome_coluna("Taxa de alfabetização (%)", 0) + assert "alfabetizacao" in nome + assert "porcentagem" in nome + + +def test_deduplicar_colunas() -> None: + assert deduplicar_colunas(["total", "total", "total"]) == [ + "total", + "total_1", + "total_2", + ] + + +def test_parsear_arquivo_indice() -> None: + conteudo = ( + "Tabela 1 - População residente - Brasil - 2022\n" + "Tabela 2 - População por UF - 2022\n" + ) + chunk = parsear_arquivo_indice(conteudo, "Tabelas_de_resultados") + assert chunk.table_name == f"{PREFIXO_TABELA}indice_tabelas_de_resultados" + assert len(chunk.df) == 2 + assert chunk.df.iloc[0]["numero"] == "1" + assert "População residente" in chunk.df.iloc[0]["descricao"] + + +def test_parsear_arquivo_indice_sem_hifen() -> None: + """Índice IBGE pode usar espaços duplos em vez de hífen.""" + conteudo = "Tabela 3 População por sexo - 2022\n" + chunk = parsear_arquivo_indice(conteudo, "Tabelas_selecionadas") + assert chunk.df.iloc[0]["numero"] == "3" + assert "População por sexo" in chunk.df.iloc[0]["descricao"] + + +def test_localizar_linha_titulo_tabela_complementar() -> None: + from quilombolas_parser import _localizar_linha_titulo + + df = pd.DataFrame( + [ + ["Censo Demográfico 2022"], + ["Tabela complementar 1 - Alfabetização - 2022"], + ["Brasil", "100"], + ] + ) + assert _localizar_linha_titulo(df) == 1 + + +def test_processar_chunk_excel_apendice() -> None: + """Simula layout do Apêndice 1 (tabela textual sem valores numéricos).""" + linhas = [ + ["Censo Demográfico 2022", None], + ["Quilombolas: tema", None], + ["Apêndice 1 - Territórios citados - Brasil - 2022", None], + ["Estado", "Território Quilombola"], + ["PA", "Cuxiu"], + ["PA", "Guajarauna"], + ] + df_raw = pd.DataFrame(linhas) + chunk = processar_chunk_excel( + df_raw, "Apendice_01.xlsx", "Apendices/xlsx", "Apendice 1" + ) + assert chunk is not None + assert chunk.table_name == f"{PREFIXO_TABELA}apendice_01" + assert list(chunk.df.columns)[0] == "estado" + assert len(chunk.df) == 2 + assert chunk.table_comment.startswith("Territórios") + + +def test_resolver_chave_primaria_territorios_por_uf() -> None: + """Vários territórios na mesma UF exigem PK além das 3 primeiras colunas.""" + df = pd.DataFrame( + [ + ["Titulado", "11", "RO", "Rondônia", "11001", "TQ A", "100", "50"], + ["Titulado", "11", "RO", "Rondônia", "11280", "TQ B", "200", "80"], + ], + columns=[ + "status_fundiario", + "unidade_da_federacao_codigo", + "unidade_da_federacao_sigla", + "unidade_da_federacao_nome", + "territorio_quilombola_codigo", + "territorio_quilombola_nome", + "populacao_residente_total", + "populacao_residente_quilombola", + ], + ) + pk = resolver_chave_primaria(df) + assert "territorio_quilombola_codigo" in pk + assert len(pk) <= 5 + assert df.drop_duplicates(subset=pk).shape[0] == len(df) + + +def test_preparar_registros_deduplica_por_pk() -> None: + from quilombolas_parser import ChunkProcessado + + chunk = ChunkProcessado( + df=pd.DataFrame( + [["PA", "Cuxiu"], ["PA", "Cuxiu"]], + columns=["estado", "territorio_quilombola"], + ), + table_name=f"{PREFIXO_TABELA}apendice_01", + table_comment="teste", + primary_key=["estado", "territorio_quilombola"], + ) + registros = preparar_registros_insercao(chunk) + assert len(registros) == 1 + + +def test_processar_chunk_excel_cabecalho_triplo() -> None: + """Simula cabeçalho hierárquico de tabela complementar.""" + linhas = [ + ["Censo Demográfico 2022"] * 3, + ["Quilombolas: tema"] * 3, + [None] * 3, + ["Tabela complementar 1 - Alfabetização por região - 2022"] * 3, + [None] * 3, + ["Região", "População 15+", "População 15+"], + [None, "Total", "Quilombolas"], + [None, None, None], + ["Brasil", "100", "10"], + ["Norte", "50", "5"], + ] + df_raw = pd.DataFrame(linhas) + chunk = processar_chunk_excel( + df_raw, + "Tabela_complementar_01.xlsx", + "Tabelas_selecionadas/xlsx", + "Tabela complementar 1", + ) + assert chunk is not None + assert "regiao" in chunk.df.columns[0] + assert len(chunk.df) == 2 + assert "total" in chunk.col_comments.get(chunk.df.columns[1], "").lower()