bncc 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bncc/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """bncc — a BNCC (Base Nacional Comum Curricular) como dados estruturados e
2
+ verificados, com API de consulta em português.
3
+
4
+ Dados embutidos, zero dependências, zero rede. Cada registro é rastreável à
5
+ fonte oficial. Projeto bncc.dev · dados CC BY 4.0 · código MIT.
6
+ """
7
+ from ._codigos import decodificar
8
+ from ._consultas import (
9
+ buscar, estatisticas, estrutura, habilidades_ef, habilidades_em,
10
+ objetivos_ei, por_codigo, progressao_ei, versao,
11
+ )
12
+ from ._pandas import para_dataframe
13
+
14
+ __all__ = [
15
+ 'buscar', 'decodificar', 'estatisticas', 'estrutura', 'habilidades_ef',
16
+ 'habilidades_em', 'objetivos_ei', 'para_dataframe', 'por_codigo',
17
+ 'progressao_ei', 'versao',
18
+ ]
bncc/_codigos.py ADDED
@@ -0,0 +1,63 @@
1
+ """Decodificador de códigos da BNCC (as três gramáticas oficiais).
2
+
3
+ Cópia adaptada de bncc-dados/pipeline/codigos.py (mesma origem, mesmos erros),
4
+ onde é exercitado pelos 1.580 códigos do dataset a cada CI.
5
+ """
6
+ import re
7
+
8
+ CAMPOS_EI = {'EO': 'O eu, o outro e o nós', 'CG': 'Corpo, gestos e movimentos',
9
+ 'TS': 'Traços, sons, cores e formas', 'EF': 'Escuta, fala, pensamento e imaginação',
10
+ 'ET': 'Espaços, tempos, quantidades, relações e transformações'}
11
+ GRUPOS_EI = {'01': 'Bebês (0–1a6m)', '02': 'Crianças bem pequenas (1a7m–3a11m)',
12
+ '03': 'Crianças pequenas (4a–5a11m)'}
13
+ COMPONENTES_EF = {'AR': 'Arte', 'CI': 'Ciências', 'EF': 'Educação Física', 'ER': 'Ensino Religioso',
14
+ 'GE': 'Geografia', 'HI': 'História', 'LI': 'Língua Inglesa',
15
+ 'LP': 'Língua Portuguesa', 'MA': 'Matemática'}
16
+ BLOCOS_EF = {'15': [1, 2, 3, 4, 5], '69': [6, 7, 8, 9], '12': [1, 2], '35': [3, 4, 5],
17
+ '67': [6, 7], '89': [8, 9]}
18
+ BLOCOS_VALIDOS_POR_COMPONENTE = {'AR': {'15', '69'}, 'LP': {'15', '69', '12', '35', '67', '89'},
19
+ 'EF': {'12', '35', '67', '89'}}
20
+ AREAS_EM = {'LGG': 'Linguagens e suas Tecnologias', 'MAT': 'Matemática e suas Tecnologias',
21
+ 'CNT': 'Ciências da Natureza e suas Tecnologias', 'CHS': 'Ciências Humanas e Sociais Aplicadas'}
22
+
23
+
24
+ def decodificar(codigo):
25
+ """Decodifica um código BNCC → dict estruturado, ou lança ValueError."""
26
+ codigo = codigo.strip().upper()
27
+
28
+ m = re.fullmatch(r'EI(0[123])(EO|CG|TS|EF|ET)(\d{2})', codigo)
29
+ if m:
30
+ grupo, campo, seq = m.groups()
31
+ return {'codigo': codigo, 'etapa': 'EI', 'grupo_etario': grupo,
32
+ 'grupo_etario_nome': GRUPOS_EI[grupo], 'campo_experiencias': campo,
33
+ 'campo_experiencias_nome': CAMPOS_EI[campo], 'sequencia': int(seq)}
34
+
35
+ m = re.fullmatch(r'EF(\d{2})([A-Z]{2})(\d{2})', codigo)
36
+ if m:
37
+ anos_str, comp, seq = m.groups()
38
+ if comp not in COMPONENTES_EF:
39
+ raise ValueError(f'{codigo}: componente {comp!r} desconhecido')
40
+ if anos_str in BLOCOS_EF:
41
+ if anos_str not in BLOCOS_VALIDOS_POR_COMPONENTE.get(comp, set()):
42
+ raise ValueError(f'{codigo}: bloco {anos_str!r} inválido para {COMPONENTES_EF[comp]}')
43
+ anos = BLOCOS_EF[anos_str]
44
+ elif anos_str.startswith('0') and 1 <= int(anos_str) <= 9:
45
+ anos = [int(anos_str)]
46
+ else:
47
+ raise ValueError(f'{codigo}: ano/bloco {anos_str!r} inválido')
48
+ return {'codigo': codigo, 'etapa': 'EF', 'anos': anos, 'bloco': anos_str in BLOCOS_EF,
49
+ 'componente': comp, 'componente_nome': COMPONENTES_EF[comp], 'sequencia': int(seq)}
50
+
51
+ m = re.fullmatch(r'EM13([A-Z]{3})(\d)(\d{2})', codigo)
52
+ if m and m.group(1) in AREAS_EM:
53
+ area, ce, seq = m.groups()
54
+ return {'codigo': codigo, 'etapa': 'EM', 'seriacao': None, 'area': area,
55
+ 'area_nome': AREAS_EM[area], 'competencia_especifica': int(ce), 'sequencia': int(seq)}
56
+
57
+ m = re.fullmatch(r'EM13LP(\d{2})', codigo)
58
+ if m:
59
+ return {'codigo': codigo, 'etapa': 'EM', 'seriacao': None, 'area': 'LGG',
60
+ 'area_nome': AREAS_EM['LGG'], 'componente': 'LP',
61
+ 'competencia_especifica': None, 'sequencia': int(m.group(1))}
62
+
63
+ raise ValueError(f'{codigo}: não corresponde a nenhuma gramática BNCC (EI/EF/EM)')
bncc/_consultas.py ADDED
@@ -0,0 +1,165 @@
1
+ """API de consulta em português — espelho 1:1 do pacote npm @bncc/dados (snake_case)."""
2
+ from ._codigos import decodificar
3
+ from ._indice import indice, normalizar_texto, resolver_nome, versao as _versao
4
+
5
+
6
+ def _resolver(reg):
7
+ i = indice()
8
+ etapa = decodificar(reg['codigo'])['etapa']
9
+ base = {'codigo': reg['codigo'], 'etapa': etapa, 'texto': reg['texto'],
10
+ 'vigencia': reg['vigencia'], 'fonte': reg['fonte']}
11
+
12
+ if etapa == 'EI':
13
+ return {**base,
14
+ 'campo_experiencias': {'id': reg['campo_experiencias'], 'nome': resolver_nome(reg['campo_experiencias'])},
15
+ 'grupo_etario': reg['grupo_etario'],
16
+ 'alinhamento': reg['alinhamento']}
17
+
18
+ if etapa == 'EF':
19
+ org = reg['organizacao']
20
+ nomes = {}
21
+ if 'unidade_tematica' in org:
22
+ nomes['unidade_tematica'] = resolver_nome(org['unidade_tematica'])
23
+ if org['tipo'] == 'campo_pratica':
24
+ nomes['campos_atuacao'] = [resolver_nome(c) for c in org['campos_atuacao']]
25
+ nomes['pratica_linguagem'] = resolver_nome(org['pratica_linguagem'])
26
+ if org['tipo'] == 'eixo':
27
+ nomes['eixo'] = resolver_nome(org['eixo'])
28
+ return {**base,
29
+ 'componente': {'id': reg['componente'], 'nome': resolver_nome(reg['componente'])},
30
+ 'anos': reg['anos'],
31
+ 'organizacao': {'tipo': org['tipo'], 'nomes': nomes},
32
+ 'objetos_conhecimento': [{'id': o, 'nome': resolver_nome(o)} for o in reg['objetos_conhecimento']]}
33
+
34
+ comps = []
35
+ for cid in reg['competencias_especificas']:
36
+ c = indice()['competencias'].get(cid, {})
37
+ comps.append({'id': cid, 'numero': c.get('numero', 0), 'texto': c.get('texto', '')})
38
+ return {**base,
39
+ 'area': {'id': reg['area'], 'nome': resolver_nome(reg['area'])},
40
+ 'componente': ({'id': reg['componente'], 'nome': resolver_nome(reg['componente'])}
41
+ if reg['componente'] else None),
42
+ 'competencias_especificas': comps,
43
+ 'campos_atuacao_social': ([{'id': c, 'nome': resolver_nome(c)} for c in reg['campos_atuacao_social']]
44
+ if reg['campos_atuacao_social'] else None)}
45
+
46
+
47
+ def por_codigo(codigo):
48
+ """Registro completo de uma aprendizagem pelo código (case-insensitive), com nomes resolvidos."""
49
+ cod = codigo.strip().upper()
50
+ reg = indice()['por_codigo'].get(cod)
51
+ if reg is None:
52
+ decodificar(cod) # gramática inválida gera o erro explicativo
53
+ raise ValueError(f'{cod}: código válido na forma, mas não existe na BNCC '
54
+ '(dica: a numeração tem lacunas legítimas)')
55
+ return _resolver(reg)
56
+
57
+
58
+ def _id_componente(componente):
59
+ if componente is None:
60
+ return None
61
+ return componente if componente.startswith('ef-comp-') else f'ef-comp-{componente.lower()}'
62
+
63
+
64
+ def habilidades_ef(componente=None, ano=None, unidade_tematica=None, pratica=None, campo_atuacao=None):
65
+ """Habilidades do Ensino Fundamental, com filtros opcionais (sigla ou id de componente)."""
66
+ comp = _id_componente(componente)
67
+ saida = []
68
+ for h in indice()['habilidades_ef']:
69
+ org = h['organizacao']
70
+ if comp and h['componente'] != comp:
71
+ continue
72
+ if ano and ano not in h['anos']:
73
+ continue
74
+ if unidade_tematica and not ('unidade_tematica' in org and resolver_nome(org['unidade_tematica']) == unidade_tematica):
75
+ continue
76
+ if pratica and not (org['tipo'] == 'campo_pratica' and resolver_nome(org['pratica_linguagem']) == pratica):
77
+ continue
78
+ if campo_atuacao and not (org['tipo'] == 'campo_pratica'
79
+ and any(resolver_nome(c) == campo_atuacao for c in org['campos_atuacao'])):
80
+ continue
81
+ saida.append(_resolver(h))
82
+ return saida
83
+
84
+
85
+ def habilidades_em(area=None, competencia=None, apenas_lp=False):
86
+ """Habilidades do Ensino Médio. `area` aceita id (em-area-lgg) ou sigla (LGG)."""
87
+ aid = area if (area is None or area.startswith('em-area-')) else f'em-area-{area.lower()}'
88
+ i = indice()
89
+ saida = []
90
+ for h in i['habilidades_em']:
91
+ if aid and h['area'] != aid:
92
+ continue
93
+ if apenas_lp and h['componente'] != 'em-comp-lp':
94
+ continue
95
+ if competencia and not any(i['competencias'].get(c, {}).get('numero') == competencia
96
+ for c in h['competencias_especificas']):
97
+ continue
98
+ saida.append(_resolver(h))
99
+ return saida
100
+
101
+
102
+ def objetivos_ei(campo=None, grupo_etario=None):
103
+ """Objetivos da Educação Infantil. `campo` aceita id (ei-campo-ts) ou sigla (TS)."""
104
+ cid = campo if (campo is None or campo.startswith('ei-campo-')) else f'ei-campo-{campo.lower()}'
105
+ gid = grupo_etario if (grupo_etario is None or grupo_etario.startswith('ei-grupo-')) else f'ei-grupo-{grupo_etario}'
106
+ return [_resolver(o) for o in indice()['objetivos_ei']
107
+ if (not cid or o['campo_experiencias'] == cid) and (not gid or o['grupo_etario'] == gid)]
108
+
109
+
110
+ def buscar(texto, etapa=None, componente=None, ano=None):
111
+ """Busca textual normalizada (sem acentos/caixa) nos enunciados. Sem rede."""
112
+ alvo = normalizar_texto(texto)
113
+ i = indice()
114
+ universo = []
115
+ if etapa in (None, 'EI'):
116
+ universo += i['objetivos_ei']
117
+ if etapa in (None, 'EF'):
118
+ universo += i['habilidades_ef']
119
+ if etapa in (None, 'EM'):
120
+ universo += i['habilidades_em']
121
+ comp = componente if (componente is None or '-comp-' in componente) else f'ef-comp-{componente.lower()}'
122
+ saida = []
123
+ for r in universo:
124
+ if alvo not in normalizar_texto(r['texto']):
125
+ continue
126
+ if comp and r.get('componente') != comp:
127
+ continue
128
+ if ano and ano not in r.get('anos', []):
129
+ continue
130
+ saida.append(_resolver(r))
131
+ return saida
132
+
133
+
134
+ def progressao_ei(codigo):
135
+ """Progressão oficial da EI: os objetivos do mesmo aspecto nas três faixas etárias."""
136
+ reg = por_codigo(codigo)
137
+ if reg['etapa'] != 'EI':
138
+ raise ValueError(f"{reg['codigo']}: progressão por alinhamento só existe na Educação Infantil")
139
+ al = indice()['alinhamento_por_id'][reg['alinhamento']]
140
+ return {'alinhamento': al['id'], 'objetivos': [por_codigo(c) for c in al['objetivos']],
141
+ 'nota': al.get('nota')}
142
+
143
+
144
+ def estrutura():
145
+ """A espinha estrutural completa (etapas, áreas, componentes, competências, recortes)."""
146
+ return indice()['estrutura']
147
+
148
+
149
+ def estatisticas():
150
+ """Contagens do dataset."""
151
+ i = indice()
152
+ return {
153
+ 'total': len(i['objetivos_ei']) + len(i['habilidades_ef']) + len(i['habilidades_em']),
154
+ 'educacao_infantil': len(i['objetivos_ei']),
155
+ 'ensino_fundamental': len(i['habilidades_ef']),
156
+ 'ensino_medio': len(i['habilidades_em']),
157
+ 'alinhamentos_ei': len(i['alinhamentos']),
158
+ 'competencias_gerais': len(i['estrutura']['competencias_gerais']),
159
+ 'competencias_especificas': len(i['estrutura']['competencias_especificas']),
160
+ }
161
+
162
+
163
+ def versao():
164
+ """Data-version, commit de origem e checksums dos dados embutidos."""
165
+ return _versao()
bncc/_indice.py ADDED
@@ -0,0 +1,61 @@
1
+ """Carregamento lazy dos dados embutidos e índices em memória."""
2
+ import json
3
+ import re
4
+ import unicodedata
5
+ from functools import lru_cache
6
+ from pathlib import Path
7
+
8
+ _DADOS = Path(__file__).parent / 'dados'
9
+
10
+
11
+ def _carregar(arquivo):
12
+ return json.loads((_DADOS / f'{arquivo}.json').read_text(encoding='utf-8'))
13
+
14
+
15
+ @lru_cache(maxsize=1)
16
+ def indice():
17
+ estrutura = _carregar('estrutura')
18
+ ei = _carregar('educacao-infantil')
19
+ ef = _carregar('ensino-fundamental')
20
+ em = _carregar('ensino-medio')
21
+
22
+ por_codigo = {}
23
+ for o in ei['objetivos']:
24
+ por_codigo[o['codigo']] = o
25
+ for h in ef['habilidades'] + em['habilidades']:
26
+ por_codigo[h['codigo']] = h
27
+
28
+ contextos = {c['id']: c for c in ef['contextos_organizacao'] + em['contextos_organizacao']}
29
+ competencias = {c['id']: c for c in estrutura['competencias_especificas']}
30
+ alinhamentos = {a['id']: a for a in ei['alinhamentos']}
31
+ nomes = {}
32
+ for c in estrutura['componentes_curriculares'] + estrutura['areas_conhecimento'] + estrutura['campos_experiencias']:
33
+ nomes[c['id']] = c['nome']
34
+
35
+ return {
36
+ 'estrutura': estrutura,
37
+ 'objetivos_ei': ei['objetivos'], 'alinhamentos': ei['alinhamentos'],
38
+ 'habilidades_ef': ef['habilidades'], 'habilidades_em': em['habilidades'],
39
+ 'por_codigo': por_codigo, 'contextos': contextos,
40
+ 'competencias': competencias, 'alinhamento_por_id': alinhamentos, 'nomes': nomes,
41
+ }
42
+
43
+
44
+ def versao():
45
+ """Metadados dos dados embutidos: data-version, commit de origem, checksums."""
46
+ return json.loads((_DADOS / 'VERSAO.json').read_text(encoding='utf-8'))
47
+
48
+
49
+ def normalizar_texto(t):
50
+ """Normalização de busca: sem acentos, minúsculas, espaços únicos (mesma regra do @bncc/dados)."""
51
+ t = unicodedata.normalize('NFD', t)
52
+ t = ''.join(ch for ch in t if not unicodedata.combining(ch))
53
+ return re.sub(r'\s+', ' ', t.casefold()).strip()
54
+
55
+
56
+ def resolver_nome(id_):
57
+ i = indice()
58
+ ctx = i['contextos'].get(id_)
59
+ if ctx:
60
+ return ctx['nome']
61
+ return i['nomes'].get(id_, id_)
bncc/_pandas.py ADDED
@@ -0,0 +1,38 @@
1
+ """Integração opcional com pandas (extra `bncc[pandas]`)."""
2
+
3
+
4
+ def para_dataframe(etapa='EF'):
5
+ """DataFrame plano das aprendizagens de uma etapa ('EI', 'EF' ou 'EM').
6
+
7
+ Requer o extra: pip install bncc[pandas]
8
+ """
9
+ try:
10
+ import pandas as pd
11
+ except ImportError as e:
12
+ raise ImportError('para_dataframe requer pandas: pip install bncc[pandas]') from e
13
+
14
+ from ._consultas import habilidades_ef, habilidades_em, objetivos_ei
15
+
16
+ if etapa == 'EF':
17
+ registros = habilidades_ef()
18
+ linhas = [{
19
+ 'codigo': r['codigo'], 'componente': r['componente']['nome'],
20
+ 'anos': ' | '.join(map(str, r['anos'])),
21
+ 'organizacao': r['organizacao']['tipo'],
22
+ 'texto': r['texto'],
23
+ } for r in registros]
24
+ elif etapa == 'EM':
25
+ linhas = [{
26
+ 'codigo': r['codigo'], 'area': r['area']['nome'],
27
+ 'componente': r['componente']['nome'] if r['componente'] else None,
28
+ 'competencias': ' | '.join(str(c['numero']) for c in r['competencias_especificas']),
29
+ 'texto': r['texto'],
30
+ } for r in habilidades_em()]
31
+ elif etapa == 'EI':
32
+ linhas = [{
33
+ 'codigo': r['codigo'], 'campo': r['campo_experiencias']['nome'],
34
+ 'grupo_etario': r['grupo_etario'], 'texto': r['texto'],
35
+ } for r in objetivos_ei()]
36
+ else:
37
+ raise ValueError(f"etapa {etapa!r} inválida; use 'EI', 'EF' ou 'EM'")
38
+ return pd.DataFrame(linhas)
bncc/dados/VERSAO.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "data_version": "dados-2026.07",
3
+ "origem": "github.com/bncc-dev/bncc-dados",
4
+ "commit": "46accba58c68af21235c0b3d1d5ab3855834f739",
5
+ "sincronizado_de": "/Users/marcosbeto/Dev/bncc-dados",
6
+ "checksums_sha256": {
7
+ "estrutura.json": "0c499251a18a08508a74b1f4fa3ce4aae79ab9871bf0be65e4384629fb30b665",
8
+ "educacao-infantil.json": "e8549b4e35313b18d4fa02eb07b4ff603c3fef173224528a7aadd965680892e9",
9
+ "ensino-fundamental.json": "e18f7573a5c80ca326292160b8cc431992db80371923f2851974b919ac925817",
10
+ "ensino-medio.json": "380ff4761d5e287791de025ed806bcf343936a6398dbbab2e9265a2c42917adf"
11
+ }
12
+ }