Pular para o conteúdo

Cap. 20 · Código

Google Meu Negócio: sync + coleta de avaliações

Mantenha a ficha do Google Meu Negócio (Google Business Profile) em dia — nome, categorias, horário, telefone e site — e colete as avaliações automaticamente. Tudo é guardado em SQLite (stdlib), com cálculo da aggregateRating e JSON-LD de Review pronto para o site. Roda ponta a ponta com dados fictícios — sem credenciais.
Python 3 · SQLite · OAuth só para dados reais

Execução

Rodar em 3 comandos

Requer credencial: apenas gmn_sync.py (sync da ficha) e a coleta ao vivo em reviews_collector.py precisam de OAuth do Google Business Profile (GBP_OAUTH_CLIENT_ID, GBP_OAUTH_CLIENT_SECRET, GBP_OAUTH_REFRESH_TOKEN, GBP_ACCOUNT_ID). Todo o resto — normalização, storage, aggregateRating, JSON-LD e o demo — roda sem nenhum segredo.

terminal
$ python3 --version # confirme Python 3
$ python3 demo.py # pipeline completo com dados fictícios
$ python3 reviews_collector.py --demo # mesma coleta via CLI (--pretty opcional)

Saída esperada (python3 demo.py)

demo.py → stdout
============================================================
COLETA DE AVALIAÇÕES — dados fictícios (sem credenciais)
Review collection — fictional data (no credentials)
============================================================
[demo] 6 avaliações fictícias armazenadas / fictional reviews stored
Avaliações (reviews)
----------------------------------------------------
★★★★★ Marina S. Atendimento impecável e entrega no prazo. Recomendo demais!
★★★★☆ João P. Ótima experiência, só achei o estacionamento apertado.
★★★★★ Ana Beatriz Equipe muito atenciosa, resolveram tudo rapidinho.
★★★☆☆ Carlos M. Bom, mas a fila estava grande no horário de pico.
★★★★★ Fernanda L. Melhor da região, sem dúvida. Voltarei sempre.
★★★★☆ Roberto A. (sem comentário / no comment)
Nota agregada (aggregate rating)
----------------------------------------------------
ratingValue = 4.3 reviewCount = 6 (best 5 / worst 1)
AggregateRating / Review JSON-LD
----------------------------------------------------
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Padaria do Bairro",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": 4.3,
"reviewCount": 6,
"bestRating": 5,
"worstRating": 1
},
"review": [
{
"@type": "Review",
"author": {
"@type": "Person",
"name": "Marina S."
},
"reviewRating": {
"@type": "Rating",
"ratingValue": 5,
"bestRating": 5,
"worstRating": 1
},
"datePublished": "2026-06-28T14:03:00Z",
"reviewBody": "Atendimento impecável e entrega no prazo. Recomendo demais!"
},
{
"@type": "Review",
"author": {
"@type": "Person",
"name": "João P."
},
"reviewRating": {
"@type": "Rating",
"ratingValue": 4,
"bestRating": 5,
"worstRating": 1
},
"datePublished": "2026-06-25T09:41:00Z",
"reviewBody": "Ótima experiência, só achei o estacionamento apertado."
},
{
"@type": "Review",
"author": {
"@type": "Person",
"name": "Ana Beatriz"
},
"reviewRating": {
"@type": "Rating",
"ratingValue": 5,
"bestRating": 5,
"worstRating": 1
},
"datePublished": "2026-06-20T18:22:00Z",
"reviewBody": "Equipe muito atenciosa, resolveram tudo rapidinho."
},
{
"@type": "Review",
"author": {
"@type": "Person",
"name": "Carlos M."
},
"reviewRating": {
"@type": "Rating",
"ratingValue": 3,
"bestRating": 5,
"worstRating": 1
},
"datePublished": "2026-06-15T12:10:00Z",
"reviewBody": "Bom, mas a fila estava grande no horário de pico."
},
{
"@type": "Review",
"author": {
"@type": "Person",
"name": "Fernanda L."
},
"reviewRating": {
"@type": "Rating",
"ratingValue": 5,
"bestRating": 5,
"worstRating": 1
},
"datePublished": "2026-06-10T20:05:00Z",
"reviewBody": "Melhor da região, sem dúvida. Voltarei sempre."
},
{
"@type": "Review",
"author": {
"@type": "Person",
"name": "Roberto A."
},
"reviewRating": {
"@type": "Rating",
"ratingValue": 4,
"bestRating": 5,
"worstRating": 1
},
"datePublished": "2026-06-05T08:30:00Z"
}
]
}
============================================================

