aukeys-opscli 0.0.6__py3-none-any.whl
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.
- aukeys_opscli-0.0.6.dist-info/METADATA +15 -0
- aukeys_opscli-0.0.6.dist-info/RECORD +92 -0
- aukeys_opscli-0.0.6.dist-info/WHEEL +4 -0
- aukeys_opscli-0.0.6.dist-info/entry_points.txt +2 -0
- opscli/__init__.py +9 -0
- opscli/amazon/__init__.py +13 -0
- opscli/amazon/cli.py +5 -0
- opscli/amazon/client.py +5 -0
- opscli/amazon/commands/__init__.py +1 -0
- opscli/amazon/commands/cli.py +200 -0
- opscli/amazon/domain/__init__.py +25 -0
- opscli/amazon/domain/exceptions.py +73 -0
- opscli/amazon/domain/models.py +74 -0
- opscli/amazon/exceptions.py +21 -0
- opscli/amazon/manager.py +5 -0
- opscli/amazon/models.py +5 -0
- opscli/amazon/parser.py +5 -0
- opscli/amazon/scraper.py +5 -0
- opscli/amazon/scraping/__init__.py +12 -0
- opscli/amazon/scraping/parser.py +66 -0
- opscli/amazon/scraping/scraper.py +315 -0
- opscli/amazon/services/__init__.py +5 -0
- opscli/amazon/services/manager.py +105 -0
- opscli/amazon/transport/__init__.py +5 -0
- opscli/amazon/transport/client.py +77 -0
- opscli/auth/__init__.py +79 -0
- opscli/auth/cli.py +255 -0
- opscli/auth/commands/__init__.py +1 -0
- opscli/auth/commands/cli.py +5 -0
- opscli/auth/config.py +55 -0
- opscli/auth/core/__init__.py +1 -0
- opscli/auth/core/device_flow.py +80 -0
- opscli/auth/core/system_registry.py +89 -0
- opscli/auth/core/token_manager.py +167 -0
- opscli/auth/domain/__init__.py +23 -0
- opscli/auth/domain/exceptions.py +33 -0
- opscli/auth/exceptions.py +23 -0
- opscli/auth/storage/__init__.py +1 -0
- opscli/auth/storage/credential_store.py +163 -0
- opscli/auth/storage/crypto.py +67 -0
- opscli/cli.py +36 -0
- opscli/config.py +3 -0
- opscli/query/__init__.py +6 -0
- opscli/query/cli.py +5 -0
- opscli/query/client.py +5 -0
- opscli/query/commands/__init__.py +1 -0
- opscli/query/commands/cli.py +140 -0
- opscli/query/domain/__init__.py +23 -0
- opscli/query/domain/exceptions.py +73 -0
- opscli/query/domain/models.py +21 -0
- opscli/query/exceptions.py +21 -0
- opscli/query/manager.py +5 -0
- opscli/query/models.py +5 -0
- opscli/query/services/__init__.py +5 -0
- opscli/query/services/manager.py +373 -0
- opscli/query/transport/__init__.py +5 -0
- opscli/query/transport/client.py +57 -0
- opscli/skills/__init__.py +37 -0
- opscli/skills/cli.py +5 -0
- opscli/skills/commands/__init__.py +1 -0
- opscli/skills/commands/cli.py +389 -0
- opscli/skills/detector.py +5 -0
- opscli/skills/discovery/__init__.py +5 -0
- opscli/skills/discovery/detector.py +225 -0
- opscli/skills/domain/__init__.py +23 -0
- opscli/skills/domain/exceptions.py +51 -0
- opscli/skills/domain/models.py +144 -0
- opscli/skills/exceptions.py +5 -0
- opscli/skills/manager.py +5 -0
- opscli/skills/models.py +19 -0
- opscli/skills/services/__init__.py +5 -0
- opscli/skills/services/manager.py +276 -0
- opscli/skills/sync/__init__.py +5 -0
- opscli/skills/sync/updater.py +274 -0
- opscli/skills/templates/ops-amazon/SKILL.md +181 -0
- opscli/skills/templates/ops-amazon/data/VERSION.json +4 -0
- opscli/skills/templates/ops-auth/SKILL.md +466 -0
- opscli/skills/templates/ops-auth/data/VERSION.json +4 -0
- opscli/skills/templates/ops-dataset-query/SKILL.md +691 -0
- opscli/skills/templates/ops-dataset-query/data/VERSION.json +4 -0
- opscli/skills/templates/ops-dataset-query/data/dataset_fields.csv +1 -0
- opscli/skills/templates/ops-dataset-query/data/datasets.csv +1 -0
- opscli/skills/templates/ops-dataset-query/data/query_metadata.json +4 -0
- opscli/skills/templates/ops-dataset-query/references//346/225/260/346/215/256/346/237/245/350/257/242/346/234/215/345/212/241/345/274/200/345/217/221/350/257/264/346/230/216/346/226/207/346/241/243.md +1126 -0
- opscli/skills/templates/ops-dataset-query/scripts/core.py +140 -0
- opscli/skills/templates/ops-dataset-query/scripts/query.py +145 -0
- opscli/skills/templates/ops-dataset-query/scripts/search.py +36 -0
- opscli/skills/templates/ops-dataset-query/scripts/updater.py +106 -0
- opscli/skills/templates/ops-skills/SKILL.md +494 -0
- opscli/skills/templates/ops-skills/data/VERSION.json +4 -0
- opscli/skills/updater.py +5 -0
- opscli/version.py +19 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aukeys-opscli
|
|
3
|
+
Version: 0.0.6
|
|
4
|
+
Summary: Aukeys 运营 CLI 工具集
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: cryptography>=38
|
|
7
|
+
Requires-Dist: httpx>=0.27
|
|
8
|
+
Requires-Dist: keyring>=25
|
|
9
|
+
Requires-Dist: rich>=13
|
|
10
|
+
Requires-Dist: typer>=0.12
|
|
11
|
+
Provides-Extra: amazon
|
|
12
|
+
Requires-Dist: playwright>=1.44; extra == 'amazon'
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
15
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
opscli/__init__.py,sha256=BcCmcibiVILynMDCEMT4ECjMqvGx6Ym97Wh7o-05MbI,303
|
|
2
|
+
opscli/cli.py,sha256=Yvov0gICf11dqJlImp6klpTFDbQTEESmmM-0eftFJT8,1088
|
|
3
|
+
opscli/config.py,sha256=eIV7yutla7rQkW9OA5Y6JG_x8c83K74OHNd0FTPeV0w,74
|
|
4
|
+
opscli/version.py,sha256=oqBlQnY7eXGkvQ8vMDwYkq2bWCeGKtcwtJJP11ECxiI,443
|
|
5
|
+
opscli/amazon/__init__.py,sha256=afpJXg0k0sVCtOXwtg4Mx5FYsMpEw5p0InnmBlOu0dw,396
|
|
6
|
+
opscli/amazon/cli.py,sha256=DcylM64-FkU7Umh4JQZip8wSrW4GZf59O0tDM3lglHs,96
|
|
7
|
+
opscli/amazon/client.py,sha256=DSVTY-9AKO59sWImEJmMdSqcAI8bY4aQOToUQWGJZLg,127
|
|
8
|
+
opscli/amazon/exceptions.py,sha256=K_3_vS2oTeQzWI4lVYnngYaoA_CLMTTEHAU1Bk0T8ZY,446
|
|
9
|
+
opscli/amazon/manager.py,sha256=Wesb_ONv7QP4tpPjhhb2tLxycm4k8UI0nY7O8lNODRo,124
|
|
10
|
+
opscli/amazon/models.py,sha256=MKfAsj_ah8pgEyxQCV5IVsT525vyYAYxzbhKelwGcdI,222
|
|
11
|
+
opscli/amazon/parser.py,sha256=Aq48kY-wmk1lhlulxoDyG-dyxMUbOfJVkD08_KT0YbI,224
|
|
12
|
+
opscli/amazon/scraper.py,sha256=liXXxLiP_SNqFq5si0Vkyo3i9GMIPSbp9UjOTws9XlU,124
|
|
13
|
+
opscli/amazon/commands/__init__.py,sha256=fjb54ZY-MKd8MV5tv1Shrh5Jfp3Un96iIUWmOms2Hq0,26
|
|
14
|
+
opscli/amazon/commands/cli.py,sha256=wfN6c8_2F5OUeInWbNcNDSUES4VpaWuGehHWMx01J6w,6480
|
|
15
|
+
opscli/amazon/domain/__init__.py,sha256=T1z0TBQPp7whDCMMFVsBkdOPP5F75FwDFjuP3qXbKe8,629
|
|
16
|
+
opscli/amazon/domain/exceptions.py,sha256=FhaWfMSyNw6o_pahHgOvtTa_2IDHLDk9gfMH_ZxynD4,1635
|
|
17
|
+
opscli/amazon/domain/models.py,sha256=bZIcNYl0BjrMZfIra2fzBrcHqWliKfVlibUIXvRw9V4,1739
|
|
18
|
+
opscli/amazon/scraping/__init__.py,sha256=3leaa_l4MV5JP0bHw5zuNje2Dcs8cZw3FCekUrY0U_M,311
|
|
19
|
+
opscli/amazon/scraping/parser.py,sha256=0oKwzo4lw8LwbeP7lvVTH89vA6PAN6A-J9S4kJw4Sb4,2121
|
|
20
|
+
opscli/amazon/scraping/scraper.py,sha256=uzIX8GFbaritDEN8FhX2NpWYGHK38VFMJ9tVYu-I2LU,12743
|
|
21
|
+
opscli/amazon/services/__init__.py,sha256=nigoKCItXBRc64ANj0wI5s0KA-mg3WfilsZdoBT7bfQ,113
|
|
22
|
+
opscli/amazon/services/manager.py,sha256=Kr6D7RCdny9Pug5GMZNjNPdTiKXAX07z4KEa67oIZKs,3786
|
|
23
|
+
opscli/amazon/transport/__init__.py,sha256=7J0EmXjDAbw7ZLmCf2L9rmfugdyLENjClPAZL_N80oI,123
|
|
24
|
+
opscli/amazon/transport/client.py,sha256=u0K6pIo6em7SGbegDckDvo_vUwK6rmBJxXo7A6aZyMQ,3154
|
|
25
|
+
opscli/auth/__init__.py,sha256=gaa7Tvetwiy2g6cXPPVTdJI_6mAtAuehcanMSu6pOUQ,2926
|
|
26
|
+
opscli/auth/cli.py,sha256=pwuA8QPrkUTJJnrVLtSNEZVQsc8Z3z9yxKTHTq2q2QU,9462
|
|
27
|
+
opscli/auth/config.py,sha256=9OUi88zOwtdQbm-kWM150eiqAtEx1tLF-y98NkDJawc,1519
|
|
28
|
+
opscli/auth/exceptions.py,sha256=SqbDW3c3VUTKbLhi69Gw1S-1rCddDL4Rx8SxOK2248Y,494
|
|
29
|
+
opscli/auth/commands/__init__.py,sha256=4fEKbn1RX587mt_wz0fu_LOuSMHLvsZWxeFEpuMhw6s,24
|
|
30
|
+
opscli/auth/commands/cli.py,sha256=5sow76FvU5rQ7gKN4l36gUkJsbIC1asCQ5e5NvkwnSY,83
|
|
31
|
+
opscli/auth/core/__init__.py,sha256=5VGShapu_K0zfId4sJ1bGJV8Tv0P059_SI4HAmXVvgo,13
|
|
32
|
+
opscli/auth/core/device_flow.py,sha256=vPVBcul9rF2k5-07H94xbyJk0g6FOxE5TccyH-sjBoM,3182
|
|
33
|
+
opscli/auth/core/system_registry.py,sha256=xxGNV1jcUw9YUbg76C8_w537nIvl2snT5RFfsf2nVQs,3885
|
|
34
|
+
opscli/auth/core/token_manager.py,sha256=ZTNKk1H5NPredRc3QUeMv3BSzIPW757HXrO4jVfBo_w,7037
|
|
35
|
+
opscli/auth/domain/__init__.py,sha256=4alApqTdpgecZEdm66c8qMPDuMlfyLeLi0sjhB9zK6U,483
|
|
36
|
+
opscli/auth/domain/exceptions.py,sha256=OAdXPuCkD0QW_mzaxyAwct3o5y3a9E_rlnde0nMe7Hk,623
|
|
37
|
+
opscli/auth/storage/__init__.py,sha256=6FEl8uU8oBsPmF_qVqODghEVQGx58mJNHKO1RGyjWE0,16
|
|
38
|
+
opscli/auth/storage/credential_store.py,sha256=70EyWVpgqTno7tkF5VAPWrCuZA2OESpyn02fbVw20GI,5789
|
|
39
|
+
opscli/auth/storage/crypto.py,sha256=aK0tY9AHAD1eEoz9WU5CU7eeMfLvnb-aiqWADb7SXEk,2349
|
|
40
|
+
opscli/query/__init__.py,sha256=Ary6sHyrpluyubFrUOFktafJ18tPZTUBSYv4LO9ZZnQ,188
|
|
41
|
+
opscli/query/cli.py,sha256=ZwRvedaPyBbCpQ56X6PVAaD4Ua0W_YPfVNoLH-j_LLI,94
|
|
42
|
+
opscli/query/client.py,sha256=p0ID9wE1x0uZf11sszWyg8-ATNC4WjKWqMMSIFfbGoQ,117
|
|
43
|
+
opscli/query/exceptions.py,sha256=BcVgurm8sgxElIXClPxM-Lfhh4if20DIJzBTHWymaBk,454
|
|
44
|
+
opscli/query/manager.py,sha256=ix_WRhH2sQPOVhoDnVTzMKknPF7g5uc6fHtjyGJA1mA,120
|
|
45
|
+
opscli/query/models.py,sha256=nUqrH86W6vQQ5tFW9YT-qTYwlhXB5KVITJ8t7Re_Qv4,130
|
|
46
|
+
opscli/query/commands/__init__.py,sha256=taIghztA6XYlBLIguowlWDE__MNcojXjx3OXPeeB68k,25
|
|
47
|
+
opscli/query/commands/cli.py,sha256=ikuUIxBDZmxInIqmke7e7splv6KR9qtgu_4yy3E_dnw,5320
|
|
48
|
+
opscli/query/domain/__init__.py,sha256=6D1aprp2xWZdK8ZJmwSclo_wvNSdHz8g_YQwTx7kVDo,538
|
|
49
|
+
opscli/query/domain/exceptions.py,sha256=-NDxXmUsADT8dINcqUikyN3ljiUvwUClIXSiej1rgrs,1670
|
|
50
|
+
opscli/query/domain/models.py,sha256=uzNYMLipb-cwgVaHd0jv_VeOLgbBSrX5k_Qi6862HyI,410
|
|
51
|
+
opscli/query/services/__init__.py,sha256=4fYylRwegYEvMZ4CjbCIQL139O6TQ16aWg6yGZ7L7u4,109
|
|
52
|
+
opscli/query/services/manager.py,sha256=6FNhMBO4WU6uCCf5v4JkYV2ey7LdE6oK2lQV1BCBwh4,14987
|
|
53
|
+
opscli/query/transport/__init__.py,sha256=ahB8_rcO71Fr7KqwKLbcnVSV3lU0RzWhGoAUJ0NWKt0,113
|
|
54
|
+
opscli/query/transport/client.py,sha256=73qMS_yEYkNHHhmV2V0A6T8tMpVxWGV1Fda_9_cHsvo,2209
|
|
55
|
+
opscli/skills/__init__.py,sha256=tTwzlqhaJQ_u1kgk1JNRiEWR1Wqswkf-CAiXUlBYUq0,1153
|
|
56
|
+
opscli/skills/cli.py,sha256=3GSFpyORg87iEJkmZp1yrTSYxGV8EIovEBuUHITMG5s,96
|
|
57
|
+
opscli/skills/detector.py,sha256=jesZFkTCDsrskpKFBQ6CQrXDq2gNy-V0tEPaOLfWhqE,127
|
|
58
|
+
opscli/skills/exceptions.py,sha256=la6gyf962mVwljn0-aM0Pn_lI1efDsN7CZ9OQ-af2OE,194
|
|
59
|
+
opscli/skills/manager.py,sha256=cbdVR4H5FkPNDKypedJuyG-Wv7XyV_0gpC7T8Frc8ug,124
|
|
60
|
+
opscli/skills/models.py,sha256=plGImIaLhtHoeD072uPl5bANnZ8pCFpiEhK3pz4trjU,406
|
|
61
|
+
opscli/skills/updater.py,sha256=GUAUpcXtjA36alPJ2c3I1ZSeQgEpOiuy8YAoy838WxQ,120
|
|
62
|
+
opscli/skills/commands/__init__.py,sha256=jIkkkeuSVjIC-_c1hP2unDt9D3Q-hWcGYnuWPMRZyRs,26
|
|
63
|
+
opscli/skills/commands/cli.py,sha256=FtcJdmllYkmsFu1VGxxcZhcDpXKU8-3cSneBIh7_k8I,14149
|
|
64
|
+
opscli/skills/discovery/__init__.py,sha256=xNTs5bSmXYWSIZkLo5CE5PZUZlu3O5VLydCRtMZ3Aec,115
|
|
65
|
+
opscli/skills/discovery/detector.py,sha256=3jwgDxlM7jD800BJ3_dHmIenF9IEPfK0MrE8uV2w_RU,9530
|
|
66
|
+
opscli/skills/domain/__init__.py,sha256=5yp3aFgEJPujcBoLtuq6ugyEJhEUMhpa99TJQOQE22Y,561
|
|
67
|
+
opscli/skills/domain/exceptions.py,sha256=4cqJluOz0JgN29zKVLhM850lj8uPM0DCySgSNCCtM2A,1412
|
|
68
|
+
opscli/skills/domain/models.py,sha256=7woJ_2BPlTRprdvpc-4xtq3Zukb_WxYubUnYqkRujMU,4669
|
|
69
|
+
opscli/skills/services/__init__.py,sha256=xxDaxtKuN3F01C5r7E6a0exzPdwfsfC_BXnSxzFX5bM,113
|
|
70
|
+
opscli/skills/services/manager.py,sha256=on5uiZ6jP62-Xzp-Tqs8vqX0IonGgGlbBzxkN4XyS68,10937
|
|
71
|
+
opscli/skills/sync/__init__.py,sha256=S2sB4lmbW0uq2-cgyTuYFUwwVD1_sqaOwh4NKUIta2I,109
|
|
72
|
+
opscli/skills/sync/updater.py,sha256=7Z4Lbb1NVwyDaOS0ld3DdDOzJYOWndh4dA9i_tad7bQ,11159
|
|
73
|
+
opscli/skills/templates/ops-amazon/SKILL.md,sha256=HIzKI1GzXIBhc4iFgI1D-2W4Mq9m9otX2j1QnNXNQe8,4022
|
|
74
|
+
opscli/skills/templates/ops-amazon/data/VERSION.json,sha256=ndUU5OQ1bOhpWcyaYSjIWdZzAi3U6CRSJa2Rf4A-GEc,50
|
|
75
|
+
opscli/skills/templates/ops-auth/SKILL.md,sha256=AOu1ux5W6HT0-7mMbIZeRIXNXoJiIkUEp9a9G2Vneus,11370
|
|
76
|
+
opscli/skills/templates/ops-auth/data/VERSION.json,sha256=erVz4OwjCnFN6FDi_nQ9Av612rvhdKUPKf6C0I-1I4c,48
|
|
77
|
+
opscli/skills/templates/ops-dataset-query/SKILL.md,sha256=xQS_OHI_IuTo2IyB-eaP-qaclxBJQbRofkcVWjmFmJE,22149
|
|
78
|
+
opscli/skills/templates/ops-dataset-query/data/VERSION.json,sha256=2FGZJ4xTABl-Vup2ogzhMurwX1BFRbwq76SSt1tkkCg,57
|
|
79
|
+
opscli/skills/templates/ops-dataset-query/data/dataset_fields.csv,sha256=5PHZ6yfA8fu2LmBl3ZSlHsYiz4bqtz4xUex5eTFt9OE,156
|
|
80
|
+
opscli/skills/templates/ops-dataset-query/data/datasets.csv,sha256=EpIJWArdOS0ToeRcxiB3fqzllbRqM3VKcE5lTi5mWBA,178
|
|
81
|
+
opscli/skills/templates/ops-dataset-query/data/query_metadata.json,sha256=DFQmOo-I7xipDJbSRpkQ8282NCHuLgc2umK2YRza4a8,37
|
|
82
|
+
opscli/skills/templates/ops-dataset-query/references/数据查询服务开发说明文档.md,sha256=1MoO03y_lKhPTvB2wcIzx19Pj35HL3c3h85LLq-9K2w,43361
|
|
83
|
+
opscli/skills/templates/ops-dataset-query/scripts/core.py,sha256=Us4lOaIiluEf8lh9f7XKfl4P45b0WYNIw3uKyGId0NE,3856
|
|
84
|
+
opscli/skills/templates/ops-dataset-query/scripts/query.py,sha256=R1bqK8U_ca0hkEOrHfesZbyIJSqbMlNZTKQeyF-ukNg,5346
|
|
85
|
+
opscli/skills/templates/ops-dataset-query/scripts/search.py,sha256=qN0ksYE-p5DVhJW50cgAesvg5TKR_2BmqtN46tGlNxE,1126
|
|
86
|
+
opscli/skills/templates/ops-dataset-query/scripts/updater.py,sha256=w9oUqRyuxa66J0_WD7xeN0TCEsHq0ZvvF6lklQSutxY,3303
|
|
87
|
+
opscli/skills/templates/ops-skills/SKILL.md,sha256=icBBUBWGyEWD1ifNDbkj58flRzRlx3QXbSljH7BQErw,14296
|
|
88
|
+
opscli/skills/templates/ops-skills/data/VERSION.json,sha256=02sPWWjduv6NTJ15muSLuXVbDS1MziAZlZtnE1A4QwI,50
|
|
89
|
+
aukeys_opscli-0.0.6.dist-info/METADATA,sha256=-tcaATjqF4P0BQ4pq-Y9sv3Jrh7SCoXruq00NR7J0MY,433
|
|
90
|
+
aukeys_opscli-0.0.6.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
91
|
+
aukeys_opscli-0.0.6.dist-info/entry_points.txt,sha256=k9acUhcy0J166h-HxKxpkmLvITs2yS958wg1r2C97FE,42
|
|
92
|
+
aukeys_opscli-0.0.6.dist-info/RECORD,,
|
opscli/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Amazon 模块对外导出。"""
|
|
2
|
+
|
|
3
|
+
from opscli.amazon.domain.models import AmazonCollectResult, AmazonProductSnapshot, AmazonSearchResult
|
|
4
|
+
from opscli.amazon.services.manager import AmazonManager
|
|
5
|
+
from opscli.amazon.transport.client import AmazonOpsClient
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AmazonManager",
|
|
9
|
+
"AmazonOpsClient",
|
|
10
|
+
"AmazonProductSnapshot",
|
|
11
|
+
"AmazonSearchResult",
|
|
12
|
+
"AmazonCollectResult",
|
|
13
|
+
]
|
opscli/amazon/cli.py
ADDED
opscli/amazon/client.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""amazon 命令层。"""
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""amazon CLI 子命令定义。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from opscli.amazon.domain.exceptions import AmazonError
|
|
10
|
+
from opscli.amazon.services.manager import AmazonManager
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Amazon 商品抓取与 ops 提交通道")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _emit(payload: dict, pretty: bool) -> None:
|
|
16
|
+
"""统一输出 JSON。"""
|
|
17
|
+
if pretty:
|
|
18
|
+
typer.echo(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
19
|
+
else:
|
|
20
|
+
typer.echo(json.dumps(payload, ensure_ascii=False))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _error_payload(command: str, exc: Exception) -> dict:
|
|
24
|
+
"""统一错误输出。"""
|
|
25
|
+
if isinstance(exc, AmazonError):
|
|
26
|
+
error = exc.to_dict()
|
|
27
|
+
else:
|
|
28
|
+
error = {
|
|
29
|
+
"code": "AMAZON_ERROR",
|
|
30
|
+
"message": str(exc),
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
"success": False,
|
|
34
|
+
"command": command,
|
|
35
|
+
"data": None,
|
|
36
|
+
"error": error,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command("scrape")
|
|
41
|
+
def scrape(
|
|
42
|
+
asin: str = typer.Option(..., "--asin", help="目标 ASIN"),
|
|
43
|
+
zip_code: str = typer.Option("10001", "--zip-code", help="邮编,用于稳定价格口径"),
|
|
44
|
+
save_history: bool = typer.Option(True, "--save-history/--no-save-history", help="是否保存本地历史"),
|
|
45
|
+
include_raw: bool = typer.Option(False, "--include-raw", help="是否输出原始抓取字段"),
|
|
46
|
+
pretty: bool = typer.Option(False, "--pretty", help="格式化输出"),
|
|
47
|
+
):
|
|
48
|
+
"""抓取单个商品。"""
|
|
49
|
+
manager = AmazonManager()
|
|
50
|
+
try:
|
|
51
|
+
result = manager.scrape_product(
|
|
52
|
+
asin=asin,
|
|
53
|
+
zip_code=zip_code,
|
|
54
|
+
save_history=save_history,
|
|
55
|
+
)
|
|
56
|
+
payload = {
|
|
57
|
+
"success": True,
|
|
58
|
+
"command": "amazon scrape",
|
|
59
|
+
"data": result.to_dict(include_raw=include_raw),
|
|
60
|
+
"error": None,
|
|
61
|
+
}
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
_emit(_error_payload("amazon scrape", exc), pretty)
|
|
64
|
+
raise typer.Exit(1)
|
|
65
|
+
|
|
66
|
+
_emit(payload, pretty)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command("payload")
|
|
70
|
+
def payload(
|
|
71
|
+
asin: str = typer.Option(..., "--asin", help="目标 ASIN"),
|
|
72
|
+
zip_code: str = typer.Option("10001", "--zip-code", help="邮编,用于稳定价格口径"),
|
|
73
|
+
save_history: bool = typer.Option(True, "--save-history/--no-save-history", help="是否保存本地历史"),
|
|
74
|
+
pretty: bool = typer.Option(False, "--pretty", help="格式化输出"),
|
|
75
|
+
):
|
|
76
|
+
"""抓取商品并输出未来提交给 ops 的标准 payload。"""
|
|
77
|
+
manager = AmazonManager()
|
|
78
|
+
try:
|
|
79
|
+
result = manager.scrape_payload(
|
|
80
|
+
asin=asin,
|
|
81
|
+
zip_code=zip_code,
|
|
82
|
+
save_history=save_history,
|
|
83
|
+
)
|
|
84
|
+
response = {
|
|
85
|
+
"success": True,
|
|
86
|
+
"command": "amazon payload",
|
|
87
|
+
"data": result,
|
|
88
|
+
"error": None,
|
|
89
|
+
}
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
_emit(_error_payload("amazon payload", exc), pretty)
|
|
92
|
+
raise typer.Exit(1)
|
|
93
|
+
|
|
94
|
+
_emit(response, pretty)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@app.command("search")
|
|
98
|
+
def search(
|
|
99
|
+
keyword: str = typer.Option(..., "--keyword", help="搜索关键词"),
|
|
100
|
+
zip_code: str = typer.Option("10001", "--zip-code", help="邮编,用于稳定价格口径"),
|
|
101
|
+
limit: int = typer.Option(10, "--limit", min=1, max=50, help="最大结果数"),
|
|
102
|
+
pretty: bool = typer.Option(False, "--pretty", help="格式化输出"),
|
|
103
|
+
):
|
|
104
|
+
"""抓取搜索结果页。"""
|
|
105
|
+
manager = AmazonManager()
|
|
106
|
+
try:
|
|
107
|
+
results = manager.search_products(keyword=keyword, zip_code=zip_code, limit=limit)
|
|
108
|
+
payload = {
|
|
109
|
+
"success": True,
|
|
110
|
+
"command": "amazon search",
|
|
111
|
+
"data": {
|
|
112
|
+
"keyword": keyword,
|
|
113
|
+
"zip_code": zip_code,
|
|
114
|
+
"count": len(results),
|
|
115
|
+
"results": [item.to_dict() for item in results],
|
|
116
|
+
},
|
|
117
|
+
"error": None,
|
|
118
|
+
}
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
_emit(_error_payload("amazon search", exc), pretty)
|
|
121
|
+
raise typer.Exit(1)
|
|
122
|
+
|
|
123
|
+
_emit(payload, pretty)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.command("schema")
|
|
127
|
+
def schema(pretty: bool = typer.Option(False, "--pretty", help="格式化输出")):
|
|
128
|
+
"""输出当前 Amazon 抓取模型与 API 预留结构。"""
|
|
129
|
+
payload = {
|
|
130
|
+
"success": True,
|
|
131
|
+
"command": "amazon schema",
|
|
132
|
+
"data": {
|
|
133
|
+
"snapshot_fields": {
|
|
134
|
+
"asin": "string",
|
|
135
|
+
"zip_code": "string",
|
|
136
|
+
"marketplace": "string",
|
|
137
|
+
"page_url": "string",
|
|
138
|
+
"page_title": "string",
|
|
139
|
+
"product_name": "string",
|
|
140
|
+
"price_text": "string",
|
|
141
|
+
"price_amount": "number|null",
|
|
142
|
+
"currency": "string|null",
|
|
143
|
+
"rating_text": "string",
|
|
144
|
+
"rating_value": "number|null",
|
|
145
|
+
"review_count_text": "string",
|
|
146
|
+
"review_count_value": "integer|null",
|
|
147
|
+
"location": "string",
|
|
148
|
+
"collected_at": "string",
|
|
149
|
+
"valid": "boolean",
|
|
150
|
+
"error": "string|null",
|
|
151
|
+
"raw": "object|null",
|
|
152
|
+
},
|
|
153
|
+
"reserved_submit_payload": {
|
|
154
|
+
"source": "string",
|
|
155
|
+
"snapshot": "AmazonProductSnapshot",
|
|
156
|
+
},
|
|
157
|
+
"search_result_fields": {
|
|
158
|
+
"asin": "string",
|
|
159
|
+
"keyword": "string",
|
|
160
|
+
"zip_code": "string",
|
|
161
|
+
"rank": "integer",
|
|
162
|
+
"title": "string",
|
|
163
|
+
"price_text": "string",
|
|
164
|
+
"price_amount": "number|null",
|
|
165
|
+
"rating_text": "string",
|
|
166
|
+
"rating_value": "number|null",
|
|
167
|
+
"review_count_text": "string",
|
|
168
|
+
"review_count_value": "integer|null",
|
|
169
|
+
"is_best_seller": "boolean",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
"error": None,
|
|
173
|
+
}
|
|
174
|
+
_emit(payload, pretty)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.command("history")
|
|
178
|
+
def history(
|
|
179
|
+
asin: str = typer.Option(..., "--asin", help="目标 ASIN"),
|
|
180
|
+
pretty: bool = typer.Option(False, "--pretty", help="格式化输出"),
|
|
181
|
+
):
|
|
182
|
+
"""读取本地历史快照。"""
|
|
183
|
+
manager = AmazonManager()
|
|
184
|
+
try:
|
|
185
|
+
records = manager.load_history(asin)
|
|
186
|
+
payload = {
|
|
187
|
+
"success": True,
|
|
188
|
+
"command": "amazon history",
|
|
189
|
+
"data": {
|
|
190
|
+
"asin": asin.upper(),
|
|
191
|
+
"count": len(records),
|
|
192
|
+
"records": records,
|
|
193
|
+
},
|
|
194
|
+
"error": None,
|
|
195
|
+
}
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
_emit(_error_payload("amazon history", exc), pretty)
|
|
198
|
+
raise typer.Exit(1)
|
|
199
|
+
|
|
200
|
+
_emit(payload, pretty)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""amazon 领域模型与异常。"""
|
|
2
|
+
|
|
3
|
+
from opscli.amazon.domain.exceptions import (
|
|
4
|
+
AmazonError,
|
|
5
|
+
BadRemoteJsonError,
|
|
6
|
+
InvalidAsinError,
|
|
7
|
+
RemoteBusinessError,
|
|
8
|
+
RemoteHttpError,
|
|
9
|
+
ScraperDependencyError,
|
|
10
|
+
SubmissionConfigError,
|
|
11
|
+
)
|
|
12
|
+
from opscli.amazon.domain.models import AmazonCollectResult, AmazonProductSnapshot, AmazonSearchResult
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AmazonError",
|
|
16
|
+
"InvalidAsinError",
|
|
17
|
+
"ScraperDependencyError",
|
|
18
|
+
"SubmissionConfigError",
|
|
19
|
+
"RemoteHttpError",
|
|
20
|
+
"RemoteBusinessError",
|
|
21
|
+
"BadRemoteJsonError",
|
|
22
|
+
"AmazonProductSnapshot",
|
|
23
|
+
"AmazonSearchResult",
|
|
24
|
+
"AmazonCollectResult",
|
|
25
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""amazon 模块异常定义。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AmazonError(Exception):
|
|
7
|
+
"""amazon 模块统一异常基类。"""
|
|
8
|
+
|
|
9
|
+
code = "AMAZON_ERROR"
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.message = message
|
|
14
|
+
|
|
15
|
+
def to_dict(self) -> dict:
|
|
16
|
+
return {
|
|
17
|
+
"code": self.code,
|
|
18
|
+
"message": self.message,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InvalidAsinError(AmazonError):
|
|
23
|
+
"""ASIN 参数不合法。"""
|
|
24
|
+
|
|
25
|
+
code = "INVALID_ASIN"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ScraperDependencyError(AmazonError):
|
|
29
|
+
"""抓取依赖未安装。"""
|
|
30
|
+
|
|
31
|
+
code = "SCRAPER_DEPENDENCY_ERROR"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SubmissionConfigError(AmazonError):
|
|
35
|
+
"""提交配置缺失或不合法。"""
|
|
36
|
+
|
|
37
|
+
code = "SUBMISSION_CONFIG_ERROR"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RemoteHttpError(AmazonError):
|
|
41
|
+
"""远端 HTTP 请求失败。"""
|
|
42
|
+
|
|
43
|
+
code = "REMOTE_HTTP_ERROR"
|
|
44
|
+
|
|
45
|
+
def __init__(self, status_code: int, message: str):
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.status_code = status_code
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> dict:
|
|
50
|
+
payload = super().to_dict()
|
|
51
|
+
payload["status_code"] = self.status_code
|
|
52
|
+
return payload
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RemoteBusinessError(AmazonError):
|
|
56
|
+
"""远端业务错误。"""
|
|
57
|
+
|
|
58
|
+
code = "REMOTE_BUSINESS_ERROR"
|
|
59
|
+
|
|
60
|
+
def __init__(self, business_code: int | str, message: str):
|
|
61
|
+
super().__init__(message)
|
|
62
|
+
self.business_code = business_code
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> dict:
|
|
65
|
+
payload = super().to_dict()
|
|
66
|
+
payload["business_code"] = self.business_code
|
|
67
|
+
return payload
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BadRemoteJsonError(AmazonError):
|
|
71
|
+
"""远端返回非法 JSON。"""
|
|
72
|
+
|
|
73
|
+
code = "BAD_REMOTE_JSON"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""amazon 模块数据模型。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class AmazonProductSnapshot:
|
|
12
|
+
"""单个 Amazon 商品抓取快照。"""
|
|
13
|
+
|
|
14
|
+
asin: str
|
|
15
|
+
zip_code: str
|
|
16
|
+
marketplace: str
|
|
17
|
+
page_url: str
|
|
18
|
+
page_title: str
|
|
19
|
+
product_name: str
|
|
20
|
+
price_text: str
|
|
21
|
+
price_amount: float | None
|
|
22
|
+
currency: str | None
|
|
23
|
+
rating_text: str
|
|
24
|
+
rating_value: float | None
|
|
25
|
+
review_count_text: str
|
|
26
|
+
review_count_value: int | None
|
|
27
|
+
location: str
|
|
28
|
+
collected_at: str
|
|
29
|
+
valid: bool
|
|
30
|
+
error: str | None = None
|
|
31
|
+
raw: dict[str, Any] | None = None
|
|
32
|
+
|
|
33
|
+
def to_dict(self, *, include_raw: bool = False) -> dict:
|
|
34
|
+
data = asdict(self)
|
|
35
|
+
if not include_raw:
|
|
36
|
+
data.pop("raw", None)
|
|
37
|
+
return data
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class AmazonSearchResult:
|
|
42
|
+
"""Amazon 搜索结果条目。"""
|
|
43
|
+
|
|
44
|
+
asin: str
|
|
45
|
+
keyword: str
|
|
46
|
+
zip_code: str
|
|
47
|
+
rank: int
|
|
48
|
+
title: str
|
|
49
|
+
price_text: str
|
|
50
|
+
price_amount: float | None
|
|
51
|
+
rating_text: str
|
|
52
|
+
rating_value: float | None
|
|
53
|
+
review_count_text: str
|
|
54
|
+
review_count_value: int | None
|
|
55
|
+
is_best_seller: bool
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
return asdict(self)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class AmazonCollectResult:
|
|
63
|
+
"""采集结果聚合对象。"""
|
|
64
|
+
|
|
65
|
+
snapshot: AmazonProductSnapshot
|
|
66
|
+
history_path: Path | None = None
|
|
67
|
+
submit_result: dict | None = None
|
|
68
|
+
|
|
69
|
+
def to_dict(self, *, include_raw: bool = False) -> dict:
|
|
70
|
+
return {
|
|
71
|
+
"snapshot": self.snapshot.to_dict(include_raw=include_raw),
|
|
72
|
+
"history_path": str(self.history_path) if self.history_path else None,
|
|
73
|
+
"submit_result": self.submit_result,
|
|
74
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""amazon exceptions 兼容导出。"""
|
|
2
|
+
|
|
3
|
+
from opscli.amazon.domain.exceptions import (
|
|
4
|
+
AmazonError,
|
|
5
|
+
BadRemoteJsonError,
|
|
6
|
+
InvalidAsinError,
|
|
7
|
+
RemoteBusinessError,
|
|
8
|
+
RemoteHttpError,
|
|
9
|
+
ScraperDependencyError,
|
|
10
|
+
SubmissionConfigError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AmazonError",
|
|
15
|
+
"InvalidAsinError",
|
|
16
|
+
"ScraperDependencyError",
|
|
17
|
+
"SubmissionConfigError",
|
|
18
|
+
"RemoteHttpError",
|
|
19
|
+
"RemoteBusinessError",
|
|
20
|
+
"BadRemoteJsonError",
|
|
21
|
+
]
|
opscli/amazon/manager.py
ADDED
opscli/amazon/models.py
ADDED
opscli/amazon/parser.py
ADDED
opscli/amazon/scraper.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""amazon 抓取层。"""
|
|
2
|
+
|
|
3
|
+
from opscli.amazon.scraping.parser import normalize_text, parse_price, parse_rating, parse_review_count
|
|
4
|
+
from opscli.amazon.scraping.scraper import AmazonScraper
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AmazonScraper",
|
|
8
|
+
"normalize_text",
|
|
9
|
+
"parse_price",
|
|
10
|
+
"parse_rating",
|
|
11
|
+
"parse_review_count",
|
|
12
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""amazon 模块字段标准化工具。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_PRICE_PATTERN = re.compile(r"(\d[\d,]*\.?\d*)")
|
|
9
|
+
_RATING_PATTERN = re.compile(r"(\d+(?:\.\d+)?)")
|
|
10
|
+
_INTEGER_PATTERN = re.compile(r"(\d[\d,]*)")
|
|
11
|
+
_LABELED_REVIEW_COUNT_PATTERN = re.compile(r"(\d[\d,]*)\s+(?:ratings?|reviews?)\b", re.IGNORECASE)
|
|
12
|
+
_ABBREVIATED_COUNT_PATTERN = re.compile(r"(\d+(?:\.\d+)?)\s*([KM])\b", re.IGNORECASE)
|
|
13
|
+
_ZERO_WIDTH_TRANSLATION = str.maketrans("", "", "\u200b\u200c\u200d\ufeff")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def normalize_text(value: str | None) -> str:
|
|
17
|
+
"""标准化抓取到的文本。"""
|
|
18
|
+
if not value:
|
|
19
|
+
return ""
|
|
20
|
+
cleaned = str(value).translate(_ZERO_WIDTH_TRANSLATION)
|
|
21
|
+
return " ".join(cleaned.strip().split())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_price(value: str | None) -> tuple[float | None, str | None]:
|
|
25
|
+
"""从价格字符串中提取金额和币种。"""
|
|
26
|
+
text = normalize_text(value)
|
|
27
|
+
match = _PRICE_PATTERN.search(text)
|
|
28
|
+
amount = None
|
|
29
|
+
if match:
|
|
30
|
+
amount = float(match.group(1).replace(",", ""))
|
|
31
|
+
|
|
32
|
+
currency = None
|
|
33
|
+
if "$" in text:
|
|
34
|
+
currency = "USD"
|
|
35
|
+
return amount, currency
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_rating(value: str | None) -> float | None:
|
|
39
|
+
"""从评分字符串中提取数值。"""
|
|
40
|
+
text = normalize_text(value)
|
|
41
|
+
match = _RATING_PATTERN.search(text)
|
|
42
|
+
if not match:
|
|
43
|
+
return None
|
|
44
|
+
return float(match.group(1))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_review_count(value: str | None) -> int | None:
|
|
48
|
+
"""从评论数字符串中提取数量。"""
|
|
49
|
+
text = normalize_text(value)
|
|
50
|
+
labeled_match = _LABELED_REVIEW_COUNT_PATTERN.search(text)
|
|
51
|
+
if labeled_match:
|
|
52
|
+
return int(labeled_match.group(1).replace(",", ""))
|
|
53
|
+
|
|
54
|
+
abbreviated_match = _ABBREVIATED_COUNT_PATTERN.search(text)
|
|
55
|
+
if abbreviated_match:
|
|
56
|
+
amount = float(abbreviated_match.group(1))
|
|
57
|
+
multiplier = 1000 if abbreviated_match.group(2).upper() == "K" else 1000000
|
|
58
|
+
return int(amount * multiplier)
|
|
59
|
+
|
|
60
|
+
if "out of" in text.lower() and "star" in text.lower():
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
match = _INTEGER_PATTERN.search(text)
|
|
64
|
+
if not match:
|
|
65
|
+
return None
|
|
66
|
+
return int(match.group(1).replace(",", ""))
|