beo-mcdp 0.2.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.
Files changed (39) hide show
  1. beo_mcdp-0.2.0/PKG-INFO +161 -0
  2. beo_mcdp-0.2.0/README.md +141 -0
  3. beo_mcdp-0.2.0/beo_mcdp.egg-info/PKG-INFO +161 -0
  4. beo_mcdp-0.2.0/beo_mcdp.egg-info/SOURCES.txt +37 -0
  5. beo_mcdp-0.2.0/beo_mcdp.egg-info/dependency_links.txt +1 -0
  6. beo_mcdp-0.2.0/beo_mcdp.egg-info/requires.txt +7 -0
  7. beo_mcdp-0.2.0/beo_mcdp.egg-info/top_level.txt +1 -0
  8. beo_mcdp-0.2.0/cdp_scraper/__init__.py +24 -0
  9. beo_mcdp-0.2.0/cdp_scraper/_version.py +1 -0
  10. beo_mcdp-0.2.0/cdp_scraper/browser/__init__.py +0 -0
  11. beo_mcdp-0.2.0/cdp_scraper/browser/actions.py +74 -0
  12. beo_mcdp-0.2.0/cdp_scraper/browser/capture.py +191 -0
  13. beo_mcdp-0.2.0/cdp_scraper/browser/diagnostics.py +57 -0
  14. beo_mcdp-0.2.0/cdp_scraper/browser/helpers.py +2 -0
  15. beo_mcdp-0.2.0/cdp_scraper/browser/session.py +142 -0
  16. beo_mcdp-0.2.0/cdp_scraper/browser/state.py +2 -0
  17. beo_mcdp-0.2.0/cdp_scraper/config.py +79 -0
  18. beo_mcdp-0.2.0/cdp_scraper/errors.py +10 -0
  19. beo_mcdp-0.2.0/cdp_scraper/logs.py +56 -0
  20. beo_mcdp-0.2.0/cdp_scraper/messaging/__init__.py +5 -0
  21. beo_mcdp-0.2.0/cdp_scraper/messaging/redpanda.py +105 -0
  22. beo_mcdp-0.2.0/cdp_scraper/parsers.py +2 -0
  23. beo_mcdp-0.2.0/cdp_scraper/parsing/__init__.py +6 -0
  24. beo_mcdp-0.2.0/cdp_scraper/parsing/mapper.py +202 -0
  25. beo_mcdp-0.2.0/cdp_scraper/parsing/paths.py +80 -0
  26. beo_mcdp-0.2.0/cdp_scraper/parsing/transforms.py +123 -0
  27. beo_mcdp-0.2.0/cdp_scraper/platforms/__init__.py +7 -0
  28. beo_mcdp-0.2.0/cdp_scraper/platforms/registry.py +28 -0
  29. beo_mcdp-0.2.0/cdp_scraper/platforms/runner.py +124 -0
  30. beo_mcdp-0.2.0/cdp_scraper/platforms/spec.py +129 -0
  31. beo_mcdp-0.2.0/cdp_scraper/utils/__init__.py +9 -0
  32. beo_mcdp-0.2.0/cdp_scraper/utils/database.py +12 -0
  33. beo_mcdp-0.2.0/cdp_scraper/utils/redpanda_client.py +8 -0
  34. beo_mcdp-0.2.0/cdp_scraper/utils/utils.py +136 -0
  35. beo_mcdp-0.2.0/setup.cfg +4 -0
  36. beo_mcdp-0.2.0/setup.py +22 -0
  37. beo_mcdp-0.2.0/tests/test_examples.py +57 -0
  38. beo_mcdp-0.2.0/tests/test_parsing.py +80 -0
  39. beo_mcdp-0.2.0/tests/test_runtime.py +168 -0
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: beo-mcdp
3
+ Version: 0.2.0
4
+ Summary: Core CDP Browser Automation and Utils Provider
5
+ Author: Dwi Hardiyanto
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: playwright>=1.57.0
9
+ Provides-Extra: patchright
10
+ Requires-Dist: patchright; extra == "patchright"
11
+ Provides-Extra: kafka
12
+ Requires-Dist: confluent-kafka; extra == "kafka"
13
+ Dynamic: author
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: provides-extra
17
+ Dynamic: requires-dist
18
+ Dynamic: requires-python
19
+ Dynamic: summary
20
+
21
+ # CDP Scraper Provider
22
+
23
+ Library *core* untuk ekstraksi data lewat browser Chrome (CDP). Satu mesin yang sama dipakai untuk
24
+ banyak sosial media: platform baru cukup **dideskripsikan** (URL, pola API, mapping field), tanpa
25
+ menulis ulang sesi browser, scroll, capture, maupun parser.
26
+
27
+ ## Instalasi
28
+
29
+ ```bash
30
+ pip install git+https://github.com/dwihard/social_scrapers.git@v0.2.0
31
+ # opsional: pip install "cdp_scraper[kafka]" / "cdp_scraper[patchright]"
32
+ ```
33
+
34
+ ## Cara kerja
35
+
36
+ 1. Browser membuka halaman (`Endpoint.url`).
37
+ 2. Library mendengarkan response API yang dipanggil halaman (`Endpoint.api_match`).
38
+ 3. JSON-nya diubah jadi list dict oleh `parser` (biasanya `FieldMapper`), di-dedupe lewat `key`, lalu halaman di-scroll sampai `limit`/data habis.
39
+
40
+ ## Menambah platform baru
41
+
42
+ ```python
43
+ from cdp_scraper import (Endpoint, F, FieldMapper, Platform, Scraper,
44
+ const, make_scroller, register_platform, end_when_falsy)
45
+
46
+ parser = FieldMapper(
47
+ items="data.posts", # path ke list item
48
+ fields={
49
+ "id": ("id", "str", ""), # (path, transform, default)
50
+ "url": F(template="https://site.com/{author.handle}/{id}"),
51
+ "likes": ("stats.likes", "int", 0),
52
+ "tags": "tags[].name", # semua elemen
53
+ "posted": F("created", transform="iso_utc"),
54
+ "platform": const("site"),
55
+ },
56
+ )
57
+
58
+ site = Platform(
59
+ name="site", # juga jadi label log: [site] ...
60
+ cdp={"cdp_url": "http://localhost:9223"}, # opsional: Chrome/slot terpisah per platform
61
+ overlay_selectors=('div.popup-close',), # popup yang ditutup otomatis
62
+ login_url_hints=("/login",), # deteksi browser belum login
63
+ endpoints=[Endpoint(
64
+ "search", url="https://site.com/search?q={keyword}", api_match="api/search",
65
+ parser=parser, params=("keyword",), label="keyword", limit=20, max_limit=50,
66
+ end=end_when_falsy("has_more"),
67
+ scroll=make_scroller(wheel=2500, click_pattern="load more"),
68
+ )],
69
+ )
70
+ register_platform(site)
71
+
72
+ result = await Scraper("site").run("search", {"keyword": "kopi"}, limit=30) # satu query
73
+ results = await Scraper("site").run_many("search", [{"keyword": "a"}, {"keyword": "b"}])
74
+ ```
75
+
76
+ `run()` melempar exception (`PageCrashed`, `BrowserBusy`, `ValueError` untuk parameter salah).
77
+ `run_many()` memakai satu tab untuk banyak query dan menjadikan error per-item `status="error"`.
78
+ Contoh lengkap TikTok dan X/Twitter ada di `examples/`.
79
+
80
+ ## Mapping parsing (`FieldMapper`)
81
+
82
+ | Sintaks path | Arti |
83
+ |---|---|
84
+ | `a.b.c` | nilai bersarang |
85
+ | `a[0].b`, `a[-1]` | indeks |
86
+ | `a[].b` | semua elemen (hasil list) |
87
+ | `a[type=1].b` | elemen yang cocok filter (hasil list) |
88
+
89
+ Bentuk spec field: `"path"` · `("path", "transform", default)` · `F(path | any_of=[...] | template=... | fn=...,
90
+ default=, transform=, required=)` · `const(x)` · `callable(record)`.
91
+
92
+ Opsi `FieldMapper`: `item_path` (sub-path record di tiap item), `where` / `record_where`
93
+ (`{path: nilai | EXISTS | callable}` atau callable), `unwrap` (membuka pembungkus), `items` boleh
94
+ list path atau callable.
95
+
96
+ Transform bawaan: `int float str bool url lower upper strip clean_text username hashtag iso_utc
97
+ date_id join first count unique drop_empty` dan `each:<transform>` untuk list. Argumen format tanggal:
98
+ `iso_utc:%a %b %d %H:%M:%S %z %Y`. Tambah sendiri: `register_transform("nama", fn)`.
99
+
100
+ Jika logika terlalu rumit untuk mapping, gunakan `F(fn=...)` atau parser Python biasa
101
+ (`parser=` menerima callable apa pun yang mengembalikan list dict).
102
+
103
+ ## Hook & aksi halaman
104
+
105
+ `prepare` (setelah halaman terbuka) dan `scroll` (tiap putaran) adalah fungsi `async def (page)`.
106
+ Bawaan: `make_scroller(...)`, `make_wait_for(selector)`, `close_overlays(page, selectors)`.
107
+ `Endpoint.capture_options` meneruskan kwargs tambahan ke `capture` (`poll_interval`, `settle`, dst).
108
+
109
+ ## Konfigurasi browser
110
+
111
+ ```python
112
+ from cdp_scraper import CDPConfig, setup_cdp
113
+ setup_cdp(cdp_url="http://localhost:9222", max_sessions=2, lock_timeout=60) # default global
114
+ cfg = CDPConfig.from_env("TWITTER_") # TWITTER_CDP_URL, TWITTER_MAX_CONCURRENT_SESSIONS, ...
115
+ ```
116
+
117
+ Setiap request memakai **tab baru sendiri** (ditutup saat selesai) dan dibatasi `max_sessions`
118
+ bersamaan per konfigurasi. Konfigurasi yang sama berbagi slot; `cdp_url` berbeda = slot berbeda.
119
+ Screenshot debug disimpan di `CDPConfig.debug_dir` (default folder kerja); matikan dengan
120
+ `debug_screenshots=False`.
121
+
122
+ ## Logging
123
+
124
+ Format seragam `[job] Status message: detail`, job = nama platform:
125
+
126
+ ```
127
+ [2026-11-10 08:19:38] [INFO] [site] Success message: Capture api/search selesai: 20 data
128
+ ```
129
+
130
+ Library hanya memakai logger `cdp_scraper` (tanpa handler). Aktifkan dengan `setup_logging()`
131
+ atau injeksi logger sendiri: `setup_cdp(custom_logger=...)`. `get_logger("kafka").error("...")`
132
+ untuk log aplikasi dengan format yang sama.
133
+
134
+ ## Kafka / Redpanda
135
+
136
+ ```python
137
+ from cdp_scraper import RedpandaProducer, publish_chunks
138
+ producer = RedpandaProducer() # KAFKA_BOOTSTRAP_SERVERS; koneksi dibuat saat dipakai
139
+ if producer.available:
140
+ stats = publish_chunks(producer, "topic-x", keywords, chunk_size=2,
141
+ build_payload=lambda chunk: {"keywords": chunk})
142
+ # {"berhasil": n, "gagal": n}
143
+ ```
144
+ Kontrak sumber data (keyword/akun) ada di `cdp_scraper.utils.database` (`KeywordSource`,
145
+ `AccountSource`); implementasinya milik aplikasi.
146
+
147
+ ## Pengujian
148
+
149
+ ```bash
150
+ python -m unittest discover -s tests -t .
151
+ ```
152
+ Tes memakai browser palsu, jadi tidak butuh Chrome/Kafka.
153
+
154
+ ## Perubahan dari 0.1.x (breaking)
155
+
156
+ - Konfigurasi sekarang `CDPConfig` (immutable, per platform); `setup_cdp(...)` tetap ada. Variabel modul `config.CDP_URL` dst dihapus.
157
+ - `get_session_slots` dan `PW_ENGINE` dihapus; pakai `CDPSessionManager` / `manager.engine`.
158
+ - `BrowserBusy` (subclass `TimeoutError`) menggantikan `TimeoutError` polos.
159
+ - `helpers.close_login_modal(page, selectors)` kini wajib diberi selector (tidak ada lagi selector TikTok bawaan).
160
+ - `utils`: tidak lagi mengimpor Flask; `extract_challenges` (khusus TikTok) dihapus; `calculate_engagement_rate` menerima `interaction_keys` / `view_key` (default sama dengan perilaku lama).
161
+ - `utils.database` kini berupa Protocol; `utils.redpanda_client` tidak lagi membuat producer saat import (gunakan `cdp_scraper.messaging`).
@@ -0,0 +1,141 @@
1
+ # CDP Scraper Provider
2
+
3
+ Library *core* untuk ekstraksi data lewat browser Chrome (CDP). Satu mesin yang sama dipakai untuk
4
+ banyak sosial media: platform baru cukup **dideskripsikan** (URL, pola API, mapping field), tanpa
5
+ menulis ulang sesi browser, scroll, capture, maupun parser.
6
+
7
+ ## Instalasi
8
+
9
+ ```bash
10
+ pip install git+https://github.com/dwihard/social_scrapers.git@v0.2.0
11
+ # opsional: pip install "cdp_scraper[kafka]" / "cdp_scraper[patchright]"
12
+ ```
13
+
14
+ ## Cara kerja
15
+
16
+ 1. Browser membuka halaman (`Endpoint.url`).
17
+ 2. Library mendengarkan response API yang dipanggil halaman (`Endpoint.api_match`).
18
+ 3. JSON-nya diubah jadi list dict oleh `parser` (biasanya `FieldMapper`), di-dedupe lewat `key`, lalu halaman di-scroll sampai `limit`/data habis.
19
+
20
+ ## Menambah platform baru
21
+
22
+ ```python
23
+ from cdp_scraper import (Endpoint, F, FieldMapper, Platform, Scraper,
24
+ const, make_scroller, register_platform, end_when_falsy)
25
+
26
+ parser = FieldMapper(
27
+ items="data.posts", # path ke list item
28
+ fields={
29
+ "id": ("id", "str", ""), # (path, transform, default)
30
+ "url": F(template="https://site.com/{author.handle}/{id}"),
31
+ "likes": ("stats.likes", "int", 0),
32
+ "tags": "tags[].name", # semua elemen
33
+ "posted": F("created", transform="iso_utc"),
34
+ "platform": const("site"),
35
+ },
36
+ )
37
+
38
+ site = Platform(
39
+ name="site", # juga jadi label log: [site] ...
40
+ cdp={"cdp_url": "http://localhost:9223"}, # opsional: Chrome/slot terpisah per platform
41
+ overlay_selectors=('div.popup-close',), # popup yang ditutup otomatis
42
+ login_url_hints=("/login",), # deteksi browser belum login
43
+ endpoints=[Endpoint(
44
+ "search", url="https://site.com/search?q={keyword}", api_match="api/search",
45
+ parser=parser, params=("keyword",), label="keyword", limit=20, max_limit=50,
46
+ end=end_when_falsy("has_more"),
47
+ scroll=make_scroller(wheel=2500, click_pattern="load more"),
48
+ )],
49
+ )
50
+ register_platform(site)
51
+
52
+ result = await Scraper("site").run("search", {"keyword": "kopi"}, limit=30) # satu query
53
+ results = await Scraper("site").run_many("search", [{"keyword": "a"}, {"keyword": "b"}])
54
+ ```
55
+
56
+ `run()` melempar exception (`PageCrashed`, `BrowserBusy`, `ValueError` untuk parameter salah).
57
+ `run_many()` memakai satu tab untuk banyak query dan menjadikan error per-item `status="error"`.
58
+ Contoh lengkap TikTok dan X/Twitter ada di `examples/`.
59
+
60
+ ## Mapping parsing (`FieldMapper`)
61
+
62
+ | Sintaks path | Arti |
63
+ |---|---|
64
+ | `a.b.c` | nilai bersarang |
65
+ | `a[0].b`, `a[-1]` | indeks |
66
+ | `a[].b` | semua elemen (hasil list) |
67
+ | `a[type=1].b` | elemen yang cocok filter (hasil list) |
68
+
69
+ Bentuk spec field: `"path"` · `("path", "transform", default)` · `F(path | any_of=[...] | template=... | fn=...,
70
+ default=, transform=, required=)` · `const(x)` · `callable(record)`.
71
+
72
+ Opsi `FieldMapper`: `item_path` (sub-path record di tiap item), `where` / `record_where`
73
+ (`{path: nilai | EXISTS | callable}` atau callable), `unwrap` (membuka pembungkus), `items` boleh
74
+ list path atau callable.
75
+
76
+ Transform bawaan: `int float str bool url lower upper strip clean_text username hashtag iso_utc
77
+ date_id join first count unique drop_empty` dan `each:<transform>` untuk list. Argumen format tanggal:
78
+ `iso_utc:%a %b %d %H:%M:%S %z %Y`. Tambah sendiri: `register_transform("nama", fn)`.
79
+
80
+ Jika logika terlalu rumit untuk mapping, gunakan `F(fn=...)` atau parser Python biasa
81
+ (`parser=` menerima callable apa pun yang mengembalikan list dict).
82
+
83
+ ## Hook & aksi halaman
84
+
85
+ `prepare` (setelah halaman terbuka) dan `scroll` (tiap putaran) adalah fungsi `async def (page)`.
86
+ Bawaan: `make_scroller(...)`, `make_wait_for(selector)`, `close_overlays(page, selectors)`.
87
+ `Endpoint.capture_options` meneruskan kwargs tambahan ke `capture` (`poll_interval`, `settle`, dst).
88
+
89
+ ## Konfigurasi browser
90
+
91
+ ```python
92
+ from cdp_scraper import CDPConfig, setup_cdp
93
+ setup_cdp(cdp_url="http://localhost:9222", max_sessions=2, lock_timeout=60) # default global
94
+ cfg = CDPConfig.from_env("TWITTER_") # TWITTER_CDP_URL, TWITTER_MAX_CONCURRENT_SESSIONS, ...
95
+ ```
96
+
97
+ Setiap request memakai **tab baru sendiri** (ditutup saat selesai) dan dibatasi `max_sessions`
98
+ bersamaan per konfigurasi. Konfigurasi yang sama berbagi slot; `cdp_url` berbeda = slot berbeda.
99
+ Screenshot debug disimpan di `CDPConfig.debug_dir` (default folder kerja); matikan dengan
100
+ `debug_screenshots=False`.
101
+
102
+ ## Logging
103
+
104
+ Format seragam `[job] Status message: detail`, job = nama platform:
105
+
106
+ ```
107
+ [2026-11-10 08:19:38] [INFO] [site] Success message: Capture api/search selesai: 20 data
108
+ ```
109
+
110
+ Library hanya memakai logger `cdp_scraper` (tanpa handler). Aktifkan dengan `setup_logging()`
111
+ atau injeksi logger sendiri: `setup_cdp(custom_logger=...)`. `get_logger("kafka").error("...")`
112
+ untuk log aplikasi dengan format yang sama.
113
+
114
+ ## Kafka / Redpanda
115
+
116
+ ```python
117
+ from cdp_scraper import RedpandaProducer, publish_chunks
118
+ producer = RedpandaProducer() # KAFKA_BOOTSTRAP_SERVERS; koneksi dibuat saat dipakai
119
+ if producer.available:
120
+ stats = publish_chunks(producer, "topic-x", keywords, chunk_size=2,
121
+ build_payload=lambda chunk: {"keywords": chunk})
122
+ # {"berhasil": n, "gagal": n}
123
+ ```
124
+ Kontrak sumber data (keyword/akun) ada di `cdp_scraper.utils.database` (`KeywordSource`,
125
+ `AccountSource`); implementasinya milik aplikasi.
126
+
127
+ ## Pengujian
128
+
129
+ ```bash
130
+ python -m unittest discover -s tests -t .
131
+ ```
132
+ Tes memakai browser palsu, jadi tidak butuh Chrome/Kafka.
133
+
134
+ ## Perubahan dari 0.1.x (breaking)
135
+
136
+ - Konfigurasi sekarang `CDPConfig` (immutable, per platform); `setup_cdp(...)` tetap ada. Variabel modul `config.CDP_URL` dst dihapus.
137
+ - `get_session_slots` dan `PW_ENGINE` dihapus; pakai `CDPSessionManager` / `manager.engine`.
138
+ - `BrowserBusy` (subclass `TimeoutError`) menggantikan `TimeoutError` polos.
139
+ - `helpers.close_login_modal(page, selectors)` kini wajib diberi selector (tidak ada lagi selector TikTok bawaan).
140
+ - `utils`: tidak lagi mengimpor Flask; `extract_challenges` (khusus TikTok) dihapus; `calculate_engagement_rate` menerima `interaction_keys` / `view_key` (default sama dengan perilaku lama).
141
+ - `utils.database` kini berupa Protocol; `utils.redpanda_client` tidak lagi membuat producer saat import (gunakan `cdp_scraper.messaging`).
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: beo-mcdp
3
+ Version: 0.2.0
4
+ Summary: Core CDP Browser Automation and Utils Provider
5
+ Author: Dwi Hardiyanto
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: playwright>=1.57.0
9
+ Provides-Extra: patchright
10
+ Requires-Dist: patchright; extra == "patchright"
11
+ Provides-Extra: kafka
12
+ Requires-Dist: confluent-kafka; extra == "kafka"
13
+ Dynamic: author
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: provides-extra
17
+ Dynamic: requires-dist
18
+ Dynamic: requires-python
19
+ Dynamic: summary
20
+
21
+ # CDP Scraper Provider
22
+
23
+ Library *core* untuk ekstraksi data lewat browser Chrome (CDP). Satu mesin yang sama dipakai untuk
24
+ banyak sosial media: platform baru cukup **dideskripsikan** (URL, pola API, mapping field), tanpa
25
+ menulis ulang sesi browser, scroll, capture, maupun parser.
26
+
27
+ ## Instalasi
28
+
29
+ ```bash
30
+ pip install git+https://github.com/dwihard/social_scrapers.git@v0.2.0
31
+ # opsional: pip install "cdp_scraper[kafka]" / "cdp_scraper[patchright]"
32
+ ```
33
+
34
+ ## Cara kerja
35
+
36
+ 1. Browser membuka halaman (`Endpoint.url`).
37
+ 2. Library mendengarkan response API yang dipanggil halaman (`Endpoint.api_match`).
38
+ 3. JSON-nya diubah jadi list dict oleh `parser` (biasanya `FieldMapper`), di-dedupe lewat `key`, lalu halaman di-scroll sampai `limit`/data habis.
39
+
40
+ ## Menambah platform baru
41
+
42
+ ```python
43
+ from cdp_scraper import (Endpoint, F, FieldMapper, Platform, Scraper,
44
+ const, make_scroller, register_platform, end_when_falsy)
45
+
46
+ parser = FieldMapper(
47
+ items="data.posts", # path ke list item
48
+ fields={
49
+ "id": ("id", "str", ""), # (path, transform, default)
50
+ "url": F(template="https://site.com/{author.handle}/{id}"),
51
+ "likes": ("stats.likes", "int", 0),
52
+ "tags": "tags[].name", # semua elemen
53
+ "posted": F("created", transform="iso_utc"),
54
+ "platform": const("site"),
55
+ },
56
+ )
57
+
58
+ site = Platform(
59
+ name="site", # juga jadi label log: [site] ...
60
+ cdp={"cdp_url": "http://localhost:9223"}, # opsional: Chrome/slot terpisah per platform
61
+ overlay_selectors=('div.popup-close',), # popup yang ditutup otomatis
62
+ login_url_hints=("/login",), # deteksi browser belum login
63
+ endpoints=[Endpoint(
64
+ "search", url="https://site.com/search?q={keyword}", api_match="api/search",
65
+ parser=parser, params=("keyword",), label="keyword", limit=20, max_limit=50,
66
+ end=end_when_falsy("has_more"),
67
+ scroll=make_scroller(wheel=2500, click_pattern="load more"),
68
+ )],
69
+ )
70
+ register_platform(site)
71
+
72
+ result = await Scraper("site").run("search", {"keyword": "kopi"}, limit=30) # satu query
73
+ results = await Scraper("site").run_many("search", [{"keyword": "a"}, {"keyword": "b"}])
74
+ ```
75
+
76
+ `run()` melempar exception (`PageCrashed`, `BrowserBusy`, `ValueError` untuk parameter salah).
77
+ `run_many()` memakai satu tab untuk banyak query dan menjadikan error per-item `status="error"`.
78
+ Contoh lengkap TikTok dan X/Twitter ada di `examples/`.
79
+
80
+ ## Mapping parsing (`FieldMapper`)
81
+
82
+ | Sintaks path | Arti |
83
+ |---|---|
84
+ | `a.b.c` | nilai bersarang |
85
+ | `a[0].b`, `a[-1]` | indeks |
86
+ | `a[].b` | semua elemen (hasil list) |
87
+ | `a[type=1].b` | elemen yang cocok filter (hasil list) |
88
+
89
+ Bentuk spec field: `"path"` · `("path", "transform", default)` · `F(path | any_of=[...] | template=... | fn=...,
90
+ default=, transform=, required=)` · `const(x)` · `callable(record)`.
91
+
92
+ Opsi `FieldMapper`: `item_path` (sub-path record di tiap item), `where` / `record_where`
93
+ (`{path: nilai | EXISTS | callable}` atau callable), `unwrap` (membuka pembungkus), `items` boleh
94
+ list path atau callable.
95
+
96
+ Transform bawaan: `int float str bool url lower upper strip clean_text username hashtag iso_utc
97
+ date_id join first count unique drop_empty` dan `each:<transform>` untuk list. Argumen format tanggal:
98
+ `iso_utc:%a %b %d %H:%M:%S %z %Y`. Tambah sendiri: `register_transform("nama", fn)`.
99
+
100
+ Jika logika terlalu rumit untuk mapping, gunakan `F(fn=...)` atau parser Python biasa
101
+ (`parser=` menerima callable apa pun yang mengembalikan list dict).
102
+
103
+ ## Hook & aksi halaman
104
+
105
+ `prepare` (setelah halaman terbuka) dan `scroll` (tiap putaran) adalah fungsi `async def (page)`.
106
+ Bawaan: `make_scroller(...)`, `make_wait_for(selector)`, `close_overlays(page, selectors)`.
107
+ `Endpoint.capture_options` meneruskan kwargs tambahan ke `capture` (`poll_interval`, `settle`, dst).
108
+
109
+ ## Konfigurasi browser
110
+
111
+ ```python
112
+ from cdp_scraper import CDPConfig, setup_cdp
113
+ setup_cdp(cdp_url="http://localhost:9222", max_sessions=2, lock_timeout=60) # default global
114
+ cfg = CDPConfig.from_env("TWITTER_") # TWITTER_CDP_URL, TWITTER_MAX_CONCURRENT_SESSIONS, ...
115
+ ```
116
+
117
+ Setiap request memakai **tab baru sendiri** (ditutup saat selesai) dan dibatasi `max_sessions`
118
+ bersamaan per konfigurasi. Konfigurasi yang sama berbagi slot; `cdp_url` berbeda = slot berbeda.
119
+ Screenshot debug disimpan di `CDPConfig.debug_dir` (default folder kerja); matikan dengan
120
+ `debug_screenshots=False`.
121
+
122
+ ## Logging
123
+
124
+ Format seragam `[job] Status message: detail`, job = nama platform:
125
+
126
+ ```
127
+ [2026-11-10 08:19:38] [INFO] [site] Success message: Capture api/search selesai: 20 data
128
+ ```
129
+
130
+ Library hanya memakai logger `cdp_scraper` (tanpa handler). Aktifkan dengan `setup_logging()`
131
+ atau injeksi logger sendiri: `setup_cdp(custom_logger=...)`. `get_logger("kafka").error("...")`
132
+ untuk log aplikasi dengan format yang sama.
133
+
134
+ ## Kafka / Redpanda
135
+
136
+ ```python
137
+ from cdp_scraper import RedpandaProducer, publish_chunks
138
+ producer = RedpandaProducer() # KAFKA_BOOTSTRAP_SERVERS; koneksi dibuat saat dipakai
139
+ if producer.available:
140
+ stats = publish_chunks(producer, "topic-x", keywords, chunk_size=2,
141
+ build_payload=lambda chunk: {"keywords": chunk})
142
+ # {"berhasil": n, "gagal": n}
143
+ ```
144
+ Kontrak sumber data (keyword/akun) ada di `cdp_scraper.utils.database` (`KeywordSource`,
145
+ `AccountSource`); implementasinya milik aplikasi.
146
+
147
+ ## Pengujian
148
+
149
+ ```bash
150
+ python -m unittest discover -s tests -t .
151
+ ```
152
+ Tes memakai browser palsu, jadi tidak butuh Chrome/Kafka.
153
+
154
+ ## Perubahan dari 0.1.x (breaking)
155
+
156
+ - Konfigurasi sekarang `CDPConfig` (immutable, per platform); `setup_cdp(...)` tetap ada. Variabel modul `config.CDP_URL` dst dihapus.
157
+ - `get_session_slots` dan `PW_ENGINE` dihapus; pakai `CDPSessionManager` / `manager.engine`.
158
+ - `BrowserBusy` (subclass `TimeoutError`) menggantikan `TimeoutError` polos.
159
+ - `helpers.close_login_modal(page, selectors)` kini wajib diberi selector (tidak ada lagi selector TikTok bawaan).
160
+ - `utils`: tidak lagi mengimpor Flask; `extract_challenges` (khusus TikTok) dihapus; `calculate_engagement_rate` menerima `interaction_keys` / `view_key` (default sama dengan perilaku lama).
161
+ - `utils.database` kini berupa Protocol; `utils.redpanda_client` tidak lagi membuat producer saat import (gunakan `cdp_scraper.messaging`).
@@ -0,0 +1,37 @@
1
+ README.md
2
+ setup.py
3
+ beo_mcdp.egg-info/PKG-INFO
4
+ beo_mcdp.egg-info/SOURCES.txt
5
+ beo_mcdp.egg-info/dependency_links.txt
6
+ beo_mcdp.egg-info/requires.txt
7
+ beo_mcdp.egg-info/top_level.txt
8
+ cdp_scraper/__init__.py
9
+ cdp_scraper/_version.py
10
+ cdp_scraper/config.py
11
+ cdp_scraper/errors.py
12
+ cdp_scraper/logs.py
13
+ cdp_scraper/parsers.py
14
+ cdp_scraper/browser/__init__.py
15
+ cdp_scraper/browser/actions.py
16
+ cdp_scraper/browser/capture.py
17
+ cdp_scraper/browser/diagnostics.py
18
+ cdp_scraper/browser/helpers.py
19
+ cdp_scraper/browser/session.py
20
+ cdp_scraper/browser/state.py
21
+ cdp_scraper/messaging/__init__.py
22
+ cdp_scraper/messaging/redpanda.py
23
+ cdp_scraper/parsing/__init__.py
24
+ cdp_scraper/parsing/mapper.py
25
+ cdp_scraper/parsing/paths.py
26
+ cdp_scraper/parsing/transforms.py
27
+ cdp_scraper/platforms/__init__.py
28
+ cdp_scraper/platforms/registry.py
29
+ cdp_scraper/platforms/runner.py
30
+ cdp_scraper/platforms/spec.py
31
+ cdp_scraper/utils/__init__.py
32
+ cdp_scraper/utils/database.py
33
+ cdp_scraper/utils/redpanda_client.py
34
+ cdp_scraper/utils/utils.py
35
+ tests/test_examples.py
36
+ tests/test_parsing.py
37
+ tests/test_runtime.py
@@ -0,0 +1,7 @@
1
+ playwright>=1.57.0
2
+
3
+ [kafka]
4
+ confluent-kafka
5
+
6
+ [patchright]
7
+ patchright
@@ -0,0 +1 @@
1
+ cdp_scraper
@@ -0,0 +1,24 @@
1
+ from ._version import __version__
2
+ from .browser.actions import close_overlays, make_scroller, make_wait_for, scroll_page_down
3
+ from .browser.capture import CaptureResult, capture, capture_api
4
+ from .browser.session import (CDPSessionManager, default_manager, get_manager,
5
+ is_browser_busy, playwright_session)
6
+ from .config import CDPConfig, get_default_config, set_default_config, setup_cdp
7
+ from .errors import BrowserBusy, CDPError, PageCrashed
8
+ from .logs import JobLogger, get_logger, setup_logging
9
+ from .messaging import RedpandaProducer, chunk_list, publish_chunks
10
+ from .parsing import EXISTS, F, FieldMapper, const, get_path, register_transform
11
+ from .platforms import (Endpoint, Platform, ScrapeResult, Scraper, end_when_empty,
12
+ end_when_falsy, get_platform, list_platforms, register_platform)
13
+
14
+ __all__ = [
15
+ "__version__", "CDPConfig", "setup_cdp", "get_default_config", "set_default_config",
16
+ "CDPError", "PageCrashed", "BrowserBusy", "JobLogger", "get_logger", "setup_logging",
17
+ "CDPSessionManager", "get_manager", "default_manager", "playwright_session", "is_browser_busy",
18
+ "capture", "capture_api", "CaptureResult",
19
+ "close_overlays", "make_scroller", "make_wait_for", "scroll_page_down",
20
+ "FieldMapper", "F", "const", "EXISTS", "get_path", "register_transform",
21
+ "Platform", "Endpoint", "Scraper", "ScrapeResult", "register_platform", "get_platform",
22
+ "list_platforms", "end_when_falsy", "end_when_empty",
23
+ "RedpandaProducer", "publish_chunks", "chunk_list",
24
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
File without changes
@@ -0,0 +1,74 @@
1
+ """Aksi halaman yang bisa dirakit per platform: tutup overlay, scroll, tunggu elemen."""
2
+ from ..logs import get_logger
3
+
4
+ _SCROLL_JS = """(o) => {
5
+ if (o.window === 'bottom') window.scrollTo(0, document.documentElement.scrollHeight);
6
+ else if (o.window === 'viewport') window.scrollBy(0, window.innerHeight * o.factor);
7
+ if (o.containers) {
8
+ for (const el of document.querySelectorAll('div')) {
9
+ const st = getComputedStyle(el);
10
+ if ((st.overflowY === 'auto' || st.overflowY === 'scroll')
11
+ && el.scrollHeight > el.clientHeight + 50) {
12
+ el.scrollTop = el.scrollHeight;
13
+ }
14
+ }
15
+ }
16
+ if (o.pattern) {
17
+ const re = new RegExp(o.pattern, 'i');
18
+ const btn = [...document.querySelectorAll('button, [role="button"]')]
19
+ .find(b => re.test((b.innerText || '').trim()));
20
+ if (btn) btn.click();
21
+ }
22
+ }"""
23
+
24
+
25
+ async def close_overlays(page, selectors):
26
+ """Klik tombol penutup popup/modal (selector CSS diberikan platform)."""
27
+ if not selectors:
28
+ return
29
+ try:
30
+ await page.evaluate(
31
+ "(sels) => document.querySelectorAll(sels.join(',')).forEach(b => b.click())",
32
+ list(selectors))
33
+ except Exception:
34
+ pass
35
+
36
+
37
+ async def scroll_page_down(page):
38
+ await page.keyboard.press("PageDown")
39
+
40
+
41
+ def make_scroller(*, wheel=2500, window_scroll="bottom", viewport_factor=2.0,
42
+ containers=False, click_pattern=None, mouse=(0.5, 0.5), key=None):
43
+ """Bangun fungsi scroll generik.
44
+
45
+ wheel: delta mouse wheel (0 = nonaktif); window_scroll: 'bottom' | 'viewport' | None;
46
+ containers: scroll semua div overflow; click_pattern: regex teks tombol 'load more'/'retry';
47
+ mouse: posisi kursor (fraksi viewport); key: tombol keyboard tambahan (mis. 'End').
48
+ """
49
+ options = {"window": window_scroll, "factor": viewport_factor,
50
+ "containers": containers, "pattern": click_pattern}
51
+ use_js = bool(window_scroll or containers or click_pattern)
52
+
53
+ async def _scroll(page):
54
+ if wheel:
55
+ vp = page.viewport_size or {"width": 1280, "height": 800}
56
+ await page.mouse.move(vp["width"] * mouse[0], vp["height"] * mouse[1])
57
+ await page.mouse.wheel(0, wheel)
58
+ if use_js:
59
+ await page.evaluate(_SCROLL_JS, options)
60
+ if key:
61
+ await page.keyboard.press(key)
62
+
63
+ return _scroll
64
+
65
+
66
+ def make_wait_for(selector, timeout_ms=15000, message=None):
67
+ """Fungsi prepare: tunggu elemen pertama muncul (hanya warning jika tidak muncul)."""
68
+ async def _wait(page, log=None):
69
+ log = log or get_logger()
70
+ try:
71
+ await page.wait_for_selector(selector, state="attached", timeout=timeout_ms)
72
+ except Exception:
73
+ log.warning(message or f"Elemen '{selector}' belum muncul setelah {timeout_ms // 1000} detik.")
74
+ return _wait