Código-fonte

Código do módulo

demo.py é o caminho sem credencial. gmn_sync.py e as partes de coleta ao vivo de reviews_collector.py precisam de OAuth do Google Business Profile (ver o aviso de credencial acima).

demo.py
#!/usr/bin/env python3
"""Cap. 20 - Standalone offline demo of the GBP review pipeline (ZERO credentials).
Runs the whole review-collection pipeline with FICTIONAL data:
1. Creates an in-memory SQLite DB via ``reviews_collector``.
2. Seeds ~6 fictional reviews (no API, no network, no credentials).
3. Prints a per-review star breakdown and the computed ``aggregateRating``.
4. Prints the schema.org AggregateRating / Review JSON-LD.
Run it standalone::
python3 demo.py
Nothing here touches Google Business Profile - the reviews are made up so you
can see the aggregate rating and JSON-LD working before wiring real OAuth
credentials into ``gmn_sync.py`` / ``reviews_collector.fetch_reviews``.
Part of the "living code" companion repository for the book
"Aparecer ou Sumir" / "Be Seen or Be Forgotten".
"""
from __future__ import annotations
import json
import reviews_collector as rc
BUSINESS_NAME = "Padaria do Bairro"
def main() -> None:
conn = rc.connect(":memory:")
rc.init_db(conn)
written = rc.collect(conn, rc.SAMPLE_REVIEWS)
print("=" * 60)
print(" COLETA DE AVALIAÇÕES — dados fictícios (sem credenciais)")
print(" Review collection — fictional data (no credentials)")
print("=" * 60)
print(f"\n[demo] {written} avaliações fictícias armazenadas / fictional reviews stored")
# Per-review breakdown (stars + author + short text).
print("\n Avaliações (reviews)")
print(" " + "-" * 52)
for r in rc.all_reviews(conn):
stars = "★" * int(r["star_rating"]) + "☆" * (5 - int(r["star_rating"]))
author = r["author"] or "Anônimo"
text = r["text"] or "(sem comentário / no comment)"
print(f" {stars} {author:<14} {text}")
# Aggregate rating.
agg = rc.aggregate_rating(conn)
print("\n Nota agregada (aggregate rating)")
print(" " + "-" * 52)
print(
f" ratingValue = {agg['ratingValue']} "
f"reviewCount = {agg['reviewCount']} "
f"(best {agg['bestRating']} / worst {agg['worstRating']})"
)
# JSON-LD.
jsonld = rc.build_jsonld(conn, business_name=BUSINESS_NAME)
print("\n AggregateRating / Review JSON-LD")
print(" " + "-" * 52)
print(json.dumps(jsonld, ensure_ascii=False, indent=2))
print("=" * 60)
conn.close()
if __name__ == "__main__":
main()
reviews_collector.py
#!/usr/bin/env python3
"""Cap. 20 - Automatic review collection for Google Business Profile.
Fetches reviews for a business location, normalizes them into a common shape,
stores them in SQLite (stdlib ``sqlite3``), computes an ``aggregateRating`` and
emits schema.org ``Review`` + ``AggregateRating`` JSON-LD you can drop into a
page to show star ratings in search results.
Two ways to run:
* ``--demo`` : loads FICTIONAL sample reviews (inline), stores them in an
in-memory DB, prints the aggregate rating and the JSON-LD.
No credentials, no network.
* (live) : ``fetch_reviews()`` hits the GBP API. That path is a
credential-gated stub — see the REQUER CREDENCIAL block.
Usage::
python3 reviews_collector.py --demo
python3 reviews_collector.py --demo --db reviews.db --pretty
============================================================================
REQUER CREDENCIAL / REQUIRES CREDENTIALS
---------------------------------------------------------------------------
Only ``fetch_reviews()`` (the live GBP call) needs OAuth credentials, read
from ``os.environ`` via ``gmn_sync``. Everything else - normalization,
storage, aggregate rating and JSON-LD - runs with zero credentials, which is
exactly what ``--demo`` exercises.
============================================================================
Part of the "living code" companion repository for the book
"Aparecer ou Sumir" / "Be Seen or Be Forgotten".
"""
from __future__ import annotations
import argparse
import json
import sqlite3
from typing import Dict, List, Optional
# ---------------------------------------------------------------------------
# Fictional sample reviews used by --demo (and by demo.py). Zero credentials.
# star_rating is 1..5; text is optional. Dates are ISO-8601.
# ---------------------------------------------------------------------------
SAMPLE_REVIEWS: List[Dict[str, object]] = [
{
"review_id": "demo-1",
"author": "Marina S.",
"star_rating": 5,
"text": "Atendimento impecável e entrega no prazo. Recomendo demais!",
"create_time": "2026-06-28T14:03:00Z",
},
{
"review_id": "demo-2",
"author": "João P.",
"star_rating": 4,
"text": "Ótima experiência, só achei o estacionamento apertado.",
"create_time": "2026-06-25T09:41:00Z",
},
{
"review_id": "demo-3",
"author": "Ana Beatriz",
"star_rating": 5,
"text": "Equipe muito atenciosa, resolveram tudo rapidinho.",
"create_time": "2026-06-20T18:22:00Z",
},
{
"review_id": "demo-4",
"author": "Carlos M.",
"star_rating": 3,
"text": "Bom, mas a fila estava grande no horário de pico.",
"create_time": "2026-06-15T12:10:00Z",
},
{
"review_id": "demo-5",
"author": "Fernanda L.",
"star_rating": 5,
"text": "Melhor da região, sem dúvida. Voltarei sempre.",
"create_time": "2026-06-10T20:05:00Z",
},
{
"review_id": "demo-6",
"author": "Roberto A.",
"star_rating": 4,
"text": None, # a review with a star rating but no written comment
"create_time": "2026-06-05T08:30:00Z",
},
]
# Map the GBP API star-rating enum to an integer 1..5.
_STAR_ENUM = {"ONE": 1, "TWO": 2, "THREE": 3, "FOUR": 4, "FIVE": 5}
# ---------------------------------------------------------------------------
# Storage (SQLite, stdlib) — fully functional, no credentials.
# ---------------------------------------------------------------------------
def connect(db_path: str = ":memory:") -> sqlite3.Connection:
"""Open a SQLite connection with row access by column name."""
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
return conn
def init_db(conn: sqlite3.Connection) -> None:
"""Create the reviews table if missing.
UNIQUE(review_id) makes ingestion idempotent: re-collecting the same review
updates it in place instead of duplicating rows.
"""
conn.execute(
"""
CREATE TABLE IF NOT EXISTS reviews (
review_id TEXT PRIMARY KEY,
author TEXT,
star_rating INTEGER NOT NULL,
text TEXT,
create_time TEXT
)
"""
)
conn.commit()
def store_reviews(conn: sqlite3.Connection, reviews: List[Dict[str, object]]) -> int:
"""Upsert normalized reviews keyed by review_id. Returns rows processed."""
prepared = [
(
r["review_id"],
r.get("author"),
int(r["star_rating"]),
r.get("text"),
r.get("create_time"),
)
for r in reviews
]
conn.executemany(
"""
INSERT INTO reviews (review_id, author, star_rating, text, create_time)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(review_id) DO UPDATE SET
author = excluded.author,
star_rating = excluded.star_rating,
text = excluded.text,
create_time = excluded.create_time
""",
prepared,
)
conn.commit()
return len(prepared)
def all_reviews(conn: sqlite3.Connection) -> List[sqlite3.Row]:
"""Return every stored review, newest first."""
cur = conn.execute(
"SELECT review_id, author, star_rating, text, create_time "
"FROM reviews ORDER BY create_time DESC"
)
return cur.fetchall()
# ---------------------------------------------------------------------------
# Normalization + aggregate rating + JSON-LD — no credentials.
# ---------------------------------------------------------------------------
def normalize_review(raw: Dict[str, object]) -> Dict[str, object]:
"""Normalize a raw GBP review payload into our common shape.
Accepts both the GBP API shape (``starRating`` enum, ``reviewId``,
``comment``, ``reviewer.displayName``) and our already-normalized shape,
so the demo rows pass through untouched.
"""
if "star_rating" in raw: # already normalized (e.g. SAMPLE_REVIEWS)
return {
"review_id": str(raw.get("review_id", "")),
"author": raw.get("author"),
"star_rating": int(raw["star_rating"]),
"text": raw.get("text"),
"create_time": raw.get("create_time"),
}
# GBP API shape.
reviewer = raw.get("reviewer") or {}
star = raw.get("starRating", "")
return {
"review_id": str(raw.get("reviewId", "")),
"author": reviewer.get("displayName") if isinstance(reviewer, dict) else None,
"star_rating": _STAR_ENUM.get(str(star), 0),
"text": raw.get("comment"),
"create_time": raw.get("createTime"),
}
def aggregate_rating(conn: sqlite3.Connection) -> Dict[str, object]:
"""Compute the aggregate rating from all stored reviews.
Returns a dict with ``ratingValue`` (mean, 1 decimal), ``reviewCount`` and
the ``bestRating`` / ``worstRating`` bounds.
"""
cur = conn.execute("SELECT COUNT(*) AS n, AVG(star_rating) AS avg FROM reviews")
row = cur.fetchone()
count = int(row["n"] or 0)
mean = round(float(row["avg"]), 1) if count else 0.0
return {
"ratingValue": mean,
"reviewCount": count,
"bestRating": 5,
"worstRating": 1,
}
def build_jsonld(
conn: sqlite3.Connection,
business_name: str = "Seu Negócio",
) -> Dict[str, object]:
"""Build a schema.org LocalBusiness graph with AggregateRating + Reviews."""
agg = aggregate_rating(conn)
reviews_ld = []
for r in all_reviews(conn):
review_ld: Dict[str, object] = {
"@type": "Review",
"author": {"@type": "Person", "name": r["author"] or "Anônimo"},
"reviewRating": {
"@type": "Rating",
"ratingValue": r["star_rating"],
"bestRating": 5,
"worstRating": 1,
},
}
if r["create_time"]:
review_ld["datePublished"] = r["create_time"]
if r["text"]:
review_ld["reviewBody"] = r["text"]
reviews_ld.append(review_ld)
return {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": business_name,
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": agg["ratingValue"],
"reviewCount": agg["reviewCount"],
"bestRating": agg["bestRating"],
"worstRating": agg["worstRating"],
},
"review": reviews_ld,
}
# ---------------------------------------------------------------------------
# Live GBP fetch — REQUER CREDENCIAL / REQUIRES CREDENTIALS.
# ---------------------------------------------------------------------------
def fetch_reviews(location_id: str, service: Optional[object] = None) -> List[Dict[str, object]]:
"""Fetch reviews for ``location_id`` from the GBP API (live call).
TODO: credential — with an authorized ``service`` from
``gmn_sync.authenticate()``::
resp = service.accounts().locations().reviews().list(
parent=f"{GBP_ACCOUNT_ID}/{location_id}",
).execute()
return [normalize_review(r) for r in resp.get("reviews", [])]
Reviews come paginated; follow ``nextPageToken`` until exhausted. Without
credentials this raises ``NotImplementedError`` — use ``--demo`` to run the
rest of the pipeline offline.
"""
import gmn_sync # local import so --demo never requires credentials
if service is None and not gmn_sync._have_oauth():
raise NotImplementedError(
"fetch_reviews() needs an authenticated GBP session. "
"Set the GBP_OAUTH_* env vars, or run with --demo."
)
# TODO: credential — replace with the real reviews().list() call.
raise NotImplementedError("GBP reviews.list not implemented yet.")
# ---------------------------------------------------------------------------
# Pipeline + CLI
# ---------------------------------------------------------------------------
def collect(
conn: sqlite3.Connection,
reviews: List[Dict[str, object]],
) -> int:
"""Normalize + store a batch of reviews. Returns rows written."""
normalized = [normalize_review(r) for r in reviews]
return store_reviews(conn, normalized)
def print_summary(conn: sqlite3.Connection, jsonld: Dict[str, object], pretty: bool) -> None:
"""Print a small text summary of the aggregate rating + the JSON-LD."""
agg = aggregate_rating(conn)
stars = "★" * int(round(agg["ratingValue"]))
print(
f"Aggregate rating: {agg['ratingValue']} {stars} "
f"({agg['reviewCount']} reviews)"
)
print("\nReview / AggregateRating JSON-LD:")
print(json.dumps(jsonld, ensure_ascii=False, indent=2 if pretty else None))
def main(argv: Optional[List[str]] = None) -> int:
parser = argparse.ArgumentParser(description="GBP review collector.")
parser.add_argument(
"--demo",
action="store_true",
help="Load fictional sample reviews (no credentials, no network).",
)
parser.add_argument(
"--location-id",
default="locations/DEMO",
help="GBP location resource name (used only for the live path).",
)
parser.add_argument(
"--db",
default=":memory:",
help="SQLite path (default: in-memory).",
)
parser.add_argument(
"--business-name",
default="Padaria do Bairro",
help="Business name to embed in the JSON-LD.",
)
parser.add_argument(
"--pretty",
action="store_true",
help="Pretty-print the JSON-LD.",
)
args = parser.parse_args(argv)
conn = connect(args.db)
init_db(conn)
if args.demo:
written = collect(conn, SAMPLE_REVIEWS)
print(f"[demo] {written} avaliações fictícias armazenadas / fictional reviews stored\n")
else:
reviews = fetch_reviews(args.location_id) # credential-gated
written = collect(conn, reviews)
print(f"[live] {written} reviews stored from {args.location_id}\n")
jsonld = build_jsonld(conn, business_name=args.business_name)
print_summary(conn, jsonld, pretty=args.pretty)
conn.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
gmn_sync.py
#!/usr/bin/env python3
"""Cap. 20 - Google Meu Negócio / Google Business Profile sync - STRUCTURE.
Pushes and keeps your business location(s) in sync with the Google Business
Profile (GBP, a.k.a. "Google Meu Negócio") API: name, categories, opening
hours, phone and website. The flow is:
authenticate() -> list_locations() -> update_location(id, patch)
============================================================================
REQUER CREDENCIAL / REQUIRES CREDENTIALS
---------------------------------------------------------------------------
The GBP API only returns/accepts live data through an authenticated OAuth
session tied to the Google account that manages the business. Every value
below is read ONLY from ``os.environ`` - no secret is ever hardcoded. Until
the credentials are set, ``authenticate()`` / ``list_locations()`` /
``update_location()`` raise ``NotImplementedError`` with setup guidance, but
the module stays importable and ``py_compile``-clean.
There is NO offline sample path here on purpose: writing to GBP is a real,
side-effecting operation. To see the companion pipeline run offline with
fictional data, use ``reviews_collector.py --demo`` or ``demo.py``.
============================================================================
Part of the "living code" companion repository for the book
"Aparecer ou Sumir" / "Be Seen or Be Forgotten".
"""
from __future__ import annotations
import os
from typing import Dict, List, Optional
# ---------------------------------------------------------------------------
# TODO: credential — all values come from the environment. Nothing is hardcoded.
# Set these before any real GBP call can succeed.
# ---------------------------------------------------------------------------
# TODO: credential — OAuth client id/secret for a Google Cloud project with the
# "Business Profile API" (mybusinessbusinessinformation) enabled.
GBP_OAUTH_CLIENT_ID = os.environ.get("GBP_OAUTH_CLIENT_ID", "")
GBP_OAUTH_CLIENT_SECRET = os.environ.get("GBP_OAUTH_CLIENT_SECRET", "")
# TODO: credential — a long-lived OAuth refresh token for the managing account.
GBP_OAUTH_REFRESH_TOKEN = os.environ.get("GBP_OAUTH_REFRESH_TOKEN", "")
# TODO: credential — the account resource name, e.g. "accounts/1234567890".
GBP_ACCOUNT_ID = os.environ.get("GBP_ACCOUNT_ID", "")
# OAuth scope needed to read and write business information.
GBP_SCOPE = "https://www.googleapis.com/auth/business.manage"
LocationPatch = Dict[str, object]
def _have_oauth() -> bool:
"""True only when every OAuth credential is present in the environment."""
return bool(
GBP_OAUTH_CLIENT_ID
and GBP_OAUTH_CLIENT_SECRET
and GBP_OAUTH_REFRESH_TOKEN
and GBP_ACCOUNT_ID
)
def authenticate() -> object:
"""Build an authorized GBP API session from the OAuth credentials.
TODO: credential — steps once you have an OAuth client + refresh token:
1. pip install google-api-python-client google-auth google-auth-oauthlib
2. Exchange the refresh token for an access token::
from google.oauth2.credentials import Credentials
creds = Credentials(
None,
refresh_token=GBP_OAUTH_REFRESH_TOKEN,
client_id=GBP_OAUTH_CLIENT_ID,
client_secret=GBP_OAUTH_CLIENT_SECRET,
token_uri="https://oauth2.googleapis.com/token",
scopes=[GBP_SCOPE],
)
3. Build the service::
from googleapiclient.discovery import build
service = build("mybusinessbusinessinformation", "v1", credentials=creds)
and return it for ``list_locations`` / ``update_location`` to use.
"""
if not _have_oauth():
print(
"[gmn_sync] Missing OAuth credentials. Set GBP_OAUTH_CLIENT_ID, "
"GBP_OAUTH_CLIENT_SECRET, GBP_OAUTH_REFRESH_TOKEN and GBP_ACCOUNT_ID "
"in your environment. See the authenticate() docstring for the "
"full setup steps."
)
raise NotImplementedError(
"authenticate() needs the four GBP_OAUTH_* / GBP_ACCOUNT_ID env vars."
)
# TODO: credential — replace with the real Credentials + build() call.
raise NotImplementedError("GBP OAuth session build not implemented yet.")
def list_locations(service: Optional[object] = None) -> List[Dict[str, object]]:
"""List the business locations managed under ``GBP_ACCOUNT_ID``.
TODO: credential — with an authorized ``service`` from ``authenticate()``::
resp = service.accounts().locations().list(
parent=GBP_ACCOUNT_ID,
readMask="name,title,categories,phoneNumbers,websiteUri,regularHours",
).execute()
return resp.get("locations", [])
Each returned location has a ``name`` like "locations/0987654321" that you
pass to :func:`update_location`.
"""
if service is None and not _have_oauth():
print(
"[gmn_sync] Cannot list locations without an authenticated session. "
"Call authenticate() once the GBP_OAUTH_* env vars are set."
)
raise NotImplementedError(
"list_locations() needs an authenticated GBP session."
)
# TODO: credential — replace with the real accounts().locations().list() call.
raise NotImplementedError("GBP locations.list not implemented yet.")
def update_location(
location_id: str,
patch: LocationPatch,
service: Optional[object] = None,
) -> Dict[str, object]:
"""Push a partial update (name, categories, hours, phone, website) to GBP.
Args:
location_id: resource name of the location, e.g. "locations/0987654321".
patch: the fields to change. Only keys present are written, e.g.::
{
"title": "Padaria do Bairro",
"phoneNumbers": {"primaryPhone": "+55 51 99999-0000"},
"websiteUri": "https://padariadobairro.com.br",
"categories": {"primaryCategory": {"name": "categories/bakery"}},
"regularHours": {...},
}
service: an authorized session from :func:`authenticate` (optional; if
omitted the function will require the OAuth env vars to be present).
TODO: credential — the real write is a PATCH with an updateMask listing only
the fields you changed::
update_mask = ",".join(patch.keys())
return service.locations().patch(
name=location_id,
updateMask=update_mask,
body=patch,
).execute()
GBP validates edits and some categories/hours changes may be reviewed by
Google before going live.
"""
if not location_id:
raise ValueError("location_id is required, e.g. 'locations/0987654321'.")
if not isinstance(patch, dict) or not patch:
raise ValueError("patch must be a non-empty dict of fields to update.")
if service is None and not _have_oauth():
print(
"[gmn_sync] Cannot update location without an authenticated session. "
f"Would PATCH {location_id} with fields: {', '.join(sorted(patch))}. "
"Set the GBP_OAUTH_* env vars and call authenticate() first."
)
raise NotImplementedError(
"update_location() needs an authenticated GBP session."
)
# TODO: credential — replace with the real locations().patch() call.
raise NotImplementedError("GBP locations.patch not implemented yet.")
def main() -> None:
"""CLI entry point. Runs the credential-gated sync flow end to end.
Without credentials it prints guidance and exits cleanly (no traceback), so
the module is safe to run as a smoke test.
"""
print("Google Meu Negócio / Google Business Profile sync")
print("-" * 52)
if not _have_oauth():
print(
"No GBP credentials found in the environment.\n"
"Set GBP_OAUTH_CLIENT_ID, GBP_OAUTH_CLIENT_SECRET, "
"GBP_OAUTH_REFRESH_TOKEN and GBP_ACCOUNT_ID, then re-run.\n"
"To try the companion review pipeline offline instead, run:\n"
" python3 reviews_collector.py --demo\n"
" python3 demo.py"
)
return
# With credentials present, wire the real flow (still stubbed until the API
# calls above are implemented).
service = authenticate()
for location in list_locations(service):
print("location:", location.get("name"), "-", location.get("title"))
if __name__ == "__main__":
main()

