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.
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.
Rodar em 3 comandos
$ 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)
============================================================
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 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 âmbar acima).
1#!/usr/bin/env python32"""Cap. 20 - Standalone offline demo of the GBP review pipeline (ZERO credentials).34Runs the whole review-collection pipeline with FICTIONAL data:56 1. Creates an in-memory SQLite DB via ``reviews_collector``.7 2. Seeds ~6 fictional reviews (no API, no network, no credentials).8 3. Prints a per-review star breakdown and the computed ``aggregateRating``.9 4. Prints the schema.org AggregateRating / Review JSON-LD.1011Run it standalone::1213 python3 demo.py1415Nothing here touches Google Business Profile - the reviews are made up so you16can see the aggregate rating and JSON-LD working before wiring real OAuth17credentials into ``gmn_sync.py`` / ``reviews_collector.fetch_reviews``.1819Part of the "living code" companion repository for the book20"Aparecer ou Sumir" / "Be Seen or Be Forgotten".21"""2223from __future__ import annotations2425import json2627import reviews_collector as rc2829BUSINESS_NAME = "Padaria do Bairro"303132def main() -> None:33 conn = rc.connect(":memory:")34 rc.init_db(conn)3536 written = rc.collect(conn, rc.SAMPLE_REVIEWS)3738 print("=" * 60)39 print(" COLETA DE AVALIAÇÕES — dados fictícios (sem credenciais)")40 print(" Review collection — fictional data (no credentials)")41 print("=" * 60)42 print(f"\n[demo] {written} avaliações fictícias armazenadas / fictional reviews stored")4344 # Per-review breakdown (stars + author + short text).45 print("\n Avaliações (reviews)")46 print(" " + "-" * 52)47 for r in rc.all_reviews(conn):48 stars = "★" * int(r["star_rating"]) + "☆" * (5 - int(r["star_rating"]))49 author = r["author"] or "Anônimo"50 text = r["text"] or "(sem comentário / no comment)"51 print(f" {stars} {author:<14} {text}")5253 # Aggregate rating.54 agg = rc.aggregate_rating(conn)55 print("\n Nota agregada (aggregate rating)")56 print(" " + "-" * 52)57 print(58 f" ratingValue = {agg['ratingValue']} "59 f"reviewCount = {agg['reviewCount']} "60 f"(best {agg['bestRating']} / worst {agg['worstRating']})"61 )6263 # JSON-LD.64 jsonld = rc.build_jsonld(conn, business_name=BUSINESS_NAME)65 print("\n AggregateRating / Review JSON-LD")66 print(" " + "-" * 52)67 print(json.dumps(jsonld, ensure_ascii=False, indent=2))68 print("=" * 60)6970 conn.close()717273if __name__ == "__main__":74 main()
1#!/usr/bin/env python32"""Cap. 20 - Automatic review collection for Google Business Profile.34Fetches reviews for a business location, normalizes them into a common shape,5stores them in SQLite (stdlib ``sqlite3``), computes an ``aggregateRating`` and6emits schema.org ``Review`` + ``AggregateRating`` JSON-LD you can drop into a7page to show star ratings in search results.89Two ways to run:1011 * ``--demo`` : loads FICTIONAL sample reviews (inline), stores them in an12 in-memory DB, prints the aggregate rating and the JSON-LD.13 No credentials, no network.14 * (live) : ``fetch_reviews()`` hits the GBP API. That path is a15 credential-gated stub — see the REQUER CREDENCIAL block.1617Usage::1819 python3 reviews_collector.py --demo20 python3 reviews_collector.py --demo --db reviews.db --pretty2122============================================================================23 REQUER CREDENCIAL / REQUIRES CREDENTIALS24 ---------------------------------------------------------------------------25 Only ``fetch_reviews()`` (the live GBP call) needs OAuth credentials, read26 from ``os.environ`` via ``gmn_sync``. Everything else - normalization,27 storage, aggregate rating and JSON-LD - runs with zero credentials, which is28 exactly what ``--demo`` exercises.29============================================================================3031Part of the "living code" companion repository for the book32"Aparecer ou Sumir" / "Be Seen or Be Forgotten".33"""3435from __future__ import annotations3637import argparse38import json39import sqlite340from typing import Dict, List, Optional4142# ---------------------------------------------------------------------------43# Fictional sample reviews used by --demo (and by demo.py). Zero credentials.44# star_rating is 1..5; text is optional. Dates are ISO-8601.45# ---------------------------------------------------------------------------46SAMPLE_REVIEWS: List[Dict[str, object]] = [47 {48 "review_id": "demo-1",49 "author": "Marina S.",50 "star_rating": 5,51 "text": "Atendimento impecável e entrega no prazo. Recomendo demais!",52 "create_time": "2026-06-28T14:03:00Z",53 },54 {55 "review_id": "demo-2",56 "author": "João P.",57 "star_rating": 4,58 "text": "Ótima experiência, só achei o estacionamento apertado.",59 "create_time": "2026-06-25T09:41:00Z",60 },61 {62 "review_id": "demo-3",63 "author": "Ana Beatriz",64 "star_rating": 5,65 "text": "Equipe muito atenciosa, resolveram tudo rapidinho.",66 "create_time": "2026-06-20T18:22:00Z",67 },68 {69 "review_id": "demo-4",70 "author": "Carlos M.",71 "star_rating": 3,72 "text": "Bom, mas a fila estava grande no horário de pico.",73 "create_time": "2026-06-15T12:10:00Z",74 },75 {76 "review_id": "demo-5",77 "author": "Fernanda L.",78 "star_rating": 5,79 "text": "Melhor da região, sem dúvida. Voltarei sempre.",80 "create_time": "2026-06-10T20:05:00Z",81 },82 {83 "review_id": "demo-6",84 "author": "Roberto A.",85 "star_rating": 4,86 "text": None, # a review with a star rating but no written comment87 "create_time": "2026-06-05T08:30:00Z",88 },89]9091# Map the GBP API star-rating enum to an integer 1..5.92_STAR_ENUM = {"ONE": 1, "TWO": 2, "THREE": 3, "FOUR": 4, "FIVE": 5}939495# ---------------------------------------------------------------------------96# Storage (SQLite, stdlib) — fully functional, no credentials.97# ---------------------------------------------------------------------------98def connect(db_path: str = ":memory:") -> sqlite3.Connection:99 """Open a SQLite connection with row access by column name."""100 conn = sqlite3.connect(db_path)101 conn.row_factory = sqlite3.Row102 return conn103104105def init_db(conn: sqlite3.Connection) -> None:106 """Create the reviews table if missing.107108 UNIQUE(review_id) makes ingestion idempotent: re-collecting the same review109 updates it in place instead of duplicating rows.110 """111 conn.execute(112 """113 CREATE TABLE IF NOT EXISTS reviews (114 review_id TEXT PRIMARY KEY,115 author TEXT,116 star_rating INTEGER NOT NULL,117 text TEXT,118 create_time TEXT119 )120 """121 )122 conn.commit()123124125def store_reviews(conn: sqlite3.Connection, reviews: List[Dict[str, object]]) -> int:126 """Upsert normalized reviews keyed by review_id. Returns rows processed."""127 prepared = [128 (129 r["review_id"],130 r.get("author"),131 int(r["star_rating"]),132 r.get("text"),133 r.get("create_time"),134 )135 for r in reviews136 ]137 conn.executemany(138 """139 INSERT INTO reviews (review_id, author, star_rating, text, create_time)140 VALUES (?, ?, ?, ?, ?)141 ON CONFLICT(review_id) DO UPDATE SET142 author = excluded.author,143 star_rating = excluded.star_rating,144 text = excluded.text,145 create_time = excluded.create_time146 """,147 prepared,148 )149 conn.commit()150 return len(prepared)151152153def all_reviews(conn: sqlite3.Connection) -> List[sqlite3.Row]:154 """Return every stored review, newest first."""155 cur = conn.execute(156 "SELECT review_id, author, star_rating, text, create_time "157 "FROM reviews ORDER BY create_time DESC"158 )159 return cur.fetchall()160161162# ---------------------------------------------------------------------------163# Normalization + aggregate rating + JSON-LD — no credentials.164# ---------------------------------------------------------------------------165def normalize_review(raw: Dict[str, object]) -> Dict[str, object]:166 """Normalize a raw GBP review payload into our common shape.167168 Accepts both the GBP API shape (``starRating`` enum, ``reviewId``,169 ``comment``, ``reviewer.displayName``) and our already-normalized shape,170 so the demo rows pass through untouched.171 """172 if "star_rating" in raw: # already normalized (e.g. SAMPLE_REVIEWS)173 return {174 "review_id": str(raw.get("review_id", "")),175 "author": raw.get("author"),176 "star_rating": int(raw["star_rating"]),177 "text": raw.get("text"),178 "create_time": raw.get("create_time"),179 }180 # GBP API shape.181 reviewer = raw.get("reviewer") or {}182 star = raw.get("starRating", "")183 return {184 "review_id": str(raw.get("reviewId", "")),185 "author": reviewer.get("displayName") if isinstance(reviewer, dict) else None,186 "star_rating": _STAR_ENUM.get(str(star), 0),187 "text": raw.get("comment"),188 "create_time": raw.get("createTime"),189 }190191192def aggregate_rating(conn: sqlite3.Connection) -> Dict[str, object]:193 """Compute the aggregate rating from all stored reviews.194195 Returns a dict with ``ratingValue`` (mean, 1 decimal), ``reviewCount`` and196 the ``bestRating`` / ``worstRating`` bounds.197 """198 cur = conn.execute("SELECT COUNT(*) AS n, AVG(star_rating) AS avg FROM reviews")199 row = cur.fetchone()200 count = int(row["n"] or 0)201 mean = round(float(row["avg"]), 1) if count else 0.0202 return {203 "ratingValue": mean,204 "reviewCount": count,205 "bestRating": 5,206 "worstRating": 1,207 }208209210def build_jsonld(211 conn: sqlite3.Connection,212 business_name: str = "Seu Negócio",213) -> Dict[str, object]:214 """Build a schema.org LocalBusiness graph with AggregateRating + Reviews."""215 agg = aggregate_rating(conn)216 reviews_ld = []217 for r in all_reviews(conn):218 review_ld: Dict[str, object] = {219 "@type": "Review",220 "author": {"@type": "Person", "name": r["author"] or "Anônimo"},221 "reviewRating": {222 "@type": "Rating",223 "ratingValue": r["star_rating"],224 "bestRating": 5,225 "worstRating": 1,226 },227 }228 if r["create_time"]:229 review_ld["datePublished"] = r["create_time"]230 if r["text"]:231 review_ld["reviewBody"] = r["text"]232 reviews_ld.append(review_ld)233234 return {235 "@context": "https://schema.org",236 "@type": "LocalBusiness",237 "name": business_name,238 "aggregateRating": {239 "@type": "AggregateRating",240 "ratingValue": agg["ratingValue"],241 "reviewCount": agg["reviewCount"],242 "bestRating": agg["bestRating"],243 "worstRating": agg["worstRating"],244 },245 "review": reviews_ld,246 }247248249# ---------------------------------------------------------------------------250# Live GBP fetch — REQUER CREDENCIAL / REQUIRES CREDENTIALS.251# ---------------------------------------------------------------------------252def fetch_reviews(location_id: str, service: Optional[object] = None) -> List[Dict[str, object]]:253 """Fetch reviews for ``location_id`` from the GBP API (live call).254255 TODO: credential — with an authorized ``service`` from256 ``gmn_sync.authenticate()``::257258 resp = service.accounts().locations().reviews().list(259 parent=f"{GBP_ACCOUNT_ID}/{location_id}",260 ).execute()261 return [normalize_review(r) for r in resp.get("reviews", [])]262263 Reviews come paginated; follow ``nextPageToken`` until exhausted. Without264 credentials this raises ``NotImplementedError`` — use ``--demo`` to run the265 rest of the pipeline offline.266 """267 import gmn_sync # local import so --demo never requires credentials268269 if service is None and not gmn_sync._have_oauth():270 raise NotImplementedError(271 "fetch_reviews() needs an authenticated GBP session. "272 "Set the GBP_OAUTH_* env vars, or run with --demo."273 )274 # TODO: credential — replace with the real reviews().list() call.275 raise NotImplementedError("GBP reviews.list not implemented yet.")276277278# ---------------------------------------------------------------------------279# Pipeline + CLI280# ---------------------------------------------------------------------------281def collect(282 conn: sqlite3.Connection,283 reviews: List[Dict[str, object]],284) -> int:285 """Normalize + store a batch of reviews. Returns rows written."""286 normalized = [normalize_review(r) for r in reviews]287 return store_reviews(conn, normalized)288289290def print_summary(conn: sqlite3.Connection, jsonld: Dict[str, object], pretty: bool) -> None:291 """Print a small text summary of the aggregate rating + the JSON-LD."""292 agg = aggregate_rating(conn)293 stars = "★" * int(round(agg["ratingValue"]))294 print(295 f"Aggregate rating: {agg['ratingValue']} {stars} "296 f"({agg['reviewCount']} reviews)"297 )298 print("\nReview / AggregateRating JSON-LD:")299 print(json.dumps(jsonld, ensure_ascii=False, indent=2 if pretty else None))300301302def main(argv: Optional[List[str]] = None) -> int:303 parser = argparse.ArgumentParser(description="GBP review collector.")304 parser.add_argument(305 "--demo",306 action="store_true",307 help="Load fictional sample reviews (no credentials, no network).",308 )309 parser.add_argument(310 "--location-id",311 default="locations/DEMO",312 help="GBP location resource name (used only for the live path).",313 )314 parser.add_argument(315 "--db",316 default=":memory:",317 help="SQLite path (default: in-memory).",318 )319 parser.add_argument(320 "--business-name",321 default="Padaria do Bairro",322 help="Business name to embed in the JSON-LD.",323 )324 parser.add_argument(325 "--pretty",326 action="store_true",327 help="Pretty-print the JSON-LD.",328 )329 args = parser.parse_args(argv)330331 conn = connect(args.db)332 init_db(conn)333334 if args.demo:335 written = collect(conn, SAMPLE_REVIEWS)336 print(f"[demo] {written} avaliações fictícias armazenadas / fictional reviews stored\n")337 else:338 reviews = fetch_reviews(args.location_id) # credential-gated339 written = collect(conn, reviews)340 print(f"[live] {written} reviews stored from {args.location_id}\n")341342 jsonld = build_jsonld(conn, business_name=args.business_name)343 print_summary(conn, jsonld, pretty=args.pretty)344 conn.close()345 return 0346347348if __name__ == "__main__":349 raise SystemExit(main())
1#!/usr/bin/env python32"""Cap. 20 - Google Meu Negócio / Google Business Profile sync - STRUCTURE.34Pushes and keeps your business location(s) in sync with the Google Business5Profile (GBP, a.k.a. "Google Meu Negócio") API: name, categories, opening6hours, phone and website. The flow is:78 authenticate() -> list_locations() -> update_location(id, patch)910============================================================================11 REQUER CREDENCIAL / REQUIRES CREDENTIALS12 ---------------------------------------------------------------------------13 The GBP API only returns/accepts live data through an authenticated OAuth14 session tied to the Google account that manages the business. Every value15 below is read ONLY from ``os.environ`` - no secret is ever hardcoded. Until16 the credentials are set, ``authenticate()`` / ``list_locations()`` /17 ``update_location()`` raise ``NotImplementedError`` with setup guidance, but18 the module stays importable and ``py_compile``-clean.1920 There is NO offline sample path here on purpose: writing to GBP is a real,21 side-effecting operation. To see the companion pipeline run offline with22 fictional data, use ``reviews_collector.py --demo`` or ``demo.py``.23============================================================================2425Part of the "living code" companion repository for the book26"Aparecer ou Sumir" / "Be Seen or Be Forgotten".27"""2829from __future__ import annotations3031import os32from typing import Dict, List, Optional3334# ---------------------------------------------------------------------------35# TODO: credential — all values come from the environment. Nothing is hardcoded.36# Set these before any real GBP call can succeed.37# ---------------------------------------------------------------------------38# TODO: credential — OAuth client id/secret for a Google Cloud project with the39# "Business Profile API" (mybusinessbusinessinformation) enabled.40GBP_OAUTH_CLIENT_ID = os.environ.get("GBP_OAUTH_CLIENT_ID", "")41GBP_OAUTH_CLIENT_SECRET = os.environ.get("GBP_OAUTH_CLIENT_SECRET", "")42# TODO: credential — a long-lived OAuth refresh token for the managing account.43GBP_OAUTH_REFRESH_TOKEN = os.environ.get("GBP_OAUTH_REFRESH_TOKEN", "")44# TODO: credential — the account resource name, e.g. "accounts/1234567890".45GBP_ACCOUNT_ID = os.environ.get("GBP_ACCOUNT_ID", "")4647# OAuth scope needed to read and write business information.48GBP_SCOPE = "https://www.googleapis.com/auth/business.manage"4950LocationPatch = Dict[str, object]515253def _have_oauth() -> bool:54 """True only when every OAuth credential is present in the environment."""55 return bool(56 GBP_OAUTH_CLIENT_ID57 and GBP_OAUTH_CLIENT_SECRET58 and GBP_OAUTH_REFRESH_TOKEN59 and GBP_ACCOUNT_ID60 )616263def authenticate() -> object:64 """Build an authorized GBP API session from the OAuth credentials.6566 TODO: credential — steps once you have an OAuth client + refresh token:67 1. pip install google-api-python-client google-auth google-auth-oauthlib68 2. Exchange the refresh token for an access token::6970 from google.oauth2.credentials import Credentials71 creds = Credentials(72 None,73 refresh_token=GBP_OAUTH_REFRESH_TOKEN,74 client_id=GBP_OAUTH_CLIENT_ID,75 client_secret=GBP_OAUTH_CLIENT_SECRET,76 token_uri="https://oauth2.googleapis.com/token",77 scopes=[GBP_SCOPE],78 )79 3. Build the service::8081 from googleapiclient.discovery import build82 service = build("mybusinessbusinessinformation", "v1", credentials=creds)8384 and return it for ``list_locations`` / ``update_location`` to use.85 """86 if not _have_oauth():87 print(88 "[gmn_sync] Missing OAuth credentials. Set GBP_OAUTH_CLIENT_ID, "89 "GBP_OAUTH_CLIENT_SECRET, GBP_OAUTH_REFRESH_TOKEN and GBP_ACCOUNT_ID "90 "in your environment. See the authenticate() docstring for the "91 "full setup steps."92 )93 raise NotImplementedError(94 "authenticate() needs the four GBP_OAUTH_* / GBP_ACCOUNT_ID env vars."95 )96 # TODO: credential — replace with the real Credentials + build() call.97 raise NotImplementedError("GBP OAuth session build not implemented yet.")9899100def list_locations(service: Optional[object] = None) -> List[Dict[str, object]]:101 """List the business locations managed under ``GBP_ACCOUNT_ID``.102103 TODO: credential — with an authorized ``service`` from ``authenticate()``::104105 resp = service.accounts().locations().list(106 parent=GBP_ACCOUNT_ID,107 readMask="name,title,categories,phoneNumbers,websiteUri,regularHours",108 ).execute()109 return resp.get("locations", [])110111 Each returned location has a ``name`` like "locations/0987654321" that you112 pass to :func:`update_location`.113 """114 if service is None and not _have_oauth():115 print(116 "[gmn_sync] Cannot list locations without an authenticated session. "117 "Call authenticate() once the GBP_OAUTH_* env vars are set."118 )119 raise NotImplementedError(120 "list_locations() needs an authenticated GBP session."121 )122 # TODO: credential — replace with the real accounts().locations().list() call.123 raise NotImplementedError("GBP locations.list not implemented yet.")124125126def update_location(127 location_id: str,128 patch: LocationPatch,129 service: Optional[object] = None,130) -> Dict[str, object]:131 """Push a partial update (name, categories, hours, phone, website) to GBP.132133 Args:134 location_id: resource name of the location, e.g. "locations/0987654321".135 patch: the fields to change. Only keys present are written, e.g.::136137 {138 "title": "Padaria do Bairro",139 "phoneNumbers": {"primaryPhone": "+55 51 99999-0000"},140 "websiteUri": "https://padariadobairro.com.br",141 "categories": {"primaryCategory": {"name": "categories/bakery"}},142 "regularHours": {...},143 }144145 service: an authorized session from :func:`authenticate` (optional; if146 omitted the function will require the OAuth env vars to be present).147148 TODO: credential — the real write is a PATCH with an updateMask listing only149 the fields you changed::150151 update_mask = ",".join(patch.keys())152 return service.locations().patch(153 name=location_id,154 updateMask=update_mask,155 body=patch,156 ).execute()157158 GBP validates edits and some categories/hours changes may be reviewed by159 Google before going live.160 """161 if not location_id:162 raise ValueError("location_id is required, e.g. 'locations/0987654321'.")163 if not isinstance(patch, dict) or not patch:164 raise ValueError("patch must be a non-empty dict of fields to update.")165 if service is None and not _have_oauth():166 print(167 "[gmn_sync] Cannot update location without an authenticated session. "168 f"Would PATCH {location_id} with fields: {', '.join(sorted(patch))}. "169 "Set the GBP_OAUTH_* env vars and call authenticate() first."170 )171 raise NotImplementedError(172 "update_location() needs an authenticated GBP session."173 )174 # TODO: credential — replace with the real locations().patch() call.175 raise NotImplementedError("GBP locations.patch not implemented yet.")176177178def main() -> None:179 """CLI entry point. Runs the credential-gated sync flow end to end.180181 Without credentials it prints guidance and exits cleanly (no traceback), so182 the module is safe to run as a smoke test.183 """184 print("Google Meu Negócio / Google Business Profile sync")185 print("-" * 52)186 if not _have_oauth():187 print(188 "No GBP credentials found in the environment.\n"189 "Set GBP_OAUTH_CLIENT_ID, GBP_OAUTH_CLIENT_SECRET, "190 "GBP_OAUTH_REFRESH_TOKEN and GBP_ACCOUNT_ID, then re-run.\n"191 "To try the companion review pipeline offline instead, run:\n"192 " python3 reviews_collector.py --demo\n"193 " python3 demo.py"194 )195 return196 # With credentials present, wire the real flow (still stubbed until the API197 # calls above are implemented).198 service = authenticate()199 for location in list_locations(service):200 print("location:", location.get("name"), "-", location.get("title"))201202203if __name__ == "__main__":204 main()
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.