anon-lib 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.
anon_lib/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Deteccao e mascaramento de PII em texto livre (pt-br e en).
2
+
3
+ Camada regex, deterministica e sem rede, opcionalmente somada a uma camada LLM
4
+ via Ollama para entidades que dependem de contexto (nomes, lugares, idades).
5
+ """
6
+
7
+ from anon_lib.anonymizer import Anonymizer
8
+
9
+ __version__ = "0.1.0"
10
+ __all__ = ["Anonymizer", "__version__"]
anon_lib/anonymizer.py ADDED
@@ -0,0 +1,160 @@
1
+ import re
2
+ import os
3
+ import copy
4
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
5
+ from anon_lib.patterns.patterns import _patterns_by_language
6
+
7
+ class Anonymizer:
8
+ def __init__(self,
9
+ llm_model_name:str="hf.co/empero-ai/Qwen3.8-4B-Distill-GGUF:Q4_K_M",
10
+ llm_ipaddress_service:str="http://localhost:11434",
11
+ language="pt-br"
12
+ ):
13
+ self.llm_model_name=llm_model_name
14
+ self.llm_ipaddress_service=llm_ipaddress_service
15
+ self._llm_client=None # criado sob demanda; veja _require_llm()
16
+ self.language=language
17
+ self._patterns_by_language=_patterns_by_language
18
+ self.list_of_undeterministics_entities = [
19
+ "PERSON_NAME", "LOCATION", "ORGANIZATION",
20
+ "DATE", "AGE",
21
+ "TIME", "MONEY", "PERCENT", "FACILITY", "GPE"
22
+ ]
23
+
24
+ self.prompt = (
25
+ "Você é um detector de dados pessoais (PII). Devolva as entidades "
26
+ "encontradas no texto em JSON, seguindo o schema pedido.\n"
27
+ "Regras:\n"
28
+ "- Copie cada valor exatamente como aparece no texto, sem reescrever.\n"
29
+ "- Não devolva o texto mascarado: quem mascara é o código.\n"
30
+ "- Se não houver nenhuma entidade, devolva uma lista vazia.\n"
31
+ "- Capture TODOS os nomes de pessoas, incluindo terceiros mencionados "
32
+ "por vínculo familiar, profissional ou social "
33
+ "(ex: 'pai de', 'filha de', 'cônjuge de', 'advogado de').\n"
34
+ "- Capture idades expressas como número seguido de 'anos' "
35
+ "(ex: '35 anos', 'com 12 anos') como entidade do tipo AGE."
36
+ )
37
+
38
+ def _require_llm(self):
39
+ """Carrega as dependencias opcionais do modo LLM e devolve (client, schema).
40
+
41
+ Erro de import vira mensagem acionavel: sem isso o usuario receberia um
42
+ ModuleNotFoundError cru, sem pista de que existe um extra a instalar.
43
+ """
44
+ try:
45
+ import ollama
46
+ from anon_lib.schemas.models_schemas import AnonOutput
47
+ except ImportError as e:
48
+ raise ImportError(
49
+ "use_llm=True requer as dependencias opcionais do modo LLM: "
50
+ "pip install anon-lib[llm]"
51
+ ) from e
52
+ if self._llm_client is None:
53
+ self._llm_client = ollama.Client(host=self.llm_ipaddress_service)
54
+ return self._llm_client, AnonOutput
55
+
56
+ @property
57
+ def list_of_words_patterns(self):
58
+ try:
59
+ # Obtém a lista de padrões a serem procurados
60
+ lista_por_idioma=self._patterns_by_language.get(self.language)
61
+ if lista_por_idioma is None:
62
+ raise ValueError(f"Idioma '{self.language}' não suportado.")
63
+ return lista_por_idioma
64
+ except Exception as e:
65
+ raise ValueError(f"Erro ao obter a lista de padrões: {e}")
66
+
67
+ def split_setences(self, sentence_text:str):
68
+ try:
69
+ text_splitter = RecursiveCharacterTextSplitter(
70
+ separators=["\n\n", "\n", ".", " ", ""],
71
+ chunk_size=1000,
72
+ chunk_overlap=100
73
+ )
74
+ return text_splitter.split_text(sentence_text)
75
+ except Exception as e:
76
+ raise ValueError(f"Erro ao realizar o split das sentenças textuais: {e}") from e
77
+
78
+ def find_keywords_and_replace(self, chunks_of_sentence_text:str=None):
79
+ if not chunks_of_sentence_text:
80
+ raise ValueError(f"Sem sentença a análisar.")
81
+
82
+ dict_found_keywords={}
83
+ valores_ja_capturados=set()
84
+ for chunk in chunks_of_sentence_text:
85
+ for pii_type, pattern in self.list_of_words_patterns.items():
86
+ for match in re.finditer(pattern, chunk):
87
+ valor=match.group(0)
88
+ if valor in valores_ja_capturados:
89
+ continue
90
+ valores_ja_capturados.add(valor)
91
+ dict_found_keywords.setdefault(pii_type, set()).add(valor)
92
+ return dict_found_keywords
93
+
94
+ def get_llm_response(self, prompt:str, format_schema:dict=None):
95
+ client, AnonOutput = self._require_llm()
96
+ try:
97
+ response = client.chat(
98
+ model=self.llm_model_name,
99
+ messages=[{"role": "user", "content": prompt}],
100
+ format=format_schema or AnonOutput.model_json_schema(),
101
+ options={"temperature": 0}
102
+ )
103
+ return response
104
+ except Exception as e:
105
+ raise ValueError(f"Erro ao obter resposta do LLM: {e}") from e
106
+
107
+ def find_undeterministic_entities(self, sentence_text:str):
108
+ prompt = (
109
+ f"{self.prompt}\n\n"
110
+ f"Tipos de entidade a procurar:\n"
111
+ f"{', '.join(self.list_of_undeterministics_entities)}\n\n"
112
+ f"Ignore estes tipos, já tratados por regex:\n"
113
+ f"{', '.join(self.list_of_words_patterns)}\n\n"
114
+ f"Texto:\n{sentence_text.strip()}"
115
+ )
116
+
117
+ _, AnonOutput = self._require_llm()
118
+ schema = copy.deepcopy(AnonOutput.model_json_schema())
119
+ schema["$defs"]["PIIEntity"]["properties"]["tipo"]["enum"] = list(
120
+ self.list_of_undeterministics_entities
121
+ )
122
+ response = self.get_llm_response(prompt=prompt, format_schema=schema)
123
+
124
+ conteudo = getattr(response.message, "content", None)
125
+ if not conteudo:
126
+ raise ValueError("Resposta do LLM veio vazia.")
127
+ try:
128
+ resultado = AnonOutput.model_validate_json(conteudo)
129
+ except Exception as e:
130
+ raise ValueError(f"Resposta do LLM não bate com o schema: {e}") from e
131
+
132
+ entidades = {}
133
+ for entidade in resultado.anon_model_output:
134
+ if entidade.valor and entidade.valor in sentence_text:
135
+ entidades.setdefault(entidade.tipo, set()).add(entidade.valor)
136
+ return entidades
137
+
138
+ def anonimize_text(self, sentence_text: str, use_llm: bool = False):
139
+ chunks = self.split_setences(sentence_text=sentence_text)
140
+ pii_words = self.find_keywords_and_replace(chunks_of_sentence_text=chunks)
141
+
142
+ if use_llm:
143
+ ja_capturados = {v for valores in pii_words.values() for v in valores}
144
+ for tipo, valores in self.find_undeterministic_entities(sentence_text).items():
145
+ novos = {v for v in valores
146
+ if not any(v in j or j in v for j in ja_capturados)}
147
+ if novos:
148
+ pii_words.setdefault(tipo, set()).update(novos)
149
+
150
+ tags = {
151
+ f"[{tipo}_{i}]": valor
152
+ for tipo, valores in pii_words.items()
153
+ for i, valor in enumerate(sorted(valores))
154
+ }
155
+
156
+ masked_text = sentence_text
157
+ for tag, valor in sorted(tags.items(), key=lambda kv: len(kv[1]), reverse=True):
158
+ masked_text = masked_text.replace(valor, tag)
159
+
160
+ return masked_text, {t: v for t, v in tags.items() if t in masked_text}
File without changes
@@ -0,0 +1,36 @@
1
+ _patterns_by_language={
2
+ "pt-br" : {
3
+ "EMAIL": r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b",
4
+ "CHAVE_PIX_ALEATORIA": r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b",
5
+ "CNPJ_NUMERICO_FORMATADO": r"\b\d{2}\.\d{3}\.\d{3}/\d{4}-\d{2}\b",
6
+ "CNPJ_ALFANUMERICO": r"\b[A-Z0-9]{2}\.[A-Z0-9]{3}\.[A-Z0-9]{3}/[A-Z0-9]{4}-\d{2}\b",
7
+ "RG": r"\b\d{2}\.\d{3}\.\d{3}-[0-9Xx]\b",
8
+ "PIS_PASEP": r"\b\d{3}\.\d{5}\.\d{2}-\d\b",
9
+ "CEP": r"(?<!\d)\d{5}-\d{3}(?!\d)",
10
+ "DATA": r"\b\d{2}/\d{2}/\d{4}\b",
11
+ "PLACA_VEICULO_MERCOSUL": r"\b[A-Z]{3}\d[A-Z]\d{2}\b",
12
+ "PLACA_VEICULO": r"\b[A-Z]{3}[- ]?\d{4}\b",
13
+ # --- Financeiro ---
14
+ "CARTAO_CREDITO_AMEX": r"(?<!\d)3[47]\d{2}[ .-]?\d{6}[ .-]?\d{5}(?!\d)",
15
+ "CARTAO_CREDITO": r"(?<!\d)(?:\d{4}[ .-]?){3}\d{4}(?!\d)",
16
+ "IP": r"(?<!\d)(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?!\d)",
17
+ "CNH": r"(?i)\bCNH\s*n?[º°]?\s*:?\s*\d{11}\b",
18
+ "AGENCIA": r"(?i)\bag(?:[êe]ncia)?\.?\s*n?[º°]?\s*:?\s*\d{4}(?:-\d)?\b",
19
+ "CONTA_BANCARIA": r"(?i)\b(?:c\/c|cc|conta)\.?\s*n?[º°]?\s*:?\s*\d{4,12}-?\d?\b",
20
+ # --- Numéricos crus (mais ambíguos, por último) ---
21
+ "TELEFONE_BR": r"(?<!\d)(?:\+55[-. ]?)?\(?\d{2}\)?[-. ]?9?\d{4}[-. ]?\d{4}(?!\d)",
22
+ "TITULO_ELEITOR": r"(?<!\d)\d{4}[ .]?\d{4}[ .]?\d{4}(?!\d)",
23
+ "CNPJ_NUMERICO": r"\b\d{14}\b",
24
+ "CNPJ_ALFANUMERICO_SEM_PONTUACAO": r"\b[A-Z0-9]{12}\d{2}\b",
25
+ "CPF": r"\b\d{3}\.\d{3}\.\d{3}[.-]\d{2}\b|(?<!\d)\d{11}(?!\d)",
26
+ },
27
+ "en": {
28
+ "EMAIL": r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b",
29
+ "SSN": r"(?<!\d)\d{3}-\d{2}-\d{4}(?!\d)",
30
+ "CREDIT_CARD_AMEX": r"(?<!\d)3[47]\d{2}[ .-]?\d{6}[ .-]?\d{5}(?!\d)",
31
+ "CREDIT_CARD": r"(?<!\d)(?:\d{4}[ .-]?){3}\d{4}(?!\d)",
32
+ "IP": r"(?<!\d)(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?!\d)",
33
+ "US_PHONE": r"(?<!\d)(?:\+?1[-.]?)?\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})(?!\d)",
34
+ "US_ZIP": r"\b\d{5}(?:-\d{4})?\b"
35
+ }
36
+ }
File without changes
@@ -0,0 +1,21 @@
1
+ from typing import List
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class PIIEntity(BaseModel):
7
+ """Uma entidade PII localizada pelo LLM no texto."""
8
+
9
+ tipo: str = Field(description="Tipo da entidade: PERSON, AGE, ADDRESS, ...")
10
+ valor: str = Field(description="Trecho exato copiado do texto, sem reescrever")
11
+
12
+
13
+ class AnonOutput(BaseModel):
14
+ """Saída estruturada esperada do LLM.
15
+
16
+ É uma lista de objetos, e não um dict {tipo: valor}, porque um dict não pode
17
+ repetir a mesma chave: um texto com duas pessoas perderia uma delas em
18
+ silêncio (é o mesmo defeito do exemplo de saída que está no README).
19
+ """
20
+
21
+ anon_model_output: List[PIIEntity]
@@ -0,0 +1,197 @@
1
+ Metadata-Version: 2.4
2
+ Name: anon-lib
3
+ Version: 0.1.0
4
+ Summary: PII detection and masking for Brazilian Portuguese and English
5
+ Project-URL: Homepage, https://github.com/wyctorfogos/anon-lib
6
+ Project-URL: Issues, https://github.com/wyctorfogos/anon-lib/issues
7
+ Author: Wyctor Fogos da Rocha
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: anonymization,lgpd,ner,pii,portuguese
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: English
14
+ Classifier: Natural Language :: Portuguese (Brazilian)
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Security
21
+ Classifier: Topic :: Text Processing :: Linguistic
22
+ Requires-Python: >=3.8.13
23
+ Requires-Dist: langchain-text-splitters>=0.2
24
+ Provides-Extra: dev
25
+ Requires-Dist: ollama>=0.3; extra == 'dev'
26
+ Requires-Dist: pydantic>=2; extra == 'dev'
27
+ Requires-Dist: pytest>=7; extra == 'dev'
28
+ Requires-Dist: python-dotenv; extra == 'dev'
29
+ Provides-Extra: llm
30
+ Requires-Dist: ollama>=0.3; extra == 'llm'
31
+ Requires-Dist: pydantic>=2; extra == 'llm'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Sentence-anonimizer
35
+
36
+ Detects and masks personally identifiable information (PII) in free text, replacing
37
+ each value with a stable tag and returning the tag → original value mapping.
38
+
39
+ Supports Brazilian Portuguese (`pt-br`) and English (`en`).
40
+
41
+ ## How it works
42
+
43
+ Two layers, and the split matters:
44
+
45
+ | Layer | Finds | Guarantees |
46
+ | --- | --- | --- |
47
+ | **Regex** (always on) | Formatted data: CPF, CNPJ, e-mail, phone, credit card, CEP… | Deterministic. Same input, same output, no network. |
48
+ | **LLM via Ollama** (opt-in) | Context-dependent data regex cannot express: names, places, organizations | Best effort. Recall varies between runs. |
49
+
50
+ The LLM only **identifies** entities and returns them as structured JSON. The masking
51
+ itself is always done in code, reusing the same tagging machinery as the regex layer.
52
+ The model never rewrites your text, so it cannot silently drop or alter passages.
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install anon-lib # regex layer only
58
+ pip install "anon-lib[llm]" # adds the optional LLM layer
59
+ ```
60
+
61
+ The base install has a single dependency and never touches the network. `ollama` and
62
+ `pydantic` come only with the `llm` extra and are imported lazily, so calling
63
+ `use_llm=True` without them raises an actionable `ImportError` instead of a bare
64
+ `ModuleNotFoundError`.
65
+
66
+ The LLM layer also needs a running [Ollama](https://ollama.com) server with a model
67
+ pulled:
68
+
69
+ ```bash
70
+ ollama pull qwen3:0.6b
71
+ ```
72
+
73
+ Point the library at it with the `llm_model_name` argument, or read it from your own
74
+ config:
75
+
76
+ ```python
77
+ Anonymizer(llm_model_name="qwen3:0.6b")
78
+ ```
79
+
80
+ ## Usage
81
+
82
+ `anonimize_text` returns a `(masked_text, tags)` tuple. Every tag in the mapping is
83
+ guaranteed to appear in the masked text.
84
+
85
+ ### Regex only (default)
86
+
87
+ ```python
88
+ from anon_lib import Anonymizer
89
+
90
+ anon = Anonymizer(language="pt-br")
91
+ masked, tags = anon.anonimize_text(
92
+ "Contato: joao.silva@empresa.com.br, CPF 123.456.789-00, "
93
+ "CNPJ 12.345.678/0001-99, cel (11) 98765-4321."
94
+ )
95
+ ```
96
+
97
+ ```
98
+ Contato: [EMAIL_0], CPF [CPF_0], CNPJ [CNPJ_NUMERICO_FORMATADO_0], cel [TELEFONE_BR_0].
99
+
100
+ {'[EMAIL_0]': 'joao.silva@empresa.com.br',
101
+ '[CPF_0]': '123.456.789-00',
102
+ '[CNPJ_NUMERICO_FORMATADO_0]': '12.345.678/0001-99',
103
+ '[TELEFONE_BR_0]': '(11) 98765-4321'}
104
+ ```
105
+
106
+ ### Regex + LLM
107
+
108
+ ```python
109
+ masked, tags = anon.anonimize_text(
110
+ "Meu nome é Maria, moro na Rua das Flores, 123 e meu CPF é 111.222.333-44.",
111
+ use_llm=True,
112
+ )
113
+ ```
114
+
115
+ ```
116
+ Meu nome é [PERSON_NAME_0], moro na [LOCATION_0], 123 e meu CPF é [CPF_0].
117
+
118
+ {'[CPF_0]': '111.222.333-44',
119
+ '[PERSON_NAME_0]': 'Maria',
120
+ '[LOCATION_0]': 'Rua das Flores'}
121
+ ```
122
+
123
+ Note the house number `123` left unmasked: that is real output, and it illustrates why
124
+ the regex layer is the one you rely on for guarantees.
125
+
126
+ Run both layers end to end with `python examples/demo.py`.
127
+
128
+ ## Detected patterns
129
+
130
+ **pt-br** — `CPF`, `CNPJ_NUMERICO_FORMATADO`, `CNPJ_ALFANUMERICO`, `CNPJ_NUMERICO`,
131
+ `CNPJ_ALFANUMERICO_SEM_PONTUACAO`, `RG`, `PIS_PASEP`, `TITULO_ELEITOR`, `CNH`, `EMAIL`,
132
+ `TELEFONE_BR`, `CEP`, `DATA`, `PLACA_VEICULO`, `PLACA_VEICULO_MERCOSUL`,
133
+ `CARTAO_CREDITO`, `CARTAO_CREDITO_AMEX`, `CHAVE_PIX_ALEATORIA`, `AGENCIA`,
134
+ `CONTA_BANCARIA`, `IP`
135
+
136
+ **en** — `EMAIL`, `SSN`, `CREDIT_CARD`, `CREDIT_CARD_AMEX`, `US_PHONE`, `US_ZIP`, `IP`
137
+
138
+ **LLM entity types** — `PERSON_NAME`, `LOCATION`, `ORGANIZATION`, `DATE`, `AGE`,
139
+ `TIME`, `MONEY`, `PERCENT`, `FACILITY`, `GPE` (configurable via
140
+ `list_of_undeterministics_entities`; the list is injected into the response schema as an
141
+ enum, so the model cannot return a type outside it).
142
+
143
+ ## Design notes
144
+
145
+ **Declaration order is priority.** Some values match several patterns — 11 bare digits
146
+ are both a CPF and a mobile number. The first pattern declared wins, so formatted
147
+ patterns come first and ambiguous bare numerics last.
148
+
149
+ **Longest value is replaced first.** The leading 12 digits of a spaced credit card also
150
+ match a voter ID. Replacing the shorter value first would corrupt the longer one.
151
+
152
+ **Label-anchored patterns include the label.** `CNH`, `AGENCIA` and `CONTA_BANCARIA` mask
153
+ the label too (`CNH: 12345678901` → `[CNH_0]`), because the bare number is
154
+ indistinguishable from a CPF without it.
155
+
156
+ **LLM values absent from the text are discarded.** A paraphrased or invented value would
157
+ produce a no-op substitution while making the output *look* anonymized.
158
+
159
+ ## Known limitations
160
+
161
+ - **No check-digit validation.** A well-formed but arithmetically invalid CPF is still
162
+ masked. For anonymization, over-detection is the safe side.
163
+ - **`DATA` matches any `dd/mm/aaaa` date**, with no way to tell a birth date from any
164
+ other. Distinguishing them requires context, which is the LLM layer's job.
165
+ - **`US_ZIP` matches any 5-digit number**, and bare 11-digit runs are inherently
166
+ ambiguous between CPF, CNH and mobile numbers.
167
+ - **`MONEY`, `PERCENT` and `TIME` are not PII** and only add noise. Adjust
168
+ `list_of_undeterministics_entities` for your use case — the list is injected into the
169
+ response schema as an enum, so it is the effective control over what the model may
170
+ return.
171
+ - **Tag numbering may skip an index.** When a short value is also a substring of a longer
172
+ masked one, it stays registered to catch standalone occurrences elsewhere. Dropping it
173
+ would leak PII.
174
+
175
+ ## Tests
176
+
177
+ ```bash
178
+ pip install -e ".[dev]" # needs Python 3.9+, as declared in pyproject.toml
179
+ pytest # 53 tests, hermetic: no server, no network (~2s)
180
+ pytest -m llm # 3 prompt-regression tests against a real Ollama
181
+ ```
182
+
183
+ The default suite stubs every LLM response, so it needs no server. The `llm` marker is
184
+ deselected by default and holds the tests that call the model for real — they verify the
185
+ prompt rules still take effect, which a stub cannot do, and skip themselves when Ollama
186
+ or the model is unavailable.
187
+
188
+ ## Layout
189
+
190
+ ```text
191
+ src/anon_lib/__init__.py public API: Anonymizer, __version__
192
+ src/anon_lib/anonymizer.py Anonymizer
193
+ src/anon_lib/patterns/patterns.py regex pattern tables, per language
194
+ src/anon_lib/schemas/models_schemas.py AnonOutput / PIIEntity (LLM response schema)
195
+ examples/demo.py runnable demo of both layers
196
+ tests/test_anonymizer.py test suite
197
+ ```
@@ -0,0 +1,10 @@
1
+ anon_lib/__init__.py,sha256=MPa-bPWPeL2OJtuBunMrSkJRaFnd3CsMvicS41OoIUU,331
2
+ anon_lib/anonymizer.py,sha256=YsWf4rRFiUkrpQO9idnsTZ4U8et3q_lfix-rHJgd8rU,7006
3
+ anon_lib/patterns/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ anon_lib/patterns/patterns.py,sha256=3ghBA6RX6mLXezC7kGgwy-a-j4nOgnyuVUzTENFmEC8,2447
5
+ anon_lib/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ anon_lib/schemas/models_schemas.py,sha256=VzoIQxcYYbLxF8wbgHlPXwyX4FWSYKiChl4h2s7pfVQ,675
7
+ anon_lib-0.1.0.dist-info/METADATA,sha256=AGfFEbt1vCLX8-PYvmyuY6W7dtCh88ZQD1ky_33pBS8,7524
8
+ anon_lib-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ anon_lib-0.1.0.dist-info/licenses/LICENSE,sha256=8KxMCd_YyztEAdhGQW8sXglF3fqE84quMIk7caSNUaQ,1078
10
+ anon_lib-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wyctor Fogos da Rocha
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.