Dúvidas

Perguntas frequentes

Preciso de credenciais para rodar?

Só para os dados reais. O demo.py e o reviews_collector.py --demo rodam ponta a ponta com avaliações fictícias, sem tocar em nenhuma API. Apenas o gmn_sync.py (sync da ficha) e a função fetch_reviews() (coleta ao vivo) precisam de OAuth do Google Business Profile: GBP_OAUTH_CLIENT_ID, GBP_OAUTH_CLIENT_SECRET, GBP_OAUTH_REFRESH_TOKEN e GBP_ACCOUNT_ID — todas lidas do ambiente, nada hardcoded.

Com que frequência devo sincronizar a ficha e coletar avaliações?

Informações da ficha (nome, categorias, horário, telefone, site) mudam pouco — sincronize sob demanda, quando algo muda, ou uma vez por dia. As avaliações chegam o tempo todo, então vale coletar de hora em hora ou algumas vezes ao dia. Como a ingestão é idempotente (chave review_id), rodar com frequência não duplica nada.

O texto das avaliações fica armazenado?

Sim. Cada avaliação vai para uma tabela SQLite reviews(review_id, author, star_rating, text, create_time). O texto é opcional (algumas avaliações só têm estrela) e o cálculo da aggregateRating usa apenas star_rating. O JSON-LD inclui o reviewBody quando há comentário.

Implementação

Prefere que eu implemente?

Conte o seu caso numa conversa e a gente define o escopo juntos.

Agendar conversa