tugatagger 0.0.1__tar.gz

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.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: tugatagger
3
+ Version: 0.0.1
4
+ Summary: A unified wrapper for Portuguese POS tagging and benchmarking.
5
+ Home-page: https://github.com/TigreGotico/tugatagger
6
+ Author: JarbasAI
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Classifier: Natural Language :: Portuguese
12
+ Requires-Python: >=3.7
13
+ Provides-Extra: spacy
14
+ Requires-Dist: spacy; extra == "spacy"
15
+ Provides-Extra: lexicon
16
+ Requires-Dist: tugalex; extra == "lexicon"
17
+ Provides-Extra: brill
18
+ Requires-Dist: brill_postagger; extra == "brill"
19
+ Dynamic: author
20
+ Dynamic: classifier
21
+ Dynamic: home-page
22
+ Dynamic: provides-extra
23
+ Dynamic: requires-python
24
+ Dynamic: summary
@@ -0,0 +1,86 @@
1
+ # TugaTagger 🇵🇹
2
+
3
+ **TugaTagger** is a unified, lightweight wrapper for Portuguese Part-of-Speech (POS) tagging. It provides a standardized interface to swap between different NLP backends, making it ideal for benchmarking different approaches or maintaining consistency across multiple microservices and repositories.
4
+
5
+ ---
6
+
7
+ ## 🚀 Key Features
8
+
9
+ * **Unified API:** Use the same `tag()` method regardless of the underlying engine.
10
+ * **Multiple Backends:** Supports **spaCy**, **Brill-style** taggers, and **Lexicon-based** lookups.
11
+ * **Robust Fallback ("Auto" Mode):** Automatically tries the best available engine, falling back to heuristic-based "guessing" if dependencies are missing.
12
+ * **Zero-Dependency Mode:** Includes a built-in rule-based tagger for environments where installing heavy NLP models isn't feasible.
13
+
14
+ ---
15
+
16
+ ## 📦 Installation
17
+
18
+ *(Note: Install the backends you intend to use)*
19
+
20
+ ```bash
21
+ pip install tugatagger[brill]
22
+
23
+ # To use spaCy
24
+ pip install tugatagger[spacy]
25
+ python -m spacy download pt_core_news_lg
26
+ ```
27
+
28
+ ---
29
+
30
+ ## 🛠 Usage
31
+
32
+ ### Quick Start
33
+
34
+ The `auto` engine is the default. It attempts to use spaCy or Brill first and falls back to a heuristic "dummy" tagger if they aren't installed.
35
+
36
+ ```python
37
+ from tugatagger import TugaTagger
38
+
39
+ tagger = TugaTagger(engine="auto")
40
+ text = "O gato preto pulou o muro."
41
+
42
+ tags = tagger.tag(text)
43
+ for word, pos in tags:
44
+ print(f"{word} -> {pos}")
45
+
46
+ ```
47
+
48
+ ### Choosing a Specific Engine
49
+
50
+ You can force a specific backend for benchmarking or production stability.
51
+
52
+ | Engine | Description | Best For... |
53
+ | --- |------------------------------------------------|--------------------------------------------|
54
+ | `spacy` | Uses `pt_core_news_lg` (or your choice). | High accuracy & context awareness. |
55
+ | `brill` | Transformation-based learning tagger. | Fast performance with good accuracy. |
56
+ | `lexicon` | Dictionary lookup from `tugalex`. | word-lookup tagging. |
57
+ | `dummy` | Heuristics based on suffixes and common words. | Low-resource / No-dependency environments. |
58
+
59
+ ```python
60
+ # Force spaCy with a specific model
61
+ tagger = TugaTagger(engine="spacy", spacy_model="pt_core_news_sm")
62
+
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 🧠 How the Heuristic Tagger Works
68
+
69
+ When using `engine="dummy"` or as a final fallback, TugaTagger uses a multi-stage guessing logic:
70
+
71
+ 1. **Punctuation/Numbers:** Identifies `PUNCT` and `NUM`.
72
+ 2. **Closed-class Lookups:** Identifies common Portuguese functional words (e.g., "o", "de", "com", "mas").
73
+ 3. **Suffix Morphology:** Analyzes word endings (e.g., `-mente` → `ADV`, `-ar/-er/-ir` → `VERB`, `-ção` → `NOUN`).
74
+ 4. **Capitalization:** Heuristic for `PROPN` (Proper Nouns).
75
+ 5. **Default:** Falls back to `NOUN`.
76
+
77
+ ---
78
+
79
+ ## 🤝 Contributing
80
+
81
+ If you'd like to add a new engine (e.g., Stanza or NLTK):
82
+
83
+ 1. Add a `tag_newengine` method to the `TugaTagger` class.
84
+ 2. Update the `engines` dictionary in the `tag()` method.
85
+ 3. Add the corresponding loading logic in `__init__`.
86
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,55 @@
1
+ import os
2
+
3
+ from setuptools import setup, find_packages
4
+
5
+ BASEDIR = os.path.abspath(os.path.dirname(__file__))
6
+
7
+
8
+ def get_version():
9
+ """ Find the version of the package"""
10
+ version_file = os.path.join(BASEDIR, 'tugatagger', 'version.py')
11
+ major, minor, build, alpha = (None, None, None, None)
12
+ with open(version_file) as f:
13
+ for line in f:
14
+ if 'VERSION_MAJOR' in line:
15
+ major = line.split('=')[1].strip()
16
+ elif 'VERSION_MINOR' in line:
17
+ minor = line.split('=')[1].strip()
18
+ elif 'VERSION_BUILD' in line:
19
+ build = line.split('=')[1].strip()
20
+ elif 'VERSION_ALPHA' in line:
21
+ alpha = line.split('=')[1].strip()
22
+
23
+ if ((major and minor and build and alpha) or
24
+ '# END_VERSION_BLOCK' in line):
25
+ break
26
+ version = f"{major}.{minor}.{build}"
27
+ if alpha and int(alpha) > 0:
28
+ version += f"a{alpha}"
29
+ return version
30
+
31
+
32
+ setup(
33
+ name="tugatagger",
34
+ version=get_version(),
35
+ description="A unified wrapper for Portuguese POS tagging and benchmarking.",
36
+ # long_description=open("README.md").read(),
37
+ # long_description_content_content_type="text/markdown",
38
+ author="JarbasAI",
39
+ url="https://github.com/TigreGotico/tugatagger",
40
+ packages=find_packages(),
41
+ install_package_data=True,
42
+ extras_require={
43
+ "spacy": ["spacy"],
44
+ "lexicon": ["tugalex"],
45
+ "brill": ["brill_postagger"],
46
+ },
47
+ classifiers=[
48
+ "Programming Language :: Python :: 3",
49
+ "License :: OSI Approved :: MIT License",
50
+ "Operating System :: OS Independent",
51
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
52
+ "Natural Language :: Portuguese",
53
+ ],
54
+ python_requires='>=3.7',
55
+ )
@@ -0,0 +1,253 @@
1
+ from typing import List, Tuple
2
+
3
+
4
+ class TugaTagger:
5
+ """
6
+ A unified interface for Portuguese Part-of-Speech (POS) tagging.
7
+
8
+ Supports multiple backends including spaCy and Brill-style taggers.
9
+ The 'auto' mode provides a fallback mechanism to ensure tagging works
10
+ even if specific dependencies are missing.
11
+ """
12
+
13
+ def __init__(self, engine: str = "auto", spacy_model: str = "pt_core_news_lg"):
14
+ """
15
+ Create a TugaTagger configured for the chosen tagging engine and optionally preload backend models.
16
+
17
+ Parameters:
18
+ engine (str): Tagging engine to use: "spacy", "brill", "dummy", or "auto".
19
+ - "spacy": preload the spaCy model in strict mode.
20
+ - "brill": preload the Brill tagger in strict mode.
21
+ - "auto": attempt to preload both spaCy and Brill (not strict).
22
+ spacy_model (str): Name of the spaCy Portuguese model to load when using spaCy (default "pt_core_news_lg").
23
+
24
+ Notes:
25
+ Load failures for a backend will be propagated when that backend is loaded in strict mode.
26
+ """
27
+ self.engine = engine
28
+ self._spacy = self._brill = self._lexicon = None
29
+
30
+ if engine in ["spacy", "auto"]:
31
+ self.load_spacy(spacy_model, strict=(engine == "spacy"))
32
+ if engine in ["brill", "auto"]:
33
+ self.load_brill(strict=(engine == "brill"))
34
+ if engine in ["lexicon", "auto"]:
35
+ self.load_lexicon(strict=(engine == "lexicon"))
36
+
37
+ def load_lexicon(self, strict: bool = True):
38
+ try:
39
+ from tugalex import TugaLexicon
40
+ self._lexicon = TugaLexicon()
41
+ except Exception as e:
42
+ if strict:
43
+ raise e
44
+
45
+ def load_spacy(self, spacy_model: str = "pt_core_news_lg", strict: bool = True):
46
+ """
47
+ Load and cache a spaCy Portuguese NLP model on the instance.
48
+
49
+ Parameters:
50
+ spacy_model (str): Name of the spaCy model to load (e.g., "pt_core_news_lg").
51
+ strict (bool): If True, re-raise any exception encountered while loading; if False, suppress the exception and leave `self._spacy` as None.
52
+
53
+ Side effects:
54
+ Assigns the loaded spaCy model to `self._spacy`.
55
+ """
56
+ try:
57
+ import spacy
58
+ self._spacy = spacy.load(spacy_model, disable=["ner", "parser"])
59
+ except Exception as e:
60
+ if strict:
61
+ raise e
62
+
63
+ def load_brill(self, strict: bool = True):
64
+ """
65
+ Load and cache a Brill-style Portuguese POS tagger on the instance.
66
+
67
+ Parameters:
68
+ strict (bool): If True, re-raise any exception encountered while importing or loading the tagger;
69
+ if False, suppress load errors and leave `self._brill` unset when loading fails.
70
+ """
71
+ try:
72
+ from brill_postaggers import BrillPostagger
73
+ self._brill = BrillPostagger.from_pretrained("pt")
74
+ except Exception as e:
75
+ if strict:
76
+ raise e
77
+
78
+ def tag(self, sentence: str) -> List[Tuple[str, str]]:
79
+ """
80
+ Tags a sentence using the configured engine.
81
+
82
+ Args:
83
+ sentence (str): The Portuguese text to tag.
84
+
85
+ Returns:
86
+ List[Tuple[str, str]]: A list of (word, tag) tuples.
87
+ """
88
+ engines = {
89
+ "auto": self.tag_auto,
90
+ "dummy": self.tag_dummy,
91
+ "brill": self.tag_brill,
92
+ "spacy": self.tag_spacy,
93
+ "lexicon": self.tag_lexicon
94
+ }
95
+ handler = engines.get(self.engine)
96
+ if not handler:
97
+ raise ValueError(f"Invalid engine: '{self.engine}'")
98
+ return handler(sentence)
99
+
100
+ def tag_auto(self, sentence: str) -> List[Tuple[str, str]]:
101
+ """
102
+ Tag a sentence by trying spaCy then Brill and using the dummy tagger if both fail.
103
+
104
+ Returns:
105
+ List[Tuple[str, str]]: A list of (word, POS-tag) tuples produced by the first successful tagger; if both spaCy and Brill raise exceptions, returns the dummy tagger's output.
106
+ """
107
+ for method in [self.tag_brill, self.tag_spacy, self.tag_lexicon]:
108
+ try:
109
+ return method(sentence)
110
+ except:
111
+ continue
112
+ return self.tag_dummy(sentence)
113
+
114
+ def tag_brill(self, sentence: str) -> List[Tuple[str, str]]:
115
+ """
116
+ Tag a sentence using the Brill-style POS tagger.
117
+
118
+ If the Brill tagger is not yet loaded, it will be initialized automatically.
119
+
120
+ Parameters:
121
+ sentence (str): Raw text sentence to be tagged.
122
+
123
+ Returns:
124
+ List[Tuple[str, str]]: List of (token, POS-tag) pairs for the input sentence.
125
+ """
126
+ if self._brill is None:
127
+ self.load_brill(strict=True)
128
+ return self._brill.tag(sentence)
129
+
130
+ def tag_spacy(self, sentence: str) -> List[Tuple[str, str]]:
131
+ """
132
+ Tag a sentence using the configured spaCy Portuguese model.
133
+
134
+ Returns:
135
+ List[Tuple[str, str]]: A list of (token_text, POS_tag) tuples, one per token. `POS_tag` is spaCy's coarse-grained part-of-speech label.
136
+ """
137
+ if self._spacy is None:
138
+ self.load_spacy(strict=True)
139
+ doc = self._spacy(sentence)
140
+ return [(tok.text, tok.pos_) for tok in doc]
141
+
142
+ def tag_lexicon(self, sentence: str) -> List[Tuple[str, str]]:
143
+ tagged = []
144
+
145
+ # TODO: improve this
146
+ tokenize = lambda k: k.lower().replace(".", " .").replace("!", " !").replace("?", " ?").split()
147
+
148
+ for word in tokenize(sentence):
149
+ if word in self._lexicon.possible_postags:
150
+ possibilities = self._lexicon.possible_postags[word]
151
+ # TODO: how to choose postag? use surrounding context
152
+ tagged.append((word, possibilities[0]))
153
+ else:
154
+ tagged.append((word, self._guess_pos(word)))
155
+ return tagged
156
+
157
+ @classmethod
158
+ def tag_dummy(cls, sentence: str) -> List[Tuple[str, str]]:
159
+ """
160
+ Split the sentence on whitespace and tag each token as the noun "NOUN".
161
+
162
+ Returns:
163
+ List[Tuple[str, str]]: A list of (word, tag) tuples where each whitespace-separated token from the input is paired with the tag "NOUN".
164
+ """
165
+ return [(word, cls._guess_pos(word)) for word in sentence.split()]
166
+
167
+ @staticmethod
168
+ def _guess_pos(word: str) -> str:
169
+ """Applies heuristic rules to guess the POS tag."""
170
+ lower_word = word.lower()
171
+
172
+ # Rule 0: Punctuation
173
+ if not word.isalnum():
174
+ return "PUNCT"
175
+
176
+ # Rule 0.5: Numbers
177
+ if word.isdigit():
178
+ return "NUM"
179
+
180
+ # 1. Closed-class words (Functional words that rarely change category)
181
+ COMMON_WORDS = {
182
+ # Articles
183
+ "o": "DET", "a": "DET", "os": "DET", "as": "DET", "um": "DET", "uma": "DET",
184
+ # Prepositions
185
+ "de": "ADP", "do": "ADP", "da": "ADP", "em": "ADP", "no": "ADP", "na": "ADP",
186
+ "por": "ADP", "para": "ADP", "com": "ADP", "sem": "ADP", #"a": "ADP",
187
+ # Conjunctions
188
+ "e": "CCONJ", "mas": "CCONJ", "ou": "CCONJ", "que": "SCONJ", "se": "SCONJ",
189
+ # Pronouns
190
+ "eu": "PRON", "ele": "PRON", "ela": "PRON", "nós": "PRON", "eles": "PRON",
191
+ "isso": "PRON", "aquilo": "PRON",
192
+ # Common Verbs (Auxiliary/Copula)
193
+ "é": "AUX", "foi": "AUX", "são": "AUX", "está": "AUX", "ser": "AUX", "ter": "AUX",
194
+ # Adverbs
195
+ "não": "ADV", "sim": "ADV", "muito": "ADV", "mais": "ADV"
196
+ }
197
+
198
+ # 2. Suffix Rules (Order matters: check longer suffixes first)
199
+ SUFFIX_RULES = [
200
+ ("mente", "ADV"), # rapidamente
201
+ ("ando", "VERB"), # cantando
202
+ ("endo", "VERB"), # correndo
203
+ ("indo", "VERB"), # partindo
204
+ ("aram", "VERB"), # cantaram
205
+ ("eram", "VERB"), # correram
206
+ ("iram", "VERB"), # partiram
207
+ ("ava", "VERB"), # cantava
208
+ ("ria", "VERB"), # cantaria
209
+ ("dor", "NOUN"), # jogador
210
+ ("ção", "NOUN"), # ação
211
+ ("são", "NOUN"), # tensão
212
+ ("dade", "NOUN"), # cidade
213
+ ("ismo", "NOUN"), # realismo
214
+ ("ista", "NOUN"), # realista
215
+ ("oso", "ADJ"), # formoso
216
+ ("osa", "ADJ"), # formosa
217
+ ("vel", "ADJ"), # amável
218
+ ("al", "ADJ"), # nacional
219
+ ("ar", "VERB"), # amar
220
+ ("er", "VERB"), # comer
221
+ ("ir", "VERB"), # partir (careful with 'ir' the verb itself)
222
+ ]
223
+
224
+ # Rule 1: Dictionary Lookup (O(1) speed)
225
+ if lower_word in COMMON_WORDS:
226
+ return COMMON_WORDS[lower_word]
227
+
228
+ # Rule 2: Suffix Morphology
229
+ for suffix, tag in SUFFIX_RULES:
230
+ if lower_word.endswith(suffix):
231
+ # Exception: prevent very short words triggering rules (e.g. "lar" ending in "ar")
232
+ if len(lower_word) > len(suffix):
233
+ return tag
234
+
235
+ # Rule 3: Capitalization (Proper Noun heuristic)
236
+ # If it's not the start of the sentence (hard to know context-free here) but matches Title case
237
+ if word[0].isupper() and word[1:].islower():
238
+ return "PROPN"
239
+
240
+ # Rule 4: Fallback
241
+ return "NOUN"
242
+
243
+ if __name__ == "__main__":
244
+ # Initialize with 'auto' to use the best available engine
245
+ tagger = TugaTagger(engine="auto")
246
+
247
+ text = "O gato preto pulou o muro."
248
+
249
+ print(f"Using engine: {tagger.engine}")
250
+ tags = tagger.tag(text)
251
+
252
+ for word, pos in tags:
253
+ print(f"{word:10} -> {pos}")
@@ -0,0 +1,10 @@
1
+ # START_VERSION_BLOCK
2
+ VERSION_MAJOR = 0
3
+ VERSION_MINOR = 0
4
+ VERSION_BUILD = 1
5
+ VERSION_ALPHA = 0
6
+ # END_VERSION_BLOCK
7
+
8
+ VERSION_STR = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}"
9
+ if VERSION_ALPHA:
10
+ VERSION_STR += f"a{VERSION_ALPHA}"
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: tugatagger
3
+ Version: 0.0.1
4
+ Summary: A unified wrapper for Portuguese POS tagging and benchmarking.
5
+ Home-page: https://github.com/TigreGotico/tugatagger
6
+ Author: JarbasAI
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Classifier: Natural Language :: Portuguese
12
+ Requires-Python: >=3.7
13
+ Provides-Extra: spacy
14
+ Requires-Dist: spacy; extra == "spacy"
15
+ Provides-Extra: lexicon
16
+ Requires-Dist: tugalex; extra == "lexicon"
17
+ Provides-Extra: brill
18
+ Requires-Dist: brill_postagger; extra == "brill"
19
+ Dynamic: author
20
+ Dynamic: classifier
21
+ Dynamic: home-page
22
+ Dynamic: provides-extra
23
+ Dynamic: requires-python
24
+ Dynamic: summary
@@ -0,0 +1,9 @@
1
+ README.md
2
+ setup.py
3
+ tugatagger/__init__.py
4
+ tugatagger/version.py
5
+ tugatagger.egg-info/PKG-INFO
6
+ tugatagger.egg-info/SOURCES.txt
7
+ tugatagger.egg-info/dependency_links.txt
8
+ tugatagger.egg-info/requires.txt
9
+ tugatagger.egg-info/top_level.txt
@@ -0,0 +1,9 @@
1
+
2
+ [brill]
3
+ brill_postagger
4
+
5
+ [lexicon]
6
+ tugalex
7
+
8
+ [spacy]
9
+ spacy
@@ -0,0 +1 @@
1
+ tugatagger