Spaces:
Running
Running
Commit
·
fe69607
1
Parent(s):
af87f53
refact: v2.1.0
Browse files- Dockerfile +28 -0
- README.md +83 -12
- __pycache__/filter.cpython-311.pyc +0 -0
- image.png +0 -0
- Início.py → main.py +67 -60
- query.sql +0 -221
- requirements.txt +2 -1
- utils/__pycache__/filter.cpython-310.pyc +0 -0
- utils/__pycache__/filter.cpython-311.pyc +0 -0
- filter.py → utils/filter.py +75 -17
Dockerfile
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use a imagem base do Python
|
2 |
+
FROM python:3.9-slim
|
3 |
+
|
4 |
+
# Defina um diretório de trabalho padrão dentro do container
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copie os arquivos necessários para o container
|
8 |
+
COPY requirements.txt /app/requirements.txt
|
9 |
+
COPY . /app
|
10 |
+
|
11 |
+
# Instale as dependências do sistema e do Python
|
12 |
+
RUN apt-get update && apt-get install -y \
|
13 |
+
build-essential \
|
14 |
+
curl \
|
15 |
+
software-properties-common \
|
16 |
+
git \
|
17 |
+
&& rm -rf /var/lib/apt/lists/*
|
18 |
+
|
19 |
+
RUN pip3 install --no-cache-dir -r requirements.txt
|
20 |
+
|
21 |
+
# Exponha a porta onde o Streamlit será executado
|
22 |
+
EXPOSE 8501
|
23 |
+
|
24 |
+
# Adicione uma verificação de saúde (healthcheck)
|
25 |
+
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health || exit 1
|
26 |
+
|
27 |
+
# Comando para iniciar o Streamlit
|
28 |
+
ENTRYPOINT ["streamlit", "run", "main.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
README.md
CHANGED
@@ -1,12 +1,83 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
# GeoCosmos
|
3 |
+
|
4 |
+
GeoCosmos é uma plataforma desenvolvida pela Kukac para a Future Mining, especializada em extração de dados de artigos científicos de geologia utilizando Inteligência Artificial. O objetivo deste projeto é proporcionar uma interface amigável para acesso e manipulação dos dados extraídos de artigos, incluindo a possibilidade de filtrar, visualizar e excluir registros diretamente de um banco de dados SQL.
|
5 |
+
|
6 |
+
## Getting Started
|
7 |
+
|
8 |
+
Para começar a utilizar o projeto, siga as etapas abaixo:
|
9 |
+
|
10 |
+
1. **Clone o repositório**
|
11 |
+
|
12 |
+
Clone este repositório para sua máquina local:
|
13 |
+
|
14 |
+
```bash
|
15 |
+
git clone https://gitlab.kukac.com.br/kukac/future-mining/geocosmos.git
|
16 |
+
```
|
17 |
+
|
18 |
+
2. **Configuração de ambiente**
|
19 |
+
|
20 |
+
Certifique-se de que você tem o Python 3.8+ instalado em sua máquina. Em seguida, instale as dependências necessárias utilizando o `pip`:
|
21 |
+
|
22 |
+
```bash
|
23 |
+
pip install -r requirements.txt
|
24 |
+
```
|
25 |
+
|
26 |
+
3. **Configuração das variáveis de ambiente**
|
27 |
+
|
28 |
+
O projeto utiliza o arquivo `.env` para carregar as variáveis de ambiente, como credenciais de acesso ao banco de dados. Certifique-se de configurar corretamente este arquivo com as variáveis `username`, `password` e `DATABASE_URL`.
|
29 |
+
|
30 |
+
## Instalação
|
31 |
+
|
32 |
+
Para rodar o projeto localmente, execute o seguinte comando:
|
33 |
+
|
34 |
+
```bash
|
35 |
+
streamlit run app.py
|
36 |
+
```
|
37 |
+
|
38 |
+
Isso iniciará o servidor do Streamlit e abrirá a interface no seu navegador.
|
39 |
+
|
40 |
+
## Funcionalidades
|
41 |
+
|
42 |
+
- **Autenticação de usuário**: Um sistema de login que protege o acesso à plataforma.
|
43 |
+
- **Visualização de dados**: Exibição de artigos científicos de geologia extraídos de um banco de dados SQL.
|
44 |
+
- **Filtros avançados**: Permite filtrar dados com base em colunas numéricas, categóricas ou de data.
|
45 |
+
- **Edição de dados**: Interface para selecionar, visualizar e excluir registros do banco de dados.
|
46 |
+
- **Download de dados**: Exporte os dados visualizados como arquivos CSV.
|
47 |
+
|
48 |
+
## Como usar
|
49 |
+
|
50 |
+
1. **Login**: Para acessar a plataforma, insira o nome de usuário e senha fornecidos.
|
51 |
+
2. **Filtrar dados**: Escolha as colunas e os filtros que deseja aplicar aos dados.
|
52 |
+
3. **Editar dados**: Selecione as linhas que deseja editar ou excluir e execute as ações necessárias.
|
53 |
+
4. **Exportar dados**: Baixe os dados filtrados ou modificados como CSV.
|
54 |
+
|
55 |
+
## Suporte
|
56 |
+
|
57 |
+
Se precisar de ajuda, abra uma issue no repositório ou entre em contato com a equipe de suporte da Kukac.
|
58 |
+
|
59 |
+
## Roadmap
|
60 |
+
|
61 |
+
- **Próximas funcionalidades**:
|
62 |
+
- Integração com novos bancos de dados geológicos.
|
63 |
+
- Melhoria na interface de filtragem e visualização de dados.
|
64 |
+
|
65 |
+
## Contribuindo
|
66 |
+
|
67 |
+
Este projeto é mantido pela Kukac e está aberto a contribuições. Caso queira colaborar, siga os passos abaixo:
|
68 |
+
|
69 |
+
1. Fork este repositório.
|
70 |
+
2. Crie uma nova branch para suas alterações.
|
71 |
+
3. Envie um pull request com suas melhorias.
|
72 |
+
|
73 |
+
## Autores e Agradecimentos
|
74 |
+
|
75 |
+
Este projeto foi desenvolvido pela equipe de inovação da Kukac, com o apoio da Future Mining.
|
76 |
+
|
77 |
+
## Licença
|
78 |
+
|
79 |
+
Este projeto é de propriedade da Future Mining.
|
80 |
+
|
81 |
+
## Status do Projeto
|
82 |
+
|
83 |
+
O desenvolvimento está ativo e contínuo.
|
__pycache__/filter.cpython-311.pyc
ADDED
Binary file (4.76 kB). View file
|
|
image.png
DELETED
Binary file (6.88 kB)
|
|
Início.py → main.py
RENAMED
@@ -1,14 +1,17 @@
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
2 |
import pandas as pd
|
3 |
from sqlalchemy import create_engine, MetaData, Table, delete
|
4 |
from sqlalchemy.orm import sessionmaker
|
5 |
-
from filter import filter_dataframe
|
6 |
import os
|
7 |
import time
|
8 |
|
9 |
# Dados de login
|
10 |
-
USERNAME = os.getenv(
|
11 |
-
PASSWORD = os.getenv(
|
12 |
|
13 |
# Configurações da página
|
14 |
st.set_page_config(
|
@@ -25,6 +28,7 @@ def authenticate_user(username: str, password: str) -> bool:
|
|
25 |
def login_screen():
|
26 |
st.subheader('⛰️ Banco de Dados - GeoCosmos')
|
27 |
|
|
|
28 |
with st.form(key='login_form'):
|
29 |
username = st.text_input("Nome de usuário")
|
30 |
password = st.text_input("Senha", type='password')
|
@@ -41,41 +45,27 @@ def login_screen():
|
|
41 |
|
42 |
# Função para ler dados da tabela SQL a partir de um arquivo
|
43 |
def load_data() -> pd.DataFrame:
|
44 |
-
|
45 |
-
dialect = os.getenv("DB_DIALECT")
|
46 |
-
driver = os.getenv("DB_DRIVER")
|
47 |
-
username = os.getenv("DB_USERNAME")
|
48 |
-
password = os.getenv("DB_PASSWORD")
|
49 |
-
host = os.getenv("DB_HOST")
|
50 |
-
port = os.getenv("DB_PORT")
|
51 |
-
database = os.getenv("DB_DATABASE")
|
52 |
-
|
53 |
-
connection_url = f"{dialect}+{driver}://{username}:{password}@{host}:{port}/{database}"
|
54 |
-
|
55 |
engine = create_engine(connection_url)
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
with open('query.sql', 'r', encoding='utf-8') as file:
|
60 |
-
query = file.read()
|
61 |
-
|
62 |
-
df = pd.read_sql(query, engine)
|
63 |
-
|
64 |
-
return df, engine, session
|
65 |
|
66 |
# Função para deletar linhas selecionadas
|
67 |
-
def delete_selected_rows(selected_ids, engine,
|
68 |
meta = MetaData()
|
69 |
-
table = Table(
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
|
76 |
-
# Função para download dos dados como CSV
|
77 |
-
def convert_df_to_csv(df):
|
78 |
-
return df.to_csv(index=False).encode('utf-8')
|
79 |
|
80 |
# Verificar o estado de login
|
81 |
if 'logged_in' not in st.session_state:
|
@@ -86,55 +76,72 @@ if not st.session_state['logged_in']:
|
|
86 |
login_screen()
|
87 |
else:
|
88 |
st.header('⛰️ Banco de Dados - GeoCosmos')
|
89 |
-
|
90 |
-
|
91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
# Aplica o filtro ao dataframe
|
93 |
filtered_df = filter_dataframe(df)
|
94 |
|
|
|
95 |
all_columns = filtered_df.columns.tolist()
|
96 |
selected_columns = st.multiselect(
|
97 |
'Escolha as colunas para exibir:',
|
98 |
-
options=[
|
99 |
placeholder="Selecione uma ou mais colunas",
|
100 |
-
default=[]
|
|
|
101 |
)
|
102 |
|
103 |
-
|
104 |
-
|
105 |
if selected_columns:
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
|
|
111 |
else:
|
112 |
displayed_df = filtered_df
|
113 |
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
# Exibe o dataframe filtrado com checkboxes para seleção
|
118 |
-
edited_df = st.data_editor(displayed_df, disabled=[col for col in displayed_df.columns if col != 'Seleção'], hide_index=False, column_order = [col for col in displayed_df.columns if col != 'id'])
|
119 |
|
120 |
-
# Verifica se há pelo menos um checkbox selecionado
|
121 |
-
selected_ids = edited_df.loc[edited_df['Seleção'], "id"].tolist()
|
122 |
|
123 |
-
#
|
124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
|
|
|
|
|
|
|
|
|
|
|
126 |
if selected_ids:
|
127 |
with st.popover("APAGAR"):
|
128 |
st.warning("Você está prestes a apagar dados. Esta ação não pode ser desfeita.")
|
129 |
confirmation = st.text_input("Digite 'APAGAR' e pressione ENTER para confirmar a deleção:")
|
130 |
-
|
131 |
if confirmation == "APAGAR":
|
132 |
if st.button("CONFIRMAR"):
|
133 |
with st.spinner('Deletando arquivos...'):
|
134 |
-
delete_selected_rows(selected_ids, engine,
|
135 |
st.success(f'Os seguintes documentos foram apagados: {selected_docs}')
|
136 |
-
|
137 |
-
time.sleep(3)
|
138 |
st.rerun()
|
139 |
-
|
140 |
-
|
|
|
1 |
+
from dotenv import load_dotenv
|
2 |
+
load_dotenv()
|
3 |
+
|
4 |
import streamlit as st
|
5 |
import pandas as pd
|
6 |
from sqlalchemy import create_engine, MetaData, Table, delete
|
7 |
from sqlalchemy.orm import sessionmaker
|
8 |
+
from utils.filter import filter_dataframe
|
9 |
import os
|
10 |
import time
|
11 |
|
12 |
# Dados de login
|
13 |
+
USERNAME = os.getenv('username')
|
14 |
+
PASSWORD = os.getenv('password')
|
15 |
|
16 |
# Configurações da página
|
17 |
st.set_page_config(
|
|
|
28 |
def login_screen():
|
29 |
st.subheader('⛰️ Banco de Dados - GeoCosmos')
|
30 |
|
31 |
+
|
32 |
with st.form(key='login_form'):
|
33 |
username = st.text_input("Nome de usuário")
|
34 |
password = st.text_input("Senha", type='password')
|
|
|
45 |
|
46 |
# Função para ler dados da tabela SQL a partir de um arquivo
|
47 |
def load_data() -> pd.DataFrame:
|
48 |
+
connection_url = os.getenv("DATABASE_URL")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
engine = create_engine(connection_url)
|
50 |
+
df = pd.read_sql_table(table_name=os.getenv("TABLE_NAME"), con=engine)
|
51 |
+
return df, engine # Removido o retorno da sessão
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
|
53 |
# Função para deletar linhas selecionadas
|
54 |
+
def delete_selected_rows(selected_ids, engine, table):
|
55 |
meta = MetaData()
|
56 |
+
table = Table(table, meta, autoload_with=engine, schema="public")
|
57 |
+
|
58 |
+
Session = sessionmaker(bind=engine)
|
59 |
+
session = Session()
|
60 |
+
|
61 |
+
try:
|
62 |
+
with session.begin():
|
63 |
+
for id_value in selected_ids:
|
64 |
+
stmt = delete(table).where(table.c.id == id_value)
|
65 |
+
session.execute(stmt)
|
66 |
+
finally:
|
67 |
+
session.close() # Garante o fechamento da sessão
|
68 |
|
|
|
|
|
|
|
69 |
|
70 |
# Verificar o estado de login
|
71 |
if 'logged_in' not in st.session_state:
|
|
|
76 |
login_screen()
|
77 |
else:
|
78 |
st.header('⛰️ Banco de Dados - GeoCosmos')
|
79 |
+
|
80 |
+
placeholder = st.empty()
|
81 |
+
|
82 |
+
with st.spinner("Carregando dados..."):
|
83 |
+
# Carrega os dados apenas uma vez
|
84 |
+
if 'df' not in st.session_state:
|
85 |
+
df, engine = load_data()
|
86 |
+
st.session_state.df = df
|
87 |
+
st.session_state.engine = engine
|
88 |
+
else:
|
89 |
+
df = st.session_state.df
|
90 |
+
engine = st.session_state.engine
|
91 |
+
|
92 |
# Aplica o filtro ao dataframe
|
93 |
filtered_df = filter_dataframe(df)
|
94 |
|
95 |
+
|
96 |
all_columns = filtered_df.columns.tolist()
|
97 |
selected_columns = st.multiselect(
|
98 |
'Escolha as colunas para exibir:',
|
99 |
+
options=[col for col in all_columns if col not in ['id', 'Seleção']],
|
100 |
placeholder="Selecione uma ou mais colunas",
|
101 |
+
default=[],
|
102 |
+
key="column_selector"
|
103 |
)
|
104 |
|
105 |
+
|
|
|
106 |
if selected_columns:
|
107 |
+
displayed_columns = selected_columns + ['id']
|
108 |
+
if 'Nome do documento' not in displayed_columns:
|
109 |
+
displayed_columns.insert(1, 'Nome do documento')
|
110 |
+
# Remove duplicatas e garante a ordem
|
111 |
+
displayed_columns = list(dict.fromkeys(displayed_columns))
|
112 |
+
displayed_df = filtered_df[displayed_columns]
|
113 |
else:
|
114 |
displayed_df = filtered_df
|
115 |
|
116 |
+
if st.button("🔄", type="tertiary"):
|
117 |
+
del st.session_state["df"]
|
118 |
+
st.rerun()
|
|
|
|
|
119 |
|
|
|
|
|
120 |
|
121 |
+
# Exibe o editor com ordem fixa e key estável
|
122 |
+
selected_df = st.dataframe(
|
123 |
+
displayed_df,
|
124 |
+
column_order=[col for col in displayed_df.columns if col != 'Seleção'],
|
125 |
+
hide_index=False,
|
126 |
+
key="stable_data_editor",
|
127 |
+
on_select="rerun",
|
128 |
+
selection_mode="multi-row"
|
129 |
+
)
|
130 |
|
131 |
+
selected_rows = selected_df['selection']['rows']
|
132 |
+
selected_ids = displayed_df.iloc[selected_rows]["id"].tolist()
|
133 |
+
|
134 |
+
# Pega os nomes dos documentos selecionados
|
135 |
+
selected_docs = displayed_df.iloc[selected_rows]["Nome do documento"].tolist()
|
136 |
if selected_ids:
|
137 |
with st.popover("APAGAR"):
|
138 |
st.warning("Você está prestes a apagar dados. Esta ação não pode ser desfeita.")
|
139 |
confirmation = st.text_input("Digite 'APAGAR' e pressione ENTER para confirmar a deleção:")
|
|
|
140 |
if confirmation == "APAGAR":
|
141 |
if st.button("CONFIRMAR"):
|
142 |
with st.spinner('Deletando arquivos...'):
|
143 |
+
delete_selected_rows(selected_ids, engine, os.getenv("TABLE_NAME"))
|
144 |
st.success(f'Os seguintes documentos foram apagados: {selected_docs}')
|
145 |
+
del st.session_state["df"]
|
|
|
146 |
st.rerun()
|
147 |
+
|
|
query.sql
DELETED
@@ -1,221 +0,0 @@
|
|
1 |
-
SELECT
|
2 |
-
id,
|
3 |
-
e.id AS "Id da entrada no banco",
|
4 |
-
e."createdAt" AS "Data Criação",
|
5 |
-
e.name AS "Nome do documento",
|
6 |
-
|
7 |
-
-- Alvo ou depósito
|
8 |
-
COALESCE(data->'responseData'->0->'response'->>'target_or_deposit', 'Não especificado') AS "Alvo ou depósito",
|
9 |
-
|
10 |
-
-- Tipo de mineralização
|
11 |
-
COALESCE(
|
12 |
-
array_to_string(
|
13 |
-
ARRAY(
|
14 |
-
SELECT DISTINCT CONCAT(
|
15 |
-
mineralization->>'type'
|
16 |
-
)
|
17 |
-
FROM jsonb_array_elements(data->'responseData'->0->'response'->'mineralizations') AS mineralization
|
18 |
-
), ', '
|
19 |
-
), 'Não especificado'
|
20 |
-
) AS "Tipo de mineralização",
|
21 |
-
|
22 |
-
-- cidade
|
23 |
-
COALESCE(data->'responseData'->0->'response'->'region'->>'city', 'Não especificado') AS "Cidades",
|
24 |
-
-- estado
|
25 |
-
COALESCE(data->'responseData'->0->'response'->'region'->>'state', 'Não especificado') AS "Estado",
|
26 |
-
-- país
|
27 |
-
COALESCE(data->'responseData'->0->'response'->'region'->>'country', 'Não especificado') AS "País",
|
28 |
-
-- região
|
29 |
-
COALESCE(data->'responseData'->0->'response'->'region'->>'region_name', 'Não especificado') AS "Região",
|
30 |
-
-- Consolidar quantidade, tipo de mineralização e minerais com concentração
|
31 |
-
-- Consolidar quantidade, tipo de mineralização e minerais com concentração
|
32 |
-
CASE
|
33 |
-
WHEN jsonb_typeof(data->'responseData'->0->'response'->'mineralizations') = 'array' THEN
|
34 |
-
array_to_string(
|
35 |
-
ARRAY(
|
36 |
-
SELECT CONCAT(
|
37 |
-
-- Mostra a quantidade e o tipo de mineralização
|
38 |
-
mineralization->>'quantity', ' de ',
|
39 |
-
mineralization->>'type',
|
40 |
-
|
41 |
-
-- Condicional para incluir minerais apenas se existirem
|
42 |
-
CASE
|
43 |
-
WHEN jsonb_typeof(mineralization->'minerals') = 'array'
|
44 |
-
AND jsonb_array_length(mineralization->'minerals') > 0 THEN
|
45 |
-
': ' || array_to_string(
|
46 |
-
ARRAY(
|
47 |
-
SELECT CONCAT(
|
48 |
-
mineral->>'name',
|
49 |
-
' (', mineral->>'concentration', ')'
|
50 |
-
)
|
51 |
-
FROM jsonb_array_elements(mineralization->'minerals') AS mineral
|
52 |
-
WHERE mineral IS NOT NULL -- Ignora nulos
|
53 |
-
), ', '
|
54 |
-
)
|
55 |
-
ELSE '' -- Não mostra nada se não houver minerais
|
56 |
-
END
|
57 |
-
)
|
58 |
-
FROM jsonb_array_elements(data->'responseData'->0->'response'->'mineralizations') AS mineralization
|
59 |
-
WHERE mineralization IS NOT NULL -- Ignora nulos
|
60 |
-
), '; '
|
61 |
-
)
|
62 |
-
ELSE NULL -- Retorna NULL se o array não existir
|
63 |
-
END AS "Quantidades e concentrações",
|
64 |
-
|
65 |
-
|
66 |
-
-- extra_info
|
67 |
-
COALESCE(data->'responseData'->0->'response'->'region'->>'extra_info', 'Não especificado') AS "Informações adicionais",
|
68 |
-
|
69 |
-
COALESCE(NULLIF(data->'responseData'->4->'response'->>'mineralization_signatures', '[]'), 'Não especificado') AS "Assinaturas de mineralização", -- Foi decidido que este era melhor
|
70 |
-
|
71 |
-
COALESCE(NULLIF(data->'responseData'->0->'response'->'geological_context'->>'tectonic_context', ''), 'Não especificado') AS "Contexto tectônico", -- OK
|
72 |
-
COALESCE(NULLIF(data->'responseData'->0->'response'->'geological_context'->>'geological_context_description', ''), 'Não especificado') AS "Contexto geológico", -- OK
|
73 |
-
COALESCE(NULLIF(data->'responseData'->0->'response'->'geological_context'->>'context_of_present_rocks', ''), 'Não especificado') AS "Rochas presentes", -- OK
|
74 |
-
|
75 |
-
-- Rochas sedimentares
|
76 |
-
CASE
|
77 |
-
WHEN jsonb_typeof(data->'responseData'->1->'response'->'rockTypes'->'sedimentares') = 'array' THEN
|
78 |
-
array_to_string(
|
79 |
-
ARRAY(
|
80 |
-
SELECT CONCAT(
|
81 |
-
sedimentares->>'name',
|
82 |
-
' (Tipo: ', sedimentares->>'type',
|
83 |
-
', Relações de contato: ', COALESCE(sedimentares->>'contact_relations', 'ND'), ')'
|
84 |
-
)
|
85 |
-
FROM jsonb_array_elements(data->'responseData'->1->'response'->'rockTypes'->'sedimentares') AS sedimentares
|
86 |
-
WHERE sedimentares IS NOT NULL -- Ignora elementos nulos
|
87 |
-
), '; '
|
88 |
-
)
|
89 |
-
ELSE '' -- Retorna vazio se o array não existir
|
90 |
-
END AS "Rochas sedimentares",
|
91 |
-
-- Rochas metamórficas
|
92 |
-
CASE
|
93 |
-
WHEN jsonb_typeof(data->'responseData'->1->'response'->'rockTypes'->'metamórficas') = 'array' THEN
|
94 |
-
array_to_string(
|
95 |
-
ARRAY(
|
96 |
-
SELECT CONCAT(
|
97 |
-
metam->>'name',
|
98 |
-
' (Tipo: ', metam->>'type',
|
99 |
-
', Relações de contato: ', COALESCE(metam->>'contact_relations', 'ND'), ')'
|
100 |
-
)
|
101 |
-
FROM jsonb_array_elements(data->'responseData'->1->'response'->'rockTypes'->'metamórficas') AS metam
|
102 |
-
WHERE metam IS NOT NULL
|
103 |
-
), '; '
|
104 |
-
)
|
105 |
-
ELSE ''
|
106 |
-
END AS "Rochas metamórficas",
|
107 |
-
|
108 |
-
-- Rochas ígneas intrusivas
|
109 |
-
CASE
|
110 |
-
WHEN jsonb_typeof(data->'responseData'->1->'response'->'rockTypes'->'ígneas_intrusivas') = 'array' THEN
|
111 |
-
array_to_string(
|
112 |
-
ARRAY(
|
113 |
-
SELECT CONCAT(
|
114 |
-
igneas_i->>'name',
|
115 |
-
' (Tipo: ', igneas_i->>'type',
|
116 |
-
', Relações de contato: ', COALESCE(igneas_i->>'contact_relations', 'ND'), ')'
|
117 |
-
)
|
118 |
-
FROM jsonb_array_elements(data->'responseData'->1->'response'->'rockTypes'->'ígneas_intrusivas') AS igneas_i
|
119 |
-
WHERE igneas_i IS NOT NULL
|
120 |
-
), '; '
|
121 |
-
)
|
122 |
-
ELSE ''
|
123 |
-
END AS "Rochas ígneas intrusivas",
|
124 |
-
|
125 |
-
-- Rochas ígneas extrusivas
|
126 |
-
CASE
|
127 |
-
WHEN jsonb_typeof(data->'responseData'->1->'response'->'rockTypes'->'ígneas_extrusivas') = 'array' THEN
|
128 |
-
array_to_string(
|
129 |
-
ARRAY(
|
130 |
-
SELECT CONCAT(
|
131 |
-
igneas_e->>'name',
|
132 |
-
' (Tipo: ', igneas_e->>'type',
|
133 |
-
', Relações de contato: ', COALESCE(igneas_e->>'contact_relations', 'ND'), ')'
|
134 |
-
)
|
135 |
-
FROM jsonb_array_elements(data->'responseData'->1->'response'->'rockTypes'->'ígneas_extrusivas') AS igneas_e
|
136 |
-
WHERE igneas_e IS NOT NULL
|
137 |
-
), '; '
|
138 |
-
)
|
139 |
-
ELSE ''
|
140 |
-
END AS "Rochas ígneas extrusivas",
|
141 |
-
|
142 |
-
-- Rochas ígneas subvulcânicas
|
143 |
-
CASE
|
144 |
-
WHEN jsonb_typeof(data->'responseData'->1->'response'->'rockTypes'->'ígneas_subvulcânicas') = 'array' THEN
|
145 |
-
array_to_string(
|
146 |
-
ARRAY(
|
147 |
-
SELECT CONCAT(
|
148 |
-
igneas_sv->>'name',
|
149 |
-
' (Tipo: ', igneas_sv->>'type',
|
150 |
-
', Relações de contato: ', COALESCE(igneas_sv->>'contact_relations', 'ND'), ')'
|
151 |
-
)
|
152 |
-
FROM jsonb_array_elements(data->'responseData'->1->'response'->'rockTypes'->'ígneas_subvulcânicas') AS igneas_sv
|
153 |
-
WHERE igneas_sv IS NOT NULL
|
154 |
-
), '; '
|
155 |
-
)
|
156 |
-
ELSE ''
|
157 |
-
END AS "Rochas ígneas subvulcânicas",
|
158 |
-
|
159 |
-
-- Rochas hospedeiras
|
160 |
-
COALESCE(NULLIF(data->'responseData'->2->'response'->'host_rocks'->>'name', '[null]'), 'Não identificado') AS "Nomes de rochas hospedeiras",
|
161 |
-
COALESCE(NULLIF(data->'responseData'->2->'response'->'host_rocks'->>'geologicalEnvironments', '[null]'), 'Não identificado') AS "Ambiente geológico",
|
162 |
-
|
163 |
-
COALESCE(NULLIF(array_to_string(ARRAY(SELECT jsonb_array_elements_text(data->'responseData'->2->'response'->'mineralFabric')), ', '), ''), 'Não identificado') AS "Textura de mineralização",
|
164 |
-
COALESCE(NULLIF(array_to_string(ARRAY(
|
165 |
-
SELECT jsonb_extract_path_text(associatedMinerals, 'name') || ' (' || 'Cor: ' || jsonb_extract_path_text(associatedMinerals, 'color') || ', Textura: ' || jsonb_extract_path_text(associatedMinerals, 'textures') || ', Nível de Alteração: ' || jsonb_extract_path_text(associatedMinerals, 'alterationLevel') || ')'
|
166 |
-
FROM jsonb_array_elements(data->'responseData'->2->'response'->'associatedMinerals') AS associatedMinerals
|
167 |
-
), ', '), ''), 'Não identificado') AS "Minerais associados",
|
168 |
-
|
169 |
-
-- Tipos de processos alterações
|
170 |
-
COALESCE(
|
171 |
-
NULLIF(
|
172 |
-
array_to_string(
|
173 |
-
ARRAY(
|
174 |
-
SELECT alteration->>'alterationType'
|
175 |
-
FROM jsonb_array_elements(data->'responseData'->2->'response'->'alteration_processes') AS alteration
|
176 |
-
), ', '
|
177 |
-
), ''
|
178 |
-
), 'Não identificado'
|
179 |
-
) AS "Tipos de processos de alterações",
|
180 |
-
|
181 |
-
-- Minerais associados a alterações hidrotermais
|
182 |
-
COALESCE(
|
183 |
-
NULLIF(
|
184 |
-
array_to_string(
|
185 |
-
ARRAY(
|
186 |
-
SELECT jsonb_array_elements_text(alteration->'associatedMinerals')
|
187 |
-
FROM jsonb_array_elements(data->'responseData'->2->'response'->'alteration_processes') AS alteration
|
188 |
-
), ', '
|
189 |
-
), ''
|
190 |
-
), 'Não identificado'
|
191 |
-
) AS "Minerais associados a processos de alteração",
|
192 |
-
|
193 |
-
COALESCE(NULLIF(data->'responseData'->2->'response'->>'chemical_pattern_analysis', '[]'), 'Não especificado') AS "Análise de padrão químico",
|
194 |
-
|
195 |
-
|
196 |
-
COALESCE(NULLIF(data->'responseData'->3->'response'->>'stable_isotopes', '[]'), 'Não especificado') AS "Isótopos Estáveis e Radiogênicos",
|
197 |
-
COALESCE(NULLIF(data->'responseData'->3->'response'->>'trace_elements_and_rare_earths', '[]'), 'Não especificado') AS "Elementos traço e terras raras",
|
198 |
-
|
199 |
-
-- COALESCE(NULLIF(data->'responseData'->3->'response'->>'structural_mapping', '[]'), 'Não especificado') AS "Geologia estrutural da região",
|
200 |
-
-- COALESCE(NULLIF(data->'responseData'->4->'response'->>'structureMineralizationRelation', '[]'), 'Não especificado') AS "Relação estrutura-mineralização",
|
201 |
-
|
202 |
-
COALESCE(NULLIF(data->'responseData'->4->'response'->>'geophysical_signatures', '[]'), 'Não especificado') AS "Assinaturas geofísicas",
|
203 |
-
|
204 |
-
COALESCE(NULLIF(data->'responseData'->5->'response'->>'additional_information', '[]'), 'Não especificado') AS "Informações adicionais de mineralização",
|
205 |
-
COALESCE(NULLIF(data->'responseData'->6->'response'->>'estimates', '[]'), 'Não especificado') AS "Estimativas de quantidade de mineralização",
|
206 |
-
|
207 |
-
COALESCE(NULLIF(data->'responseData'->7->'response'->>'potential', '[]'), 'Não especificado') AS "Potencial de descoberta",
|
208 |
-
COALESCE(NULLIF(data->'responseData'->8->'response'->>'strengths', '[]'), 'Não especificado') AS "Pontos fortes",
|
209 |
-
COALESCE(NULLIF(data->'responseData'->8->'response'->>'investment_risk', '[]'), 'Não especificado') AS "Risco de investimento",
|
210 |
-
COALESCE(NULLIF(data->'responseData'->8->'response'->>'economic_potential', '[]'), 'Não especificado') AS "Potencial econômico",
|
211 |
-
COALESCE(NULLIF(data->'responseData'->9->'response'->>'explanation', '[]'), 'Não especificado') AS "Explicação da favorabilidade",
|
212 |
-
COALESCE(NULLIF(data->'responseData'->9->'response'->>'favorability', '[]'), 'Não especificado') AS "Favorabilidade"
|
213 |
-
|
214 |
-
|
215 |
-
FROM
|
216 |
-
public."Extraction" e
|
217 |
-
|
218 |
-
ORDER BY
|
219 |
-
e."createdAt";
|
220 |
-
|
221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
@@ -2,4 +2,5 @@ pandas==2.2.2
|
|
2 |
SQLAlchemy
|
3 |
streamlit
|
4 |
toml==0.10.2
|
5 |
-
psycopg2
|
|
|
|
2 |
SQLAlchemy
|
3 |
streamlit
|
4 |
toml==0.10.2
|
5 |
+
psycopg2-binary
|
6 |
+
python-dotenv
|
utils/__pycache__/filter.cpython-310.pyc
ADDED
Binary file (4.22 kB). View file
|
|
utils/__pycache__/filter.cpython-311.pyc
ADDED
Binary file (4.8 kB). View file
|
|
filter.py → utils/filter.py
RENAMED
@@ -7,37 +7,84 @@ from pandas.api.types import (
|
|
7 |
is_object_dtype,
|
8 |
)
|
9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
def filter_dataframe(df: pd.DataFrame) -> pd.DataFrame:
|
11 |
df = df.copy()
|
12 |
|
13 |
-
#
|
|
|
|
|
|
|
|
|
14 |
for col in df.columns:
|
15 |
if is_object_dtype(df[col]):
|
16 |
try:
|
17 |
-
df[col] = pd.to_datetime(df[col])
|
18 |
except Exception:
|
19 |
pass
|
20 |
-
|
21 |
if is_datetime64_any_dtype(df[col]):
|
22 |
df[col] = df[col].dt.tz_localize(None)
|
23 |
|
24 |
modification_container = st.container()
|
25 |
|
26 |
with modification_container:
|
27 |
-
to_filter_columns = st.multiselect(
|
|
|
|
|
|
|
|
|
28 |
for column in to_filter_columns:
|
29 |
left, right = st.columns((1, 20))
|
30 |
left.write("↳")
|
31 |
-
|
|
|
32 |
if is_categorical_dtype(df[column]) or df[column].nunique() < 5000:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
user_cat_input = right.multiselect(
|
34 |
f"Valores para {column}",
|
35 |
-
|
36 |
-
default=[],
|
37 |
placeholder="Escolha uma opção"
|
38 |
)
|
39 |
if user_cat_input: # Filtrar apenas se houver seleção
|
40 |
-
|
|
|
|
|
|
|
|
|
|
|
41 |
elif is_numeric_dtype(df[column]):
|
42 |
_min = float(df[column].min())
|
43 |
_max = float(df[column].max())
|
@@ -59,22 +106,33 @@ def filter_dataframe(df: pd.DataFrame) -> pd.DataFrame:
|
|
59 |
),
|
60 |
format="YYYY-MM-DD",
|
61 |
)
|
62 |
-
if len(user_date_input) == 2:
|
63 |
-
|
64 |
-
start_date, end_date
|
65 |
-
df = df.loc[df[column].between(start_date, end_date)]
|
66 |
else:
|
67 |
-
# Para colunas de texto
|
68 |
-
|
69 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
user_text_input = right.multiselect(
|
71 |
f"Valores para {column}",
|
72 |
unique_values,
|
73 |
default=[],
|
74 |
placeholder="Escolha uma opção",
|
75 |
)
|
76 |
-
if user_text_input:
|
77 |
-
|
|
|
|
|
|
|
|
|
|
|
78 |
else:
|
79 |
user_text_input = right.text_input(
|
80 |
f"Substring ou regex em {column}",
|
|
|
7 |
is_object_dtype,
|
8 |
)
|
9 |
|
10 |
+
def make_hashable(x):
|
11 |
+
"""
|
12 |
+
Converte recursivamente listas (e, se necessário, dicionários) em tipos hashable.
|
13 |
+
"""
|
14 |
+
if isinstance(x, list):
|
15 |
+
return tuple(make_hashable(e) for e in x)
|
16 |
+
# Se precisar, trate dicionários também:
|
17 |
+
if isinstance(x, dict):
|
18 |
+
return tuple(sorted((k, make_hashable(v)) for k, v in x.items()))
|
19 |
+
return x
|
20 |
+
|
21 |
+
def flatten_unique_values(series: pd.Series) -> list:
|
22 |
+
"""
|
23 |
+
Achata os valores únicos de uma série.
|
24 |
+
Se um valor for uma tupla, extrai cada item individualmente.
|
25 |
+
"""
|
26 |
+
unique_values_set = set()
|
27 |
+
for val in series.dropna().unique():
|
28 |
+
# Se o valor for uma tupla, adicione cada item separadamente
|
29 |
+
if isinstance(val, tuple):
|
30 |
+
unique_values_set.update(val)
|
31 |
+
else:
|
32 |
+
unique_values_set.add(val)
|
33 |
+
return list(unique_values_set)
|
34 |
+
|
35 |
def filter_dataframe(df: pd.DataFrame) -> pd.DataFrame:
|
36 |
df = df.copy()
|
37 |
|
38 |
+
# Primeiro, converta todos os valores da DataFrame para hashable
|
39 |
+
for col in df.columns:
|
40 |
+
df[col] = df[col].apply(make_hashable)
|
41 |
+
|
42 |
+
# Tenta converter strings para datetime e remover fuso horário
|
43 |
for col in df.columns:
|
44 |
if is_object_dtype(df[col]):
|
45 |
try:
|
46 |
+
df[col] = pd.to_datetime(df[col], format="%d-%m-%Y")
|
47 |
except Exception:
|
48 |
pass
|
|
|
49 |
if is_datetime64_any_dtype(df[col]):
|
50 |
df[col] = df[col].dt.tz_localize(None)
|
51 |
|
52 |
modification_container = st.container()
|
53 |
|
54 |
with modification_container:
|
55 |
+
to_filter_columns = st.multiselect(
|
56 |
+
"Filtrar por valor",
|
57 |
+
[column for column in df.columns if column != 'id'],
|
58 |
+
placeholder="Selecione um ou mais itens para filtrar"
|
59 |
+
)
|
60 |
for column in to_filter_columns:
|
61 |
left, right = st.columns((1, 20))
|
62 |
left.write("↳")
|
63 |
+
|
64 |
+
# Para colunas categóricas ou com poucos valores únicos, use multiselect
|
65 |
if is_categorical_dtype(df[column]) or df[column].nunique() < 5000:
|
66 |
+
raw_unique_values = df[column].dropna().unique()
|
67 |
+
# Verifica se há valores do tipo tupla (decorrentes de listas convertidas)
|
68 |
+
if any(isinstance(val, tuple) for val in raw_unique_values):
|
69 |
+
unique_values = flatten_unique_values(df[column])
|
70 |
+
is_flattened = True
|
71 |
+
else:
|
72 |
+
unique_values = list(raw_unique_values)
|
73 |
+
is_flattened = False
|
74 |
+
|
75 |
user_cat_input = right.multiselect(
|
76 |
f"Valores para {column}",
|
77 |
+
unique_values,
|
78 |
+
default=[], # Sem valores pré-selecionados
|
79 |
placeholder="Escolha uma opção"
|
80 |
)
|
81 |
if user_cat_input: # Filtrar apenas se houver seleção
|
82 |
+
if is_flattened:
|
83 |
+
df = df[df[column].apply(
|
84 |
+
lambda x: any(item in x for item in user_cat_input) if isinstance(x, tuple) else x in user_cat_input
|
85 |
+
)]
|
86 |
+
else:
|
87 |
+
df = df[df[column].isin(user_cat_input)]
|
88 |
elif is_numeric_dtype(df[column]):
|
89 |
_min = float(df[column].min())
|
90 |
_max = float(df[column].max())
|
|
|
106 |
),
|
107 |
format="YYYY-MM-DD",
|
108 |
)
|
109 |
+
if isinstance(user_date_input, tuple) and len(user_date_input) == 2:
|
110 |
+
start_date, end_date = map(pd.to_datetime, user_date_input)
|
111 |
+
df = df[df[column].between(start_date, end_date)]
|
|
|
112 |
else:
|
113 |
+
# Para colunas de texto
|
114 |
+
raw_unique_values = df[column].dropna().unique()
|
115 |
+
if any(isinstance(val, tuple) for val in raw_unique_values):
|
116 |
+
unique_values = flatten_unique_values(df[column])
|
117 |
+
is_flattened = True
|
118 |
+
else:
|
119 |
+
unique_values = list(raw_unique_values)
|
120 |
+
is_flattened = False
|
121 |
+
|
122 |
+
if len(unique_values) < 5000:
|
123 |
user_text_input = right.multiselect(
|
124 |
f"Valores para {column}",
|
125 |
unique_values,
|
126 |
default=[],
|
127 |
placeholder="Escolha uma opção",
|
128 |
)
|
129 |
+
if user_text_input:
|
130 |
+
if is_flattened:
|
131 |
+
df = df[df[column].apply(
|
132 |
+
lambda x: any(item in x for item in user_text_input) if isinstance(x, tuple) else x in user_text_input
|
133 |
+
)]
|
134 |
+
else:
|
135 |
+
df = df[df[column].isin(user_text_input)]
|
136 |
else:
|
137 |
user_text_input = right.text_input(
|
138 |
f"Substring ou regex em {column}",
|