graphsense-python 2.11.0__tar.gz → 2.13.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 (196) hide show
  1. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/PKG-INFO +31 -16
  2. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/README.md +26 -15
  3. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/__init__.py +1 -1
  4. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api/addresses_api.py +309 -6
  5. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api_client.py +1 -1
  6. graphsense_python-2.13.0/graphsense/cli/__init__.py +1 -0
  7. graphsense_python-2.13.0/graphsense/cli/__main__.py +4 -0
  8. graphsense_python-2.13.0/graphsense/cli/bulk_cmd.py +95 -0
  9. graphsense_python-2.13.0/graphsense/cli/context.py +64 -0
  10. graphsense_python-2.13.0/graphsense/cli/convenience.py +444 -0
  11. graphsense_python-2.13.0/graphsense/cli/errors.py +74 -0
  12. graphsense_python-2.13.0/graphsense/cli/gs.py +139 -0
  13. graphsense_python-2.13.0/graphsense/cli/main.py +218 -0
  14. graphsense_python-2.13.0/graphsense/cli/raw.py +259 -0
  15. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/configuration.py +2 -2
  16. graphsense_python-2.13.0/graphsense/ext/__init__.py +6 -0
  17. graphsense_python-2.13.0/graphsense/ext/bulk.py +51 -0
  18. graphsense_python-2.13.0/graphsense/ext/client.py +520 -0
  19. graphsense_python-2.13.0/graphsense/ext/deprecation.py +81 -0
  20. graphsense_python-2.13.0/graphsense/ext/io.py +229 -0
  21. graphsense_python-2.13.0/graphsense/ext/output.py +245 -0
  22. graphsense_python-2.13.0/graphsense/ext/selectors.py +28 -0
  23. graphsense_python-2.13.0/graphsense/gs_files/__init__.py +69 -0
  24. graphsense_python-2.13.0/graphsense/gs_files/encoder.py +336 -0
  25. graphsense_python-2.13.0/graphsense/gs_files/parser.py +440 -0
  26. graphsense_python-2.13.0/graphsense/gs_files/summary.py +34 -0
  27. graphsense_python-2.13.0/graphsense/gs_files/writer.py +73 -0
  28. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/address.py +3 -1
  29. graphsense_python-2.13.0/graphsense/models/cluster.py +140 -0
  30. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/entity.py +1 -1
  31. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/neighbor_cluster.py +8 -2
  32. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/neighbor_entity.py +9 -3
  33. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense_python.egg-info/PKG-INFO +31 -16
  34. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense_python.egg-info/SOURCES.txt +37 -1
  35. graphsense_python-2.13.0/graphsense_python.egg-info/entry_points.txt +2 -0
  36. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense_python.egg-info/requires.txt +5 -0
  37. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/pyproject.toml +19 -1
  38. graphsense_python-2.13.0/tests/test_cli_bulk.py +85 -0
  39. graphsense_python-2.13.0/tests/test_cli_convenience.py +295 -0
  40. graphsense_python-2.13.0/tests/test_cli_errors.py +118 -0
  41. graphsense_python-2.13.0/tests/test_cli_gs.py +80 -0
  42. graphsense_python-2.13.0/tests/test_cli_io_pipes.py +132 -0
  43. graphsense_python-2.13.0/tests/test_cli_raw_mirror.py +88 -0
  44. graphsense_python-2.13.0/tests/test_cli_tags_and_dates.py +360 -0
  45. graphsense_python-2.13.0/tests/test_ext_bulk.py +53 -0
  46. graphsense_python-2.13.0/tests/test_ext_client.py +217 -0
  47. graphsense_python-2.13.0/tests/test_ext_io.py +181 -0
  48. graphsense_python-2.13.0/tests/test_ext_output.py +168 -0
  49. graphsense_python-2.13.0/tests/test_ext_selectors.py +25 -0
  50. graphsense_python-2.13.0/tests/test_readme_template.py +45 -0
  51. graphsense_python-2.13.0/tests/test_regen_survives.py +60 -0
  52. graphsense_python-2.11.0/graphsense/models/cluster.py +0 -191
  53. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api/__init__.py +0 -0
  54. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api/blocks_api.py +0 -0
  55. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api/bulk_api.py +0 -0
  56. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api/clusters_api.py +0 -0
  57. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api/entities_api.py +0 -0
  58. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api/general_api.py +0 -0
  59. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api/rates_api.py +0 -0
  60. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api/tags_api.py +0 -0
  61. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api/tokens_api.py +0 -0
  62. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api/txs_api.py +0 -0
  63. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/api_response.py +0 -0
  64. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/compat.py +0 -0
  65. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/exceptions.py +0 -0
  66. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/__init__.py +0 -0
  67. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/actor.py +0 -0
  68. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/actor_context.py +0 -0
  69. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/address.py +0 -0
  70. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/address_tag.py +0 -0
  71. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/address_tags.py +0 -0
  72. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/address_tx.py +0 -0
  73. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/address_tx_utxo.py +0 -0
  74. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/address_txs.py +0 -0
  75. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/block.py +0 -0
  76. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/block_at_date.py +0 -0
  77. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/concept.py +0 -0
  78. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/currency_stats.py +0 -0
  79. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/entity.py +0 -0
  80. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/entity_addresses.py +0 -0
  81. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/external_conversion.py +0 -0
  82. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/http_validation_error.py +0 -0
  83. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/label_summary.py +0 -0
  84. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/labeled_item_ref.py +0 -0
  85. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/link.py +0 -0
  86. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/link_utxo.py +0 -0
  87. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/links.py +0 -0
  88. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/links_inner.py +0 -0
  89. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/location_inner.py +0 -0
  90. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/neighbor_address.py +0 -0
  91. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/neighbor_addresses.py +0 -0
  92. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/neighbor_entities.py +0 -0
  93. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/neighbor_entity.py +0 -0
  94. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/rate.py +0 -0
  95. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/rates.py +0 -0
  96. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/related_address.py +0 -0
  97. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/related_addresses.py +0 -0
  98. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/search_result.py +0 -0
  99. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/search_result_by_currency.py +0 -0
  100. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/search_result_level1.py +0 -0
  101. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/search_result_level2.py +0 -0
  102. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/search_result_level3.py +0 -0
  103. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/search_result_level4.py +0 -0
  104. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/search_result_level5.py +0 -0
  105. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/search_result_level6.py +0 -0
  106. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/stats.py +0 -0
  107. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/tag.py +0 -0
  108. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/tag_cloud_entry.py +0 -0
  109. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/tag_summary.py +0 -0
  110. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/taxonomy.py +0 -0
  111. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/token_config.py +0 -0
  112. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/token_configs.py +0 -0
  113. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/tx.py +0 -0
  114. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/tx_account.py +0 -0
  115. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/tx_ref.py +0 -0
  116. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/tx_summary.py +0 -0
  117. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/tx_utxo.py +0 -0
  118. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/tx_value.py +0 -0
  119. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/user_reported_tag.py +0 -0
  120. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/user_tag_report_response.py +0 -0
  121. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/validation_error.py +0 -0
  122. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/model/values.py +0 -0
  123. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/__init__.py +0 -0
  124. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/actor.py +0 -0
  125. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/actor_context.py +0 -0
  126. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/address_output.py +0 -0
  127. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/address_tag.py +0 -0
  128. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/address_tags.py +0 -0
  129. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/address_tx.py +0 -0
  130. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/address_tx_utxo.py +0 -0
  131. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/address_txs.py +0 -0
  132. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/block.py +0 -0
  133. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/block_at_date.py +0 -0
  134. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/change_heuristics.py +0 -0
  135. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/cluster_addresses.py +0 -0
  136. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/coin_join_consensus.py +0 -0
  137. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/coin_join_heuristics.py +0 -0
  138. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/concept.py +0 -0
  139. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/consensus_entry.py +0 -0
  140. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/currency_stats.py +0 -0
  141. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/direct_change_heuristic.py +0 -0
  142. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/entity_addresses.py +0 -0
  143. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/external_conversion.py +0 -0
  144. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/http_validation_error.py +0 -0
  145. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/join_market_heuristic.py +0 -0
  146. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/label_summary.py +0 -0
  147. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/labeled_item_ref.py +0 -0
  148. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/link.py +0 -0
  149. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/link_utxo.py +0 -0
  150. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/links.py +0 -0
  151. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/links_inner.py +0 -0
  152. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/location_inner.py +0 -0
  153. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/multi_input_change_heuristic.py +0 -0
  154. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/neighbor_address.py +0 -0
  155. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/neighbor_addresses.py +0 -0
  156. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/neighbor_clusters.py +0 -0
  157. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/neighbor_entities.py +0 -0
  158. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/one_time_change_heuristic.py +0 -0
  159. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/rate.py +0 -0
  160. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/rates.py +0 -0
  161. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/related_address.py +0 -0
  162. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/related_addresses.py +0 -0
  163. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/search_result.py +0 -0
  164. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/search_result_by_currency.py +0 -0
  165. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/search_result_level1.py +0 -0
  166. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/search_result_level2.py +0 -0
  167. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/search_result_level3.py +0 -0
  168. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/search_result_level4.py +0 -0
  169. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/search_result_level5.py +0 -0
  170. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/search_result_level6.py +0 -0
  171. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/stats.py +0 -0
  172. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/tag.py +0 -0
  173. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/tag_cloud_entry.py +0 -0
  174. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/tag_summary.py +0 -0
  175. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/taxonomy.py +0 -0
  176. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/token_config.py +0 -0
  177. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/token_configs.py +0 -0
  178. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/tx.py +0 -0
  179. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/tx_account.py +0 -0
  180. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/tx_ref.py +0 -0
  181. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/tx_summary.py +0 -0
  182. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/tx_utxo.py +0 -0
  183. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/tx_value.py +0 -0
  184. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/user_reported_tag.py +0 -0
  185. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/user_tag_report_response.py +0 -0
  186. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/utxo_heuristics.py +0 -0
  187. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/validation_error.py +0 -0
  188. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/values.py +0 -0
  189. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/wasabi_heuristic.py +0 -0
  190. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/whirlpool_coin_join_heuristic.py +0 -0
  191. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/models/whirlpool_tx0_heuristic.py +0 -0
  192. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/py.typed +0 -0
  193. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense/rest.py +0 -0
  194. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense_python.egg-info/dependency_links.txt +0 -0
  195. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/graphsense_python.egg-info/top_level.txt +0 -0
  196. {graphsense_python-2.11.0 → graphsense_python-2.13.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphsense-python
3
- Version: 2.11.0
3
+ Version: 2.13.0
4
4
  Summary: GraphSense API
5
5
  Author-email: Iknaio Cryptoasset Analytics GmbH <contact@iknaio.com>
6
6
  License-Expression: MIT
@@ -15,6 +15,10 @@ Requires-Dist: urllib3<3.0.0,>=2.1.0
15
15
  Requires-Dist: python-dateutil>=2.8.2
16
16
  Requires-Dist: pydantic>=2
17
17
  Requires-Dist: typing-extensions>=4.7.1
18
+ Provides-Extra: cli
19
+ Requires-Dist: rich-click>=1.7; extra == "cli"
20
+ Requires-Dist: jmespath>=1.0; extra == "cli"
21
+ Requires-Dist: pygments>=2.15; extra == "cli"
18
22
 
19
23
  # graphsense-python
20
24
  GraphSense API provides programmatic access to blockchain analytics data across
@@ -41,8 +45,8 @@ for details.
41
45
 
42
46
  This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
43
47
 
44
- - API version: 2.11.0
45
- - Package version: 2.11.0
48
+ - API version: 2.13.0
49
+ - Package version: 2.13.0
46
50
  - Generator version: 7.19.0
47
51
  - Build package: org.openapitools.codegen.languages.PythonClientCodegen
48
52
  For more information, please visit [https://www.iknaio.com/](https://www.iknaio.com/)
@@ -52,37 +56,39 @@ For more information, please visit [https://www.iknaio.com/](https://www.iknaio.
52
56
  Python 3.9+
53
57
 
54
58
  ## Installation & Usage
55
- ### pip install
56
59
 
57
- If the python package is hosted on a repository, you can install directly using:
60
+ This project uses [uv](https://docs.astral.sh/uv/) for environment and
61
+ dependency management.
62
+
63
+ ### Add to a uv project
58
64
 
59
65
  ```sh
60
- pip install git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git
66
+ uv add graphsense-python
67
+ # or, to include the optional `[cli]` extra (installs the `graphsense` CLI):
68
+ uv add 'graphsense-python[cli]'
61
69
  ```
62
- (you may need to run `pip` with root permission: `sudo pip install git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git`)
63
70
 
64
- Then import the package:
65
- ```python
66
- import graphsense
67
- ```
71
+ ### Install from git
68
72
 
69
- ### Setuptools
73
+ ```sh
74
+ uv add 'git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git'
75
+ ```
70
76
 
71
- Install via [Setuptools](http://pypi.python.org/pypi/setuptools).
77
+ ### One-off script usage
72
78
 
73
79
  ```sh
74
- python setup.py install --user
80
+ uv run --with graphsense-python python my_script.py
75
81
  ```
76
- (or `sudo python setup.py install` to install the package for all users)
77
82
 
78
83
  Then import the package:
84
+
79
85
  ```python
80
86
  import graphsense
81
87
  ```
82
88
 
83
89
  ### Tests
84
90
 
85
- Execute `pytest` to run the tests.
91
+ Execute `uv run pytest` to run the tests.
86
92
 
87
93
  ## Getting Started
88
94
 
@@ -137,6 +143,7 @@ All URIs are relative to *https://api.iknaio.com*
137
143
  Class | Method | HTTP request | Description
138
144
  ------------ | ------------- | ------------- | -------------
139
145
  *AddressesApi* | [**get_address**](docs/AddressesApi.md#get_address) | **GET** /{currency}/addresses/{address} | Get an address
146
+ *AddressesApi* | [**get_address_cluster**](docs/AddressesApi.md#get_address_cluster) | **GET** /{currency}/addresses/{address}/cluster | Get the cluster for an address
140
147
  *AddressesApi* | [**get_address_entity**](docs/AddressesApi.md#get_address_entity) | **GET** /{currency}/addresses/{address}/entity | Get the entity for an address
141
148
  *AddressesApi* | [**get_tag_summary_by_address**](docs/AddressesApi.md#get_tag_summary_by_address) | **GET** /{currency}/addresses/{address}/tag_summary | Get address attribution tag summary
142
149
  *AddressesApi* | [**list_address_links**](docs/AddressesApi.md#list_address_links) | **GET** /{currency}/addresses/{address}/links | List transactions between two addresses
@@ -277,3 +284,11 @@ Authentication schemes defined for the API:
277
284
  contact@iknaio.com
278
285
 
279
286
 
287
+ ## Using the high-level wrapper or CLI
288
+
289
+ - **Python** — [`README_EXT.md`](README_EXT.md) documents
290
+ `graphsense.ext.GraphSense`, a single facade that removes boilerplate and
291
+ bundles commonly-paired calls (ships with the base install).
292
+ - **Shell** — [`README_CLI.md`](README_CLI.md) documents the `graphsense` CLI
293
+ (installed via `uv add 'graphsense-python[cli]'`): pipe-friendly,
294
+ JSON/CSV I/O, auto-bulk.
@@ -23,8 +23,8 @@ for details.
23
23
 
24
24
  This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
25
25
 
26
- - API version: 2.11.0
27
- - Package version: 2.11.0
26
+ - API version: 2.13.0
27
+ - Package version: 2.13.0
28
28
  - Generator version: 7.19.0
29
29
  - Build package: org.openapitools.codegen.languages.PythonClientCodegen
30
30
  For more information, please visit [https://www.iknaio.com/](https://www.iknaio.com/)
@@ -34,37 +34,39 @@ For more information, please visit [https://www.iknaio.com/](https://www.iknaio.
34
34
  Python 3.9+
35
35
 
36
36
  ## Installation & Usage
37
- ### pip install
38
37
 
39
- If the python package is hosted on a repository, you can install directly using:
38
+ This project uses [uv](https://docs.astral.sh/uv/) for environment and
39
+ dependency management.
40
+
41
+ ### Add to a uv project
40
42
 
41
43
  ```sh
42
- pip install git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git
44
+ uv add graphsense-python
45
+ # or, to include the optional `[cli]` extra (installs the `graphsense` CLI):
46
+ uv add 'graphsense-python[cli]'
43
47
  ```
44
- (you may need to run `pip` with root permission: `sudo pip install git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git`)
45
48
 
46
- Then import the package:
47
- ```python
48
- import graphsense
49
- ```
49
+ ### Install from git
50
50
 
51
- ### Setuptools
51
+ ```sh
52
+ uv add 'git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git'
53
+ ```
52
54
 
53
- Install via [Setuptools](http://pypi.python.org/pypi/setuptools).
55
+ ### One-off script usage
54
56
 
55
57
  ```sh
56
- python setup.py install --user
58
+ uv run --with graphsense-python python my_script.py
57
59
  ```
58
- (or `sudo python setup.py install` to install the package for all users)
59
60
 
60
61
  Then import the package:
62
+
61
63
  ```python
62
64
  import graphsense
63
65
  ```
64
66
 
65
67
  ### Tests
66
68
 
67
- Execute `pytest` to run the tests.
69
+ Execute `uv run pytest` to run the tests.
68
70
 
69
71
  ## Getting Started
70
72
 
@@ -119,6 +121,7 @@ All URIs are relative to *https://api.iknaio.com*
119
121
  Class | Method | HTTP request | Description
120
122
  ------------ | ------------- | ------------- | -------------
121
123
  *AddressesApi* | [**get_address**](docs/AddressesApi.md#get_address) | **GET** /{currency}/addresses/{address} | Get an address
124
+ *AddressesApi* | [**get_address_cluster**](docs/AddressesApi.md#get_address_cluster) | **GET** /{currency}/addresses/{address}/cluster | Get the cluster for an address
122
125
  *AddressesApi* | [**get_address_entity**](docs/AddressesApi.md#get_address_entity) | **GET** /{currency}/addresses/{address}/entity | Get the entity for an address
123
126
  *AddressesApi* | [**get_tag_summary_by_address**](docs/AddressesApi.md#get_tag_summary_by_address) | **GET** /{currency}/addresses/{address}/tag_summary | Get address attribution tag summary
124
127
  *AddressesApi* | [**list_address_links**](docs/AddressesApi.md#list_address_links) | **GET** /{currency}/addresses/{address}/links | List transactions between two addresses
@@ -259,3 +262,11 @@ Authentication schemes defined for the API:
259
262
  contact@iknaio.com
260
263
 
261
264
 
265
+ ## Using the high-level wrapper or CLI
266
+
267
+ - **Python** — [`README_EXT.md`](README_EXT.md) documents
268
+ `graphsense.ext.GraphSense`, a single facade that removes boilerplate and
269
+ bundles commonly-paired calls (ships with the base install).
270
+ - **Shell** — [`README_CLI.md`](README_CLI.md) documents the `graphsense` CLI
271
+ (installed via `uv add 'graphsense-python[cli]'`): pipe-friendly,
272
+ JSON/CSV I/O, auto-bulk.
@@ -12,7 +12,7 @@
12
12
  """
13
13
 
14
14
 
15
- __version__ = "2.11.0"
15
+ __version__ = "2.13.0"
16
16
 
17
17
  # Define package exports
18
18
  __all__ = [
@@ -20,6 +20,7 @@ from typing_extensions import Annotated
20
20
  from graphsense.models.address import Address
21
21
  from graphsense.models.address_tags import AddressTags
22
22
  from graphsense.models.address_txs import AddressTxs
23
+ from graphsense.models.cluster import Cluster
23
24
  from graphsense.models.entity import Entity
24
25
  from graphsense.models.links import Links
25
26
  from graphsense.models.neighbor_addresses import NeighborAddresses
@@ -433,6 +434,305 @@ class AddressesApi:
433
434
 
434
435
 
435
436
 
437
+ @validate_call_compat
438
+ def get_address_cluster(
439
+ self,
440
+ currency: Annotated[StrictStr, Field(description="The cryptocurrency code (e.g., btc)")],
441
+ address: Annotated[StrictStr, Field(description="The cryptocurrency address")],
442
+ include_actors: Annotated[Optional[StrictBool], Field(description="Whether to include actor information")] = None,
443
+ _request_timeout: Union[
444
+ None,
445
+ Annotated[StrictFloat, Field(gt=0)],
446
+ Tuple[
447
+ Annotated[StrictFloat, Field(gt=0)],
448
+ Annotated[StrictFloat, Field(gt=0)]
449
+ ]
450
+ ] = None,
451
+ _request_auth: Optional[Dict[StrictStr, Any]] = None,
452
+ _content_type: Optional[StrictStr] = None,
453
+ _headers: Optional[Dict[StrictStr, Any]] = None,
454
+ _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
455
+ ) -> Cluster:
456
+ """Get the cluster for an address
457
+
458
+ Returns the address cluster that contains the given address.
459
+
460
+ :param currency: The cryptocurrency code (e.g., btc) (required)
461
+ :type currency: str
462
+ :param address: The cryptocurrency address (required)
463
+ :type address: str
464
+ :param include_actors: Whether to include actor information
465
+ :type include_actors: bool
466
+ :param _request_timeout: timeout setting for this request. If one
467
+ number provided, it will be total request
468
+ timeout. It can also be a pair (tuple) of
469
+ (connection, read) timeouts.
470
+ :type _request_timeout: int, tuple(int, int), optional
471
+ :param _request_auth: set to override the auth_settings for an a single
472
+ request; this effectively ignores the
473
+ authentication in the spec for a single request.
474
+ :type _request_auth: dict, optional
475
+ :param _content_type: force content-type for the request.
476
+ :type _content_type: str, Optional
477
+ :param _headers: set to override the headers for a single
478
+ request; this effectively ignores the headers
479
+ in the spec for a single request.
480
+ :type _headers: dict, optional
481
+ :param _host_index: set to override the host_index for a single
482
+ request; this effectively ignores the host_index
483
+ in the spec for a single request.
484
+ :type _host_index: int, optional
485
+ :return: Returns the result object.
486
+ """ # noqa: E501
487
+
488
+ _param = self._get_address_cluster_serialize(
489
+ currency=currency,
490
+ address=address,
491
+ include_actors=include_actors,
492
+ _request_auth=_request_auth,
493
+ _content_type=_content_type,
494
+ _headers=_headers,
495
+ _host_index=_host_index
496
+ )
497
+
498
+ _response_types_map: Dict[str, Optional[str]] = {
499
+ '200': "Cluster",
500
+ '404': None,
501
+ '422': "HTTPValidationError",
502
+ }
503
+ response_data = self.api_client.call_api(
504
+ *_param,
505
+ _request_timeout=_request_timeout
506
+ )
507
+ response_data.read()
508
+ return self.api_client.response_deserialize(
509
+ response_data=response_data,
510
+ response_types_map=_response_types_map,
511
+ ).data
512
+
513
+
514
+ @validate_call_compat
515
+ def get_address_cluster_with_http_info(
516
+ self,
517
+ currency: Annotated[StrictStr, Field(description="The cryptocurrency code (e.g., btc)")],
518
+ address: Annotated[StrictStr, Field(description="The cryptocurrency address")],
519
+ include_actors: Annotated[Optional[StrictBool], Field(description="Whether to include actor information")] = None,
520
+ _request_timeout: Union[
521
+ None,
522
+ Annotated[StrictFloat, Field(gt=0)],
523
+ Tuple[
524
+ Annotated[StrictFloat, Field(gt=0)],
525
+ Annotated[StrictFloat, Field(gt=0)]
526
+ ]
527
+ ] = None,
528
+ _request_auth: Optional[Dict[StrictStr, Any]] = None,
529
+ _content_type: Optional[StrictStr] = None,
530
+ _headers: Optional[Dict[StrictStr, Any]] = None,
531
+ _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
532
+ ) -> ApiResponse[Cluster]:
533
+ """Get the cluster for an address
534
+
535
+ Returns the address cluster that contains the given address.
536
+
537
+ :param currency: The cryptocurrency code (e.g., btc) (required)
538
+ :type currency: str
539
+ :param address: The cryptocurrency address (required)
540
+ :type address: str
541
+ :param include_actors: Whether to include actor information
542
+ :type include_actors: bool
543
+ :param _request_timeout: timeout setting for this request. If one
544
+ number provided, it will be total request
545
+ timeout. It can also be a pair (tuple) of
546
+ (connection, read) timeouts.
547
+ :type _request_timeout: int, tuple(int, int), optional
548
+ :param _request_auth: set to override the auth_settings for an a single
549
+ request; this effectively ignores the
550
+ authentication in the spec for a single request.
551
+ :type _request_auth: dict, optional
552
+ :param _content_type: force content-type for the request.
553
+ :type _content_type: str, Optional
554
+ :param _headers: set to override the headers for a single
555
+ request; this effectively ignores the headers
556
+ in the spec for a single request.
557
+ :type _headers: dict, optional
558
+ :param _host_index: set to override the host_index for a single
559
+ request; this effectively ignores the host_index
560
+ in the spec for a single request.
561
+ :type _host_index: int, optional
562
+ :return: Returns the result object.
563
+ """ # noqa: E501
564
+
565
+ _param = self._get_address_cluster_serialize(
566
+ currency=currency,
567
+ address=address,
568
+ include_actors=include_actors,
569
+ _request_auth=_request_auth,
570
+ _content_type=_content_type,
571
+ _headers=_headers,
572
+ _host_index=_host_index
573
+ )
574
+
575
+ _response_types_map: Dict[str, Optional[str]] = {
576
+ '200': "Cluster",
577
+ '404': None,
578
+ '422': "HTTPValidationError",
579
+ }
580
+ response_data = self.api_client.call_api(
581
+ *_param,
582
+ _request_timeout=_request_timeout
583
+ )
584
+ response_data.read()
585
+ return self.api_client.response_deserialize(
586
+ response_data=response_data,
587
+ response_types_map=_response_types_map,
588
+ )
589
+
590
+
591
+ @validate_call_compat
592
+ def get_address_cluster_without_preload_content(
593
+ self,
594
+ currency: Annotated[StrictStr, Field(description="The cryptocurrency code (e.g., btc)")],
595
+ address: Annotated[StrictStr, Field(description="The cryptocurrency address")],
596
+ include_actors: Annotated[Optional[StrictBool], Field(description="Whether to include actor information")] = None,
597
+ _request_timeout: Union[
598
+ None,
599
+ Annotated[StrictFloat, Field(gt=0)],
600
+ Tuple[
601
+ Annotated[StrictFloat, Field(gt=0)],
602
+ Annotated[StrictFloat, Field(gt=0)]
603
+ ]
604
+ ] = None,
605
+ _request_auth: Optional[Dict[StrictStr, Any]] = None,
606
+ _content_type: Optional[StrictStr] = None,
607
+ _headers: Optional[Dict[StrictStr, Any]] = None,
608
+ _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
609
+ ) -> RESTResponseType:
610
+ """Get the cluster for an address
611
+
612
+ Returns the address cluster that contains the given address.
613
+
614
+ :param currency: The cryptocurrency code (e.g., btc) (required)
615
+ :type currency: str
616
+ :param address: The cryptocurrency address (required)
617
+ :type address: str
618
+ :param include_actors: Whether to include actor information
619
+ :type include_actors: bool
620
+ :param _request_timeout: timeout setting for this request. If one
621
+ number provided, it will be total request
622
+ timeout. It can also be a pair (tuple) of
623
+ (connection, read) timeouts.
624
+ :type _request_timeout: int, tuple(int, int), optional
625
+ :param _request_auth: set to override the auth_settings for an a single
626
+ request; this effectively ignores the
627
+ authentication in the spec for a single request.
628
+ :type _request_auth: dict, optional
629
+ :param _content_type: force content-type for the request.
630
+ :type _content_type: str, Optional
631
+ :param _headers: set to override the headers for a single
632
+ request; this effectively ignores the headers
633
+ in the spec for a single request.
634
+ :type _headers: dict, optional
635
+ :param _host_index: set to override the host_index for a single
636
+ request; this effectively ignores the host_index
637
+ in the spec for a single request.
638
+ :type _host_index: int, optional
639
+ :return: Returns the result object.
640
+ """ # noqa: E501
641
+
642
+ _param = self._get_address_cluster_serialize(
643
+ currency=currency,
644
+ address=address,
645
+ include_actors=include_actors,
646
+ _request_auth=_request_auth,
647
+ _content_type=_content_type,
648
+ _headers=_headers,
649
+ _host_index=_host_index
650
+ )
651
+
652
+ _response_types_map: Dict[str, Optional[str]] = {
653
+ '200': "Cluster",
654
+ '404': None,
655
+ '422': "HTTPValidationError",
656
+ }
657
+ response_data = self.api_client.call_api(
658
+ *_param,
659
+ _request_timeout=_request_timeout
660
+ )
661
+ return response_data.response
662
+
663
+
664
+ def _get_address_cluster_serialize(
665
+ self,
666
+ currency,
667
+ address,
668
+ include_actors,
669
+ _request_auth,
670
+ _content_type,
671
+ _headers,
672
+ _host_index,
673
+ ) -> RequestSerialized:
674
+
675
+ _host = None
676
+
677
+ _collection_formats: Dict[str, str] = {
678
+ }
679
+
680
+ _path_params: Dict[str, str] = {}
681
+ _query_params: List[Tuple[str, str]] = []
682
+ _header_params: Dict[str, Optional[str]] = _headers or {}
683
+ _form_params: List[Tuple[str, str]] = []
684
+ _files: Dict[
685
+ str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
686
+ ] = {}
687
+ _body_params: Optional[bytes] = None
688
+
689
+ # process the path parameters
690
+ if currency is not None:
691
+ _path_params['currency'] = currency
692
+ if address is not None:
693
+ _path_params['address'] = address
694
+ # process the query parameters
695
+ if include_actors is not None:
696
+
697
+ _query_params.append(('include_actors', include_actors))
698
+
699
+ # process the header parameters
700
+ # process the form parameters
701
+ # process the body parameter
702
+
703
+
704
+ # set the HTTP header `Accept`
705
+ if 'Accept' not in _header_params:
706
+ _header_params['Accept'] = self.api_client.select_header_accept(
707
+ [
708
+ 'application/json'
709
+ ]
710
+ )
711
+
712
+
713
+ # authentication setting
714
+ _auth_settings: List[str] = [
715
+ 'api_key'
716
+ ]
717
+
718
+ return self.api_client.param_serialize(
719
+ method='GET',
720
+ resource_path='/{currency}/addresses/{address}/cluster',
721
+ path_params=_path_params,
722
+ query_params=_query_params,
723
+ header_params=_header_params,
724
+ body=_body_params,
725
+ post_params=_form_params,
726
+ files=_files,
727
+ auth_settings=_auth_settings,
728
+ collection_formats=_collection_formats,
729
+ _host=_host,
730
+ _request_auth=_request_auth
731
+ )
732
+
733
+
734
+
735
+
436
736
  @validate_call_compat
437
737
  def get_address_entity(
438
738
  self,
@@ -452,9 +752,9 @@ class AddressesApi:
452
752
  _headers: Optional[Dict[StrictStr, Any]] = None,
453
753
  _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
454
754
  ) -> Entity:
455
- """Get the entity for an address
755
+ """(Deprecated) Get the entity for an address
456
756
 
457
- Returns the clustered entity that contains the given address.
757
+ Deprecated alias for `GET /{currency}/addresses/{address}/cluster`. Returns the address cluster that contains the given address.
458
758
 
459
759
  :param currency: The cryptocurrency code (e.g., btc) (required)
460
760
  :type currency: str
@@ -483,6 +783,7 @@ class AddressesApi:
483
783
  :type _host_index: int, optional
484
784
  :return: Returns the result object.
485
785
  """ # noqa: E501
786
+ warnings.warn("GET /{currency}/addresses/{address}/entity is deprecated.", DeprecationWarning)
486
787
 
487
788
  _param = self._get_address_entity_serialize(
488
789
  currency=currency,
@@ -529,9 +830,9 @@ class AddressesApi:
529
830
  _headers: Optional[Dict[StrictStr, Any]] = None,
530
831
  _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
531
832
  ) -> ApiResponse[Entity]:
532
- """Get the entity for an address
833
+ """(Deprecated) Get the entity for an address
533
834
 
534
- Returns the clustered entity that contains the given address.
835
+ Deprecated alias for `GET /{currency}/addresses/{address}/cluster`. Returns the address cluster that contains the given address.
535
836
 
536
837
  :param currency: The cryptocurrency code (e.g., btc) (required)
537
838
  :type currency: str
@@ -560,6 +861,7 @@ class AddressesApi:
560
861
  :type _host_index: int, optional
561
862
  :return: Returns the result object.
562
863
  """ # noqa: E501
864
+ warnings.warn("GET /{currency}/addresses/{address}/entity is deprecated.", DeprecationWarning)
563
865
 
564
866
  _param = self._get_address_entity_serialize(
565
867
  currency=currency,
@@ -606,9 +908,9 @@ class AddressesApi:
606
908
  _headers: Optional[Dict[StrictStr, Any]] = None,
607
909
  _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
608
910
  ) -> RESTResponseType:
609
- """Get the entity for an address
911
+ """(Deprecated) Get the entity for an address
610
912
 
611
- Returns the clustered entity that contains the given address.
913
+ Deprecated alias for `GET /{currency}/addresses/{address}/cluster`. Returns the address cluster that contains the given address.
612
914
 
613
915
  :param currency: The cryptocurrency code (e.g., btc) (required)
614
916
  :type currency: str
@@ -637,6 +939,7 @@ class AddressesApi:
637
939
  :type _host_index: int, optional
638
940
  :return: Returns the result object.
639
941
  """ # noqa: E501
942
+ warnings.warn("GET /{currency}/addresses/{address}/entity is deprecated.", DeprecationWarning)
640
943
 
641
944
  _param = self._get_address_entity_serialize(
642
945
  currency=currency,
@@ -95,7 +95,7 @@ class ApiClient:
95
95
  self.default_headers[header_name] = header_value
96
96
  self.cookie = cookie
97
97
  # Set default User-Agent.
98
- self.user_agent = 'OpenAPI-Generator/2.11.0/python'
98
+ self.user_agent = 'OpenAPI-Generator/2.13.0/python'
99
99
  self.client_side_validation = configuration.client_side_validation
100
100
 
101
101
  def __enter__(self):
@@ -0,0 +1 @@
1
+ """graphsense CLI — requires the [cli] extra (`pip install graphsense-python[cli]`)."""
@@ -0,0 +1,4 @@
1
+ from graphsense.cli.main import cli
2
+
3
+ if __name__ == "__main__":
4
+ cli()
@@ -0,0 +1,95 @@
1
+ """`graphsense bulk <operation>` — direct access to /bulk.json / /bulk.csv."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import rich_click as click
8
+
9
+ from graphsense.cli.context import CliContext
10
+ from graphsense.ext import io as io_mod
11
+ from graphsense.ext import output as out_mod
12
+
13
+ pass_ctx = click.make_pass_decorator(CliContext)
14
+
15
+
16
+ @click.command(name="bulk")
17
+ @click.argument("operation")
18
+ @click.argument("currency")
19
+ @click.argument("keys", nargs=-1)
20
+ @click.option(
21
+ "--key-field",
22
+ default="address",
23
+ help="Field name the bulk operation expects (address, tx_hash, cluster, ...).",
24
+ )
25
+ @click.option("--num-pages", type=int, default=1)
26
+ @pass_ctx
27
+ def bulk_command(
28
+ ctx: CliContext,
29
+ operation: str,
30
+ currency: str,
31
+ keys: tuple[str, ...],
32
+ key_field: str,
33
+ num_pages: int,
34
+ ) -> None:
35
+ """Call /bulk.json/<operation> or /bulk.csv/<operation> for a list of keys.
36
+
37
+ Keys come from positionals, --input, or stdin (JSON/CSV/lines, with
38
+ --address-jq / --address-col).
39
+ Output format follows --format (default json).
40
+ """
41
+ ids = _collect_keys(ctx, keys)
42
+ if not ids:
43
+ raise click.UsageError("no keys provided for bulk")
44
+
45
+ fmt = (ctx.format or "json").lower()
46
+ gs = ctx.gs()
47
+ result = gs.bulk(
48
+ operation,
49
+ ids,
50
+ currency=currency,
51
+ format="csv" if fmt == "csv" else "json",
52
+ num_pages=num_pages,
53
+ key_field=key_field,
54
+ )
55
+
56
+ if fmt == "csv":
57
+ _write_raw(ctx, result)
58
+ else:
59
+ out_mod.write(
60
+ result,
61
+ output=ctx.output,
62
+ directory=ctx.directory,
63
+ format=ctx.format,
64
+ color=ctx.color,
65
+ )
66
+
67
+
68
+ def _collect_keys(ctx: CliContext, positional: tuple[str, ...]) -> list[str]:
69
+ if positional:
70
+ return list(positional)
71
+ text = ctx.read_input_text()
72
+ if text is None:
73
+ return []
74
+ return io_mod.parse_input(
75
+ text,
76
+ input_format=ctx.input_format,
77
+ jq=ctx.address_jq,
78
+ col=ctx.address_col,
79
+ )
80
+
81
+
82
+ def _write_raw(ctx: CliContext, payload) -> None:
83
+ """For CSV bulk output, the server already returns flat rows; pass through."""
84
+ text: Optional[str]
85
+ if isinstance(payload, (bytes, bytearray)):
86
+ text = payload.decode("utf-8")
87
+ elif isinstance(payload, str):
88
+ text = payload
89
+ else:
90
+ # Some generated clients deserialize the CSV as {"value": "...raw..."}
91
+ text = str(payload)
92
+ with out_mod.open_out(ctx.output) as fh:
93
+ fh.write(text)
94
+ if not text.endswith("\n"):
95
+ fh.write("\n")