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.
Files changed (92) hide show
  1. aukeys_opscli-0.0.6.dist-info/METADATA +15 -0
  2. aukeys_opscli-0.0.6.dist-info/RECORD +92 -0
  3. aukeys_opscli-0.0.6.dist-info/WHEEL +4 -0
  4. aukeys_opscli-0.0.6.dist-info/entry_points.txt +2 -0
  5. opscli/__init__.py +9 -0
  6. opscli/amazon/__init__.py +13 -0
  7. opscli/amazon/cli.py +5 -0
  8. opscli/amazon/client.py +5 -0
  9. opscli/amazon/commands/__init__.py +1 -0
  10. opscli/amazon/commands/cli.py +200 -0
  11. opscli/amazon/domain/__init__.py +25 -0
  12. opscli/amazon/domain/exceptions.py +73 -0
  13. opscli/amazon/domain/models.py +74 -0
  14. opscli/amazon/exceptions.py +21 -0
  15. opscli/amazon/manager.py +5 -0
  16. opscli/amazon/models.py +5 -0
  17. opscli/amazon/parser.py +5 -0
  18. opscli/amazon/scraper.py +5 -0
  19. opscli/amazon/scraping/__init__.py +12 -0
  20. opscli/amazon/scraping/parser.py +66 -0
  21. opscli/amazon/scraping/scraper.py +315 -0
  22. opscli/amazon/services/__init__.py +5 -0
  23. opscli/amazon/services/manager.py +105 -0
  24. opscli/amazon/transport/__init__.py +5 -0
  25. opscli/amazon/transport/client.py +77 -0
  26. opscli/auth/__init__.py +79 -0
  27. opscli/auth/cli.py +255 -0
  28. opscli/auth/commands/__init__.py +1 -0
  29. opscli/auth/commands/cli.py +5 -0
  30. opscli/auth/config.py +55 -0
  31. opscli/auth/core/__init__.py +1 -0
  32. opscli/auth/core/device_flow.py +80 -0
  33. opscli/auth/core/system_registry.py +89 -0
  34. opscli/auth/core/token_manager.py +167 -0
  35. opscli/auth/domain/__init__.py +23 -0
  36. opscli/auth/domain/exceptions.py +33 -0
  37. opscli/auth/exceptions.py +23 -0
  38. opscli/auth/storage/__init__.py +1 -0
  39. opscli/auth/storage/credential_store.py +163 -0
  40. opscli/auth/storage/crypto.py +67 -0
  41. opscli/cli.py +36 -0
  42. opscli/config.py +3 -0
  43. opscli/query/__init__.py +6 -0
  44. opscli/query/cli.py +5 -0
  45. opscli/query/client.py +5 -0
  46. opscli/query/commands/__init__.py +1 -0
  47. opscli/query/commands/cli.py +140 -0
  48. opscli/query/domain/__init__.py +23 -0
  49. opscli/query/domain/exceptions.py +73 -0
  50. opscli/query/domain/models.py +21 -0
  51. opscli/query/exceptions.py +21 -0
  52. opscli/query/manager.py +5 -0
  53. opscli/query/models.py +5 -0
  54. opscli/query/services/__init__.py +5 -0
  55. opscli/query/services/manager.py +373 -0
  56. opscli/query/transport/__init__.py +5 -0
  57. opscli/query/transport/client.py +57 -0
  58. opscli/skills/__init__.py +37 -0
  59. opscli/skills/cli.py +5 -0
  60. opscli/skills/commands/__init__.py +1 -0
  61. opscli/skills/commands/cli.py +389 -0
  62. opscli/skills/detector.py +5 -0
  63. opscli/skills/discovery/__init__.py +5 -0
  64. opscli/skills/discovery/detector.py +225 -0
  65. opscli/skills/domain/__init__.py +23 -0
  66. opscli/skills/domain/exceptions.py +51 -0
  67. opscli/skills/domain/models.py +144 -0
  68. opscli/skills/exceptions.py +5 -0
  69. opscli/skills/manager.py +5 -0
  70. opscli/skills/models.py +19 -0
  71. opscli/skills/services/__init__.py +5 -0
  72. opscli/skills/services/manager.py +276 -0
  73. opscli/skills/sync/__init__.py +5 -0
  74. opscli/skills/sync/updater.py +274 -0
  75. opscli/skills/templates/ops-amazon/SKILL.md +181 -0
  76. opscli/skills/templates/ops-amazon/data/VERSION.json +4 -0
  77. opscli/skills/templates/ops-auth/SKILL.md +466 -0
  78. opscli/skills/templates/ops-auth/data/VERSION.json +4 -0
  79. opscli/skills/templates/ops-dataset-query/SKILL.md +691 -0
  80. opscli/skills/templates/ops-dataset-query/data/VERSION.json +4 -0
  81. opscli/skills/templates/ops-dataset-query/data/dataset_fields.csv +1 -0
  82. opscli/skills/templates/ops-dataset-query/data/datasets.csv +1 -0
  83. opscli/skills/templates/ops-dataset-query/data/query_metadata.json +4 -0
  84. 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
  85. opscli/skills/templates/ops-dataset-query/scripts/core.py +140 -0
  86. opscli/skills/templates/ops-dataset-query/scripts/query.py +145 -0
  87. opscli/skills/templates/ops-dataset-query/scripts/search.py +36 -0
  88. opscli/skills/templates/ops-dataset-query/scripts/updater.py +106 -0
  89. opscli/skills/templates/ops-skills/SKILL.md +494 -0
  90. opscli/skills/templates/ops-skills/data/VERSION.json +4 -0
  91. opscli/skills/updater.py +5 -0
  92. 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ opscli = opscli.cli:app
