brag-cli 0.1.0__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,14 @@
1
+ Metadata-Version: 2.3
2
+ Name: brag-cli
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: Antonio Henrique Machado
6
+ Author-email: Antonio Henrique Machado <machadoah@proton.me>
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: pydantic>=2.12.5
9
+ Requires-Dist: rich>=15.0.0
10
+ Requires-Dist: tomlkit>=0.15.0
11
+ Requires-Dist: typer>=0.23.1
12
+ Requires-Python: >=3.14
13
+ Description-Content-Type: text/markdown
14
+
File without changes
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "brag-cli"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [{ name = "Antonio Henrique Machado", email = "machadoah@proton.me" }]
7
+ requires-python = ">=3.14"
8
+ dependencies = [
9
+ "httpx>=0.28.1",
10
+ "pydantic>=2.12.5",
11
+ "rich>=15.0.0",
12
+ "tomlkit>=0.15.0",
13
+ "typer>=0.23.1",
14
+ ]
15
+
16
+ [project.scripts]
17
+ brag = "brag_cli.main:app"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.11.6,<0.12.0"]
21
+ build-backend = "uv_build"
File without changes
File without changes
@@ -0,0 +1,39 @@
1
+ import typer
2
+ import json
3
+ from rich.console import Console
4
+ from pathlib import Path
5
+ from ..config import load_secrets, save_secrets
6
+
7
+ ai_app = typer.Typer()
8
+ console = Console()
9
+
10
+
11
+ @ai_app.command("config-provider")
12
+ def config_provider(
13
+ provider: str = typer.Option(..., prompt="Provider (ex: groq, openai, openrouter)"),
14
+ model: str = typer.Option(..., prompt="Model (ex: llama3-8b-8192, gpt-4o-mini)"),
15
+ ):
16
+ """Configura o provedor de IA e modelo no data/profile.json."""
17
+ profile_path = Path.cwd() / "profile.json"
18
+ if not profile_path.exists():
19
+ console.print(
20
+ "[red]Arquivo profile.json não encontrado na raiz. Rode `brag init` primeiro.[/red]"
21
+ )
22
+ raise typer.Exit(1)
23
+
24
+ data = json.loads(profile_path.read_text())
25
+ data["llm"] = {"provider": provider.lower(), "model": model}
26
+ profile_path.write_text(json.dumps(data, indent=2))
27
+ console.print(f"[green]Provedor configurado: {provider} / {model}[/green]")
28
+
29
+
30
+ @ai_app.command("config-key")
31
+ def config_key():
32
+ """Configura a chave do provider no secrets.toml (BYOK)."""
33
+ api_key = typer.prompt("Insira a API Key do seu provedor", hide_input=True)
34
+ secrets = load_secrets()
35
+ secrets["api_key"] = api_key
36
+ save_secrets(secrets)
37
+ console.print(
38
+ "[green]API Key salva com segurança no seu secrets.toml local.[/green]"
39
+ )
File without changes
@@ -0,0 +1,55 @@
1
+ import typer
2
+ import subprocess
3
+ from rich.console import Console
4
+ from datetime import datetime
5
+ from ..config import check_local_workspace
6
+
7
+ svc_app = typer.Typer()
8
+ console = Console()
9
+
10
+ @svc_app.callback(invoke_without_command=True)
11
+ def svc(
12
+ push: bool = typer.Option(False, "--push", help="Faz commit das alterações locais e push para o repositório remoto."),
13
+ pull: bool = typer.Option(False, "--pull", help="Faz pull das alterações mais recentes do repositório remoto.")
14
+ ):
15
+ """Sincroniza o repositório local (abstrai git pull e git push)."""
16
+ if not push and not pull:
17
+ console.print("[yellow]Forneça --push ou --pull[/yellow]")
18
+ return
19
+
20
+ is_valid, msg = check_local_workspace()
21
+ if not is_valid:
22
+ console.print(f"[red]Erro:[/red] {msg}")
23
+ console.print("Rode [cyan]brag init[/cyan] ou entre no diretório correto.")
24
+ raise typer.Exit(1)
25
+
26
+ if pull:
27
+ console.print("[dim]Executando git pull...[/dim]")
28
+ try:
29
+ subprocess.run(["git", "pull"], check=True)
30
+ console.print("[green]Sincronizado com sucesso (pull).[/green]")
31
+ except subprocess.CalledProcessError:
32
+ console.print("[red]Falha ao executar git pull.[/red]")
33
+ raise typer.Exit(1)
34
+
35
+ if push:
36
+ console.print("[dim]Adicionando alterações ao git...[/dim]")
37
+ now = datetime.now()
38
+ commit_msg = f"{now.strftime('%Y-%m-%d %H:%M:%S')} - add entry for {now.strftime('%m')}/{now.strftime('%Y')}"
39
+
40
+ try:
41
+ subprocess.run(["git", "add", "data/"], check=True)
42
+ status = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True)
43
+ if not status.stdout.strip():
44
+ console.print("[yellow]Nenhuma alteração em 'data/' para enviar.[/yellow]")
45
+ return
46
+
47
+ subprocess.run(["git", "commit", "-m", commit_msg], check=True)
48
+ console.print(f"[dim]Commit gerado: '{commit_msg}'[/dim]")
49
+
50
+ console.print("[dim]Executando git push...[/dim]")
51
+ subprocess.run(["git", "push"], check=True)
52
+ console.print("[green]Alterações enviadas com sucesso (push).[/green]")
53
+ except subprocess.CalledProcessError as e:
54
+ console.print(f"[red]Falha ao sincronizar (push). Verifique seu remoto.[/red]")
55
+ raise typer.Exit(1)
@@ -0,0 +1,33 @@
1
+ import json
2
+ from pathlib import Path
3
+ import tomlkit
4
+
5
+ def get_secrets_file() -> Path:
6
+ return Path.cwd() / "secrets.toml"
7
+
8
+ def load_secrets() -> dict:
9
+ secrets_file = get_secrets_file()
10
+ if not secrets_file.exists():
11
+ return {}
12
+ try:
13
+ with secrets_file.open("r") as f:
14
+ return tomlkit.load(f)
15
+ except Exception:
16
+ return {}
17
+
18
+ def save_secrets(secrets: dict):
19
+ secrets_file = get_secrets_file()
20
+ with secrets_file.open("w") as f:
21
+ tomlkit.dump(secrets, f)
22
+
23
+ def check_local_workspace():
24
+ """Valida se o diretório atual é um workspace válido do braguissimo."""
25
+ cwd = Path.cwd()
26
+
27
+ if not (cwd / ".git").exists():
28
+ return False, "O diretório atual não é um repositório git."
29
+
30
+ if not (cwd / "profile.json").exists():
31
+ return False, "Arquivo 'profile.json' não encontrado no diretório atual."
32
+
33
+ return True, "OK"
@@ -0,0 +1,56 @@
1
+ import httpx
2
+ from rich.console import Console
3
+
4
+ console = Console()
5
+
6
+ class LLMClient:
7
+ def __init__(self, provider: str, model: str, api_key: str):
8
+ self.provider = provider.lower()
9
+ self.model = model
10
+ self.api_key = api_key
11
+
12
+ if self.provider == "groq":
13
+ self.base_url = "https://api.groq.com/openai/v1"
14
+ elif self.provider == "openai":
15
+ self.base_url = "https://api.openai.com/v1"
16
+ elif self.provider == "openrouter":
17
+ self.base_url = "https://openrouter.ai/api/v1"
18
+ else:
19
+ # Fallback to standard OpenAI compatibility format
20
+ self.base_url = "https://api.openai.com/v1"
21
+
22
+ def restructure_brag(self, title: str, description: str) -> str:
23
+ prompt = (
24
+ "Atue como um mentor de carreira especialista. Você receberá o título e a descrição de uma "
25
+ "conquista profissional (um 'brag'). Reescreva a descrição para torná-la mais profissional, "
26
+ "clara, focada em impacto e métricas, mas mantenha o texto em primeira pessoa. Não invente dados. "
27
+ "Responda APENAS com o texto reestruturado, sem introduções ou explicações.\n\n"
28
+ f"Título: {title}\n"
29
+ f"Descrição original: {description}"
30
+ )
31
+
32
+ payload = {
33
+ "model": self.model,
34
+ "messages": [{"role": "user", "content": prompt}],
35
+ "temperature": 0.3
36
+ }
37
+
38
+ headers = {
39
+ "Authorization": f"Bearer {self.api_key}",
40
+ "Content-Type": "application/json"
41
+ }
42
+
43
+ with console.status(f"[dim]Reestruturando brag usando {self.provider} ({self.model})...[/dim]"):
44
+ try:
45
+ resp = httpx.post(f"{self.base_url}/chat/completions", json=payload, headers=headers, timeout=30.0)
46
+ resp.raise_for_status()
47
+ data = resp.json()
48
+ return data["choices"][0]["message"]["content"].strip()
49
+ except httpx.HTTPError as e:
50
+ console.print(f"[red]Erro na chamada LLM: {e}[/red]")
51
+ if hasattr(e, 'response') and e.response:
52
+ console.print(f"[dim]{e.response.text}[/dim]")
53
+ return description
54
+ except Exception as e:
55
+ console.print(f"[red]Erro interno no LLM: {e}[/red]")
56
+ return description
@@ -0,0 +1,280 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from datetime import datetime
4
+ import json
5
+ import uuid
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ from .commands.svc import svc_app
10
+ from .commands.ai import ai_app
11
+ from .config import check_local_workspace
12
+
13
+ app = typer.Typer(name="braguissimo", help="CLI para registrar suas conquistas (Local + BYOS).")
14
+ app.add_typer(svc_app, name="svc", help="Sincronização do repositório (push/pull)")
15
+ app.add_typer(ai_app, name="ai", help="Comandos de IA e configuração de BYOK")
16
+ console = Console()
17
+
18
+ @app.command()
19
+ def init():
20
+ """Inicializa um repositório local do braguissimo no diretório atual."""
21
+ cwd = Path.cwd()
22
+
23
+ if (cwd / ".git").exists() and (cwd / "profile.json").exists():
24
+ console.print("[yellow]O repositório já está inicializado neste diretório.[/yellow]")
25
+ return
26
+
27
+ console.print("[dim]Inicializando repositório Git local...[/dim]")
28
+ try:
29
+ subprocess.run(["git", "init"], check=True, capture_output=True)
30
+ except subprocess.CalledProcessError:
31
+ console.print("[red]Falha ao rodar git init. Você tem o git instalado?[/red]")
32
+ raise typer.Exit(1)
33
+
34
+ owner = typer.prompt("Nome de usuário (github username)", default="")
35
+
36
+ # Criar profile.json na raiz
37
+ profile_path = cwd / "profile.json"
38
+ if not profile_path.exists():
39
+ profile_path.write_text(json.dumps({
40
+ "name": owner,
41
+ "created_at": datetime.utcnow().isoformat() + "Z",
42
+ "initialized": True
43
+ }, indent=2))
44
+
45
+ # Criar .gitignore
46
+ gitignore_path = cwd / ".gitignore"
47
+ ignores = ["secrets.toml", ".venv/", "__pycache__/"]
48
+ if not gitignore_path.exists():
49
+ gitignore_path.write_text("\n".join(ignores) + "\n")
50
+ else:
51
+ content = gitignore_path.read_text()
52
+ to_append = [ign for ign in ignores if ign not in content]
53
+ if to_append:
54
+ with gitignore_path.open("a") as f:
55
+ f.write("\n" + "\n".join(to_append) + "\n")
56
+
57
+ console.print("[green]Repositório inicializado com sucesso![/green]")
58
+ console.print("Adicione seu remote com: [cyan]git remote add origin <sua-url>[/cyan]")
59
+
60
+ @app.command()
61
+ def add(title: str = typer.Option(None, "--title", "-t", help="Título da conquista"),
62
+ description: str = typer.Option(None, "--message", "-m", help="Descrição livre"),
63
+ tags: str = typer.Option("", "--tags", help="Tags separadas por vírgula")):
64
+ """Adiciona um novo brag localmente."""
65
+ is_valid, msg = check_local_workspace()
66
+ if not is_valid:
67
+ console.print(f"[red]{msg}[/red]")
68
+ console.print("Rode [cyan]brag init[/cyan] primeiro neste diretório.")
69
+ raise typer.Exit(1)
70
+
71
+ date_input = typer.prompt("Data (DD/MM/AAAA) [Deixe vazio para hoje]", default="", show_default=False)
72
+ if not date_input:
73
+ target_date = datetime.utcnow()
74
+ else:
75
+ try:
76
+ target_date = datetime.strptime(date_input, "%d/%m/%Y")
77
+ except ValueError:
78
+ console.print("[red]Formato de data inválido. Use DD/MM/AAAA.[/red]")
79
+ raise typer.Exit(1)
80
+
81
+ if not title:
82
+ title = typer.prompt("Título da conquista")
83
+ if not description:
84
+ description = typer.prompt("Descrição")
85
+ if not tags:
86
+ tags = typer.prompt("Tags (separadas por vírgula)", default="", show_default=False)
87
+
88
+ # AI Reestruturação
89
+ from .llm import LLMClient
90
+ from .config import load_secrets
91
+
92
+ profile_path = Path.cwd() / "profile.json"
93
+ secrets = load_secrets()
94
+ api_key = secrets.get("api_key")
95
+
96
+ if api_key and profile_path.exists():
97
+ try:
98
+ profile_data = json.loads(profile_path.read_text())
99
+ llm_config = profile_data.get("llm")
100
+ if llm_config and llm_config.get("provider") and llm_config.get("model"):
101
+ want_ai = typer.confirm("Você quer reestruturar o brag com IA?", default=True)
102
+ if want_ai:
103
+ client = LLMClient(llm_config["provider"], llm_config["model"], api_key)
104
+ new_desc = client.restructure_brag(title, description)
105
+ console.print("\n[dim]--- Texto sugerido ---[/dim]")
106
+ console.print(new_desc)
107
+ console.print("[dim]------------------------[/dim]\n")
108
+ if typer.confirm("Deseja usar essa nova descrição?", default=True):
109
+ description = new_desc
110
+ except Exception as e:
111
+ console.print(f"[dim]Falha ao carregar IA: {e}[/dim]")
112
+
113
+ tag_list = [t.strip() for t in tags.split(",") if t.strip()]
114
+
115
+ brag = {
116
+ "id": str(uuid.uuid4()),
117
+ "date": target_date.isoformat() + "Z",
118
+ "title": title,
119
+ "description": description,
120
+ "tags": tag_list
121
+ }
122
+
123
+ month_dir = Path.cwd() / "data" / "entries" / target_date.strftime('%Y')
124
+ month_dir.mkdir(parents=True, exist_ok=True)
125
+
126
+ path = month_dir / f"{target_date.strftime('%m')}.json"
127
+
128
+ if path.exists():
129
+ data = json.loads(path.read_text())
130
+ data.setdefault("entries", []).append(brag)
131
+ else:
132
+ data = {
133
+ "month": target_date.strftime('%B'),
134
+ "year": target_date.year,
135
+ "entries": [brag]
136
+ }
137
+
138
+ path.write_text(json.dumps(data, indent=2))
139
+ console.print(f"[green]Brag adicionado localmente! (ID: {brag['id']})[/green]")
140
+ console.print("Use [cyan]brag svc --push[/cyan] para enviar ao repositório remoto.")
141
+
142
+ @app.command()
143
+ def list(month: str = typer.Option(None, help="Mês no formato MM"),
144
+ year: str = typer.Option(None, help="Ano no formato YYYY")):
145
+ """Lista os brags locais do período."""
146
+ is_valid, msg = check_local_workspace()
147
+ if not is_valid:
148
+ console.print(f"[red]{msg}[/red]")
149
+ raise typer.Exit(1)
150
+
151
+ now = datetime.now()
152
+ m = month or now.strftime('%m')
153
+ y = year or now.strftime('%Y')
154
+
155
+ path = Path.cwd() / "data" / "entries" / y / f"{m}.json"
156
+
157
+ if not path.exists():
158
+ console.print("[yellow]Nenhum brag encontrado localmente para este período.[/yellow]")
159
+ return
160
+
161
+ data = json.loads(path.read_text())
162
+ entries = data.get("entries", [])
163
+
164
+ if not entries:
165
+ console.print("[yellow]Nenhum brag encontrado localmente para este período.[/yellow]")
166
+ return
167
+
168
+ for entry in entries:
169
+ console.print(f"\n[cyan]{entry['title']}[/cyan] ({entry.get('date', '')})")
170
+ console.print(f"ID: [dim]{entry['id']}[/dim]")
171
+ console.print(f"{entry['description']}")
172
+ if entry.get('tags'):
173
+ console.print(f"Tags: {', '.join(entry['tags'])}")
174
+
175
+ @app.command()
176
+ def edit(id: str):
177
+ """Permite editar um brag local específico."""
178
+ console.print(f"[yellow]O arquivo é local! Você pode editá-lo diretamente em `data/entries/` com seu editor preferido.[/yellow]")
179
+
180
+ @app.command()
181
+ def delete(id: str):
182
+ """Remove um brag específico."""
183
+ console.print(f"[yellow]O arquivo é local! Você pode remover a entrada do JSON em `data/entries/`.[/yellow]")
184
+
185
+ @app.command()
186
+ def status():
187
+ """Exibe o status do repositório local e métricas rápidas."""
188
+ is_valid, msg = check_local_workspace()
189
+ if not is_valid:
190
+ console.print(f"[red]{msg}[/red]")
191
+ raise typer.Exit(1)
192
+
193
+ from .stats import get_git_info, get_all_brags, get_current_month_count
194
+ from rich.table import Table
195
+ from rich.panel import Panel
196
+
197
+ profile_path = Path.cwd() / "profile.json"
198
+ owner = "Desconhecido"
199
+ if profile_path.exists():
200
+ try:
201
+ owner = json.loads(profile_path.read_text()).get("name", "Desconhecido")
202
+ except Exception:
203
+ pass
204
+
205
+ git_info = get_git_info()
206
+ brags = get_all_brags()
207
+ current_count = get_current_month_count(brags)
208
+
209
+ table = Table(show_header=False, box=None, expand=True)
210
+ table.add_column("Chave", style="dim", width=25)
211
+ table.add_column("Valor", style="bold")
212
+
213
+ table.add_row("Usuário", f"[cyan]{owner}[/cyan]")
214
+ table.add_row("Repositório Remoto", f"{git_info['remote_url']}")
215
+
216
+ changes_str = "[yellow]Existem alterações não sincronizadas[/yellow]" if git_info['has_changes'] else "[green]Tudo sincronizado[/green]"
217
+ table.add_row("Status Git", changes_str)
218
+
219
+ table.add_row("Brags neste mês", f"{current_count}")
220
+ table.add_row("Brags no total", f"{len(brags)}")
221
+
222
+ panel = Panel(table, title="📊 Braguíssimo Status", border_style="blue", expand=True)
223
+ console.print(panel)
224
+
225
+ @app.command()
226
+ def analytics():
227
+ """Exibe um dashboard com métricas detalhadas de uso."""
228
+ is_valid, msg = check_local_workspace()
229
+ if not is_valid:
230
+ console.print(f"[red]{msg}[/red]")
231
+ raise typer.Exit(1)
232
+
233
+ from .stats import get_all_brags, get_monthly_stats, get_tag_stats
234
+ from rich.table import Table
235
+ from rich.panel import Panel
236
+
237
+ brags = get_all_brags()
238
+ if not brags:
239
+ console.print("[yellow]Você ainda não registrou nenhum brag para analisar.[/yellow]")
240
+ return
241
+
242
+ counts, top_month, top_count = get_monthly_stats(brags)
243
+ tags_freq = get_tag_stats(brags)
244
+
245
+ # 1. Tabela Geral
246
+ overview_table = Table(title="Visão Geral", show_header=False, box=None, expand=True)
247
+ overview_table.add_column("Métrica", style="dim")
248
+ overview_table.add_column("Valor", style="bold")
249
+ overview_table.add_row("Total de Entradas", str(len(brags)))
250
+ if top_month:
251
+ overview_table.add_row("Mês Mais Produtivo", f"{top_month} ({top_count} brags)")
252
+
253
+ # 2. Distribuição de Tags
254
+ tag_table = Table(title="Top Tags", show_edge=False, header_style="dim", expand=True)
255
+ tag_table.add_column("Tag")
256
+ tag_table.add_column("Uso", justify="right")
257
+
258
+ for tag, freq in tags_freq[:5]:
259
+ tag_table.add_row(f"[cyan]{tag}[/cyan]", str(freq))
260
+
261
+ if not tags_freq:
262
+ tag_table.add_row("Nenhuma tag", "0")
263
+
264
+ # 3. Timeline de Meses
265
+ timeline_table = Table(title="Atividade por Mês", show_edge=False, header_style="dim", expand=True)
266
+ timeline_table.add_column("Mês")
267
+ timeline_table.add_column("Volume", justify="left")
268
+ timeline_table.add_column("Qtd", justify="right")
269
+
270
+ for month_key in sorted(counts.keys()):
271
+ qtd = counts[month_key]
272
+ bar = "█" * min(qtd, 40)
273
+ timeline_table.add_row(month_key, f"[green]{bar}[/green]", str(qtd))
274
+
275
+ console.print(Panel(overview_table, border_style="blue", expand=True))
276
+ console.print(Panel(tag_table, border_style="cyan", expand=True))
277
+ console.print(Panel(timeline_table, border_style="green", expand=True))
278
+
279
+ if __name__ == "__main__":
280
+ app()
File without changes
@@ -0,0 +1,83 @@
1
+ import json
2
+ import subprocess
3
+ from pathlib import Path
4
+ from datetime import datetime
5
+ from collections import Counter, defaultdict
6
+
7
+ def get_git_info():
8
+ """Retorna um dicionário com status git (has_changes, remote_url)."""
9
+ info = {"has_changes": False, "remote_url": "Sem remote configurado"}
10
+ try:
11
+ # Pega a origin URL
12
+ remote = subprocess.run(["git", "remote", "get-url", "origin"], capture_output=True, text=True)
13
+ if remote.returncode == 0:
14
+ info["remote_url"] = remote.stdout.strip()
15
+
16
+ # Pega as modificações locais em data/ ou em tudo
17
+ status = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True)
18
+ if status.stdout.strip():
19
+ info["has_changes"] = True
20
+ except FileNotFoundError:
21
+ pass
22
+ except subprocess.CalledProcessError:
23
+ pass
24
+ return info
25
+
26
+ def get_all_brags():
27
+ """Lê todos os JSONs e retorna a lista combinada de todos os brags."""
28
+ cwd = Path.cwd()
29
+ entries_dir = cwd / "data" / "entries"
30
+
31
+ all_brags = []
32
+ if not entries_dir.exists():
33
+ return all_brags
34
+
35
+ for json_file in entries_dir.rglob("*.json"):
36
+ try:
37
+ data = json.loads(json_file.read_text())
38
+ for entry in data.get("entries", []):
39
+ all_brags.append(entry)
40
+ except Exception:
41
+ pass
42
+
43
+ # Sort por date descendente
44
+ all_brags.sort(key=lambda x: x.get("date", ""), reverse=True)
45
+ return all_brags
46
+
47
+ def get_monthly_stats(brags):
48
+ """
49
+ Agrupa os brags por YYYY-MM.
50
+ Retorna (timeline_dict, top_month, top_month_count).
51
+ """
52
+ counts = defaultdict(int)
53
+ for b in brags:
54
+ date_str = b.get("date", "")
55
+ if len(date_str) >= 7:
56
+ month_key = date_str[:7]
57
+ counts[month_key] += 1
58
+
59
+ top_month = None
60
+ top_count = 0
61
+ if counts:
62
+ top_month = max(counts, key=counts.get)
63
+ top_count = counts[top_month]
64
+
65
+ return counts, top_month, top_count
66
+
67
+ def get_tag_stats(brags):
68
+ """Calcula frequência de tags."""
69
+ tag_counter = Counter()
70
+ for b in brags:
71
+ for t in b.get("tags", []):
72
+ if t:
73
+ tag_counter[t] += 1
74
+ return tag_counter.most_common()
75
+
76
+ def get_current_month_count(brags):
77
+ now_prefix = datetime.now().strftime("%Y-%m")
78
+ count = 0
79
+ for b in brags:
80
+ date_str = b.get("date", "")
81
+ if date_str.startswith(now_prefix):
82
+ count += 1
83
+ return count