opscli/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """opscli 包入口。
2
+
3
+ 有意 re-export AuthClient,使以下两种导入方式均可用(铁律3):
4
+ from opscli import AuthClient # 通过顶层 re-export
5
+ from opscli.auth import AuthClient # 直接导入子模块
6
+ """
7
+ from opscli.auth import AuthClient
8
+
9
+ __all__ = ["AuthClient"]
@@ -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
@@ -0,0 +1,5 @@
1
+ """amazon CLI 兼容导出。"""
2
+
3
+ from opscli.amazon.commands.cli import app
4
+
5
+ __all__ = ["app"]
@@ -0,0 +1,5 @@
1
+ """amazon client 兼容导出。"""
2
+
3
+ from opscli.amazon.transport.client import AmazonOpsClient
4
+
5
+ __all__ = ["AmazonOpsClient"]
@@ -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
+ ]
@@ -0,0 +1,5 @@
1
+ """amazon manager 兼容导出。"""
2
+
3
+ from opscli.amazon.services.manager import AmazonManager
4
+
5
+ __all__ = ["AmazonManager"]
@@ -0,0 +1,5 @@
1
+ """amazon models 兼容导出。"""
2
+
3
+ from opscli.amazon.domain.models import AmazonCollectResult, AmazonProductSnapshot, AmazonSearchResult
4
+
5
+ __all__ = ["AmazonProductSnapshot", "AmazonSearchResult", "AmazonCollectResult"]
@@ -0,0 +1,5 @@
1
+ """amazon parser 兼容导出。"""
2
+
3
+ from opscli.amazon.scraping.parser import normalize_text, parse_price, parse_rating, parse_review_count
4
+
5
+ __all__ = ["normalize_text", "parse_price", "parse_rating", "parse_review_count"]
@@ -0,0 +1,5 @@
1
+ """amazon scraper 兼容导出。"""
2
+
3
+ from opscli.amazon.scraping.scraper import AmazonScraper
4
+
5
+ __all__ = ["AmazonScraper"]
@@ -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(",", ""))