graphsense-python 2.15.0__tar.gz → 2.15.1__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.
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/PKG-INFO +3 -3
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/README.md +2 -2
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/__init__.py +1 -1
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api/addresses_api.py +3 -3
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api/bulk_api.py +12 -6
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api/clusters_api.py +3 -3
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api/entities_api.py +3 -3
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api_client.py +1 -1
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/configuration.py +2 -2
- graphsense_python-2.15.1/graphsense/ext/bulk.py +116 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/ext/client.py +20 -3
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/address.py +2 -2
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/entity.py +1 -1
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/neighbor_address.py +3 -3
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/neighbor_cluster.py +3 -3
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/neighbor_entity.py +3 -3
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense_python.egg-info/PKG-INFO +3 -3
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/pyproject.toml +1 -1
- graphsense_python-2.15.1/tests/test_ext_bulk.py +105 -0
- graphsense_python-2.15.0/graphsense/ext/bulk.py +0 -51
- graphsense_python-2.15.0/tests/test_ext_bulk.py +0 -53
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/address_scan/__init__.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/address_scan/decompress.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/address_scan/detectors.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/address_scan/scanner.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/address_scan/validators.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api/__init__.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api/blocks_api.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api/general_api.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api/graph_api.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api/rates_api.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api/tags_api.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api/tokens_api.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api/txs_api.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/api_response.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/cli/__init__.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/cli/__main__.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/cli/bulk_cmd.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/cli/context.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/cli/convenience.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/cli/errors.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/cli/gs.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/cli/main.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/cli/raw.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/cli/scan.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/compat.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/exceptions.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/ext/__init__.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/ext/deprecation.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/ext/io.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/ext/output.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/ext/selectors.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/gs_files/__init__.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/gs_files/encoder.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/gs_files/parser.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/gs_files/summary.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/gs_files/writer.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/__init__.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/actor.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/actor_context.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/address.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/address_tag.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/address_tags.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/address_tx.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/address_tx_utxo.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/address_txs.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/block.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/block_at_date.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/concept.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/currency_stats.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/entity.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/entity_addresses.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/external_conversion.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/http_validation_error.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/label_summary.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/labeled_item_ref.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/link.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/link_utxo.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/links.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/links_inner.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/location_inner.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/neighbor_address.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/neighbor_addresses.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/neighbor_entities.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/neighbor_entity.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/rate.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/rates.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/related_address.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/related_addresses.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/search_result.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/search_result_by_currency.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/search_result_level1.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/search_result_level2.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/search_result_level3.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/search_result_level4.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/search_result_level5.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/search_result_level6.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/stats.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/tag.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/tag_cloud_entry.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/tag_summary.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/taxonomy.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/token_config.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/token_configs.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/tx.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/tx_account.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/tx_ref.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/tx_summary.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/tx_utxo.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/tx_value.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/user_reported_tag.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/user_tag_report_response.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/validation_error.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/model/values.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/__init__.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/actor.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/actor_context.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/address_output.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/address_tag.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/address_tags.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/address_tx.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/address_tx_utxo.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/address_txs.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/block.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/block_at_date.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/change_heuristics.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/cluster.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/cluster_addresses.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/coin_join_consensus.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/coin_join_heuristics.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/comparison_signal.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/comparison_verdict.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/concept.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/consensus_entry.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/currency_stats.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/direct_change_heuristic.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/entity_addresses.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/external_conversion.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_address_network_summary.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_address_overall.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_address_ref.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_address_summary.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_compare_note.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_compare_request.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_compare_signal.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_compare_verdict.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_compared_tx.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_comparison.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_lineage_edge.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_note.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_summary.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_summary_request.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_tx_characteristics.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_tx_network_summary.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_tx_overall.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_tx_ref.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/graph_tx_summary.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/http_validation_error.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/join_market_heuristic.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/label_summary.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/labeled_item_ref.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/lineage_edge.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/link.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/link_utxo.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/links.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/links_inner.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/location_inner.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/multi_input_change_heuristic.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/neighbor_addresses.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/neighbor_clusters.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/neighbor_entities.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/one_time_change_heuristic.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/per_tx_inner.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/rate.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/rates.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/related_address.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/related_addresses.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/search_result.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/search_result_by_currency.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/search_result_level1.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/search_result_level2.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/search_result_level3.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/search_result_level4.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/search_result_level5.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/search_result_level6.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/stats.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/tag.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/tag_cloud_entry.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/tag_summary.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/taxonomy.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/token_config.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/token_configs.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/transaction_comparison.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/tx.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/tx_account.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/tx_characteristics.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/tx_compared_item.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/tx_ref.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/tx_summary.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/tx_utxo.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/tx_value.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/user_reported_tag.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/user_tag_report_response.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/utxo_heuristics.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/validation_error.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/values.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/wasabi_heuristic.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/whirlpool_coin_join_heuristic.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/models/whirlpool_tx0_heuristic.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/py.typed +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense/rest.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense_python.egg-info/SOURCES.txt +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense_python.egg-info/dependency_links.txt +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense_python.egg-info/entry_points.txt +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense_python.egg-info/requires.txt +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/graphsense_python.egg-info/top_level.txt +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/setup.cfg +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_cli_bulk.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_cli_convenience.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_cli_errors.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_cli_gs.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_cli_io_pipes.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_cli_raw_mirror.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_cli_scan.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_cli_tags_and_dates.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_ext_client.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_ext_io.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_ext_output.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_ext_selectors.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_readme_template.py +0 -0
- {graphsense_python-2.15.0 → graphsense_python-2.15.1}/tests/test_regen_survives.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphsense-python
|
|
3
|
-
Version: 2.15.
|
|
3
|
+
Version: 2.15.1
|
|
4
4
|
Summary: GraphSense API
|
|
5
5
|
Author-email: Iknaio Cryptoasset Analytics GmbH <contact@iknaio.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -63,8 +63,8 @@ connector or tool source; the assistant then handles the rest.
|
|
|
63
63
|
|
|
64
64
|
This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
|
65
65
|
|
|
66
|
-
- API version: 2.15.
|
|
67
|
-
- Package version: 2.15.
|
|
66
|
+
- API version: 2.15.1
|
|
67
|
+
- Package version: 2.15.1
|
|
68
68
|
- Generator version: 7.19.0
|
|
69
69
|
- Build package: org.openapitools.codegen.languages.PythonClientCodegen
|
|
70
70
|
For more information, please visit [https://www.iknaio.com/](https://www.iknaio.com/)
|
|
@@ -41,8 +41,8 @@ connector or tool source; the assistant then handles the rest.
|
|
|
41
41
|
|
|
42
42
|
This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
|
43
43
|
|
|
44
|
-
- API version: 2.15.
|
|
45
|
-
- Package version: 2.15.
|
|
44
|
+
- API version: 2.15.1
|
|
45
|
+
- Package version: 2.15.1
|
|
46
46
|
- Generator version: 7.19.0
|
|
47
47
|
- Build package: org.openapitools.codegen.languages.PythonClientCodegen
|
|
48
48
|
For more information, please visit [https://www.iknaio.com/](https://www.iknaio.com/)
|
|
@@ -1795,7 +1795,7 @@ class AddressesApi:
|
|
|
1795
1795
|
) -> NeighborAddresses:
|
|
1796
1796
|
"""List neighboring addresses
|
|
1797
1797
|
|
|
1798
|
-
Returns neighboring addresses connected to the given address in the transaction graph.
|
|
1798
|
+
Returns neighboring addresses connected to the given address in the transaction graph. Per neighbor, `value` and `no_txs` are **edge-scoped** (the value transferred and number of transactions between the queried address and that neighbor), while the embedded `address` object carries the neighbor's own **lifetime** attributes (its balance, degrees, first_tx/last_tx, …), not relative to the queried address.
|
|
1799
1799
|
|
|
1800
1800
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
1801
1801
|
:type currency: str
|
|
@@ -1892,7 +1892,7 @@ class AddressesApi:
|
|
|
1892
1892
|
) -> ApiResponse[NeighborAddresses]:
|
|
1893
1893
|
"""List neighboring addresses
|
|
1894
1894
|
|
|
1895
|
-
Returns neighboring addresses connected to the given address in the transaction graph.
|
|
1895
|
+
Returns neighboring addresses connected to the given address in the transaction graph. Per neighbor, `value` and `no_txs` are **edge-scoped** (the value transferred and number of transactions between the queried address and that neighbor), while the embedded `address` object carries the neighbor's own **lifetime** attributes (its balance, degrees, first_tx/last_tx, …), not relative to the queried address.
|
|
1896
1896
|
|
|
1897
1897
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
1898
1898
|
:type currency: str
|
|
@@ -1989,7 +1989,7 @@ class AddressesApi:
|
|
|
1989
1989
|
) -> RESTResponseType:
|
|
1990
1990
|
"""List neighboring addresses
|
|
1991
1991
|
|
|
1992
|
-
Returns neighboring addresses connected to the given address in the transaction graph.
|
|
1992
|
+
Returns neighboring addresses connected to the given address in the transaction graph. Per neighbor, `value` and `no_txs` are **edge-scoped** (the value transferred and number of transactions between the queried address and that neighbor), while the embedded `address` object carries the neighbor's own **lifetime** attributes (its balance, degrees, first_tx/last_tx, …), not relative to the queried address.
|
|
1993
1993
|
|
|
1994
1994
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
1995
1995
|
:type currency: str
|
|
@@ -147,7 +147,7 @@ class BulkApi:
|
|
|
147
147
|
) -> object:
|
|
148
148
|
"""Stream bulk operation results as CSV
|
|
149
149
|
|
|
150
|
-
Executes a supported operation for multiple key values and streams flattened result rows as CSV.
|
|
150
|
+
Executes a supported operation for multiple key values and streams flattened result rows as CSV. Each key list in the request body is capped (10,000 items by default), as is the body itself (8 MiB by default); split longer requests into several calls.
|
|
151
151
|
|
|
152
152
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
153
153
|
:type currency: str
|
|
@@ -193,6 +193,7 @@ class BulkApi:
|
|
|
193
193
|
_response_types_map: Dict[str, Optional[str]] = {
|
|
194
194
|
'200': "object",
|
|
195
195
|
'400': None,
|
|
196
|
+
'413': None,
|
|
196
197
|
'422': None,
|
|
197
198
|
}
|
|
198
199
|
response_data = self.api_client.call_api(
|
|
@@ -228,7 +229,7 @@ class BulkApi:
|
|
|
228
229
|
) -> ApiResponse[object]:
|
|
229
230
|
"""Stream bulk operation results as CSV
|
|
230
231
|
|
|
231
|
-
Executes a supported operation for multiple key values and streams flattened result rows as CSV.
|
|
232
|
+
Executes a supported operation for multiple key values and streams flattened result rows as CSV. Each key list in the request body is capped (10,000 items by default), as is the body itself (8 MiB by default); split longer requests into several calls.
|
|
232
233
|
|
|
233
234
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
234
235
|
:type currency: str
|
|
@@ -274,6 +275,7 @@ class BulkApi:
|
|
|
274
275
|
_response_types_map: Dict[str, Optional[str]] = {
|
|
275
276
|
'200': "object",
|
|
276
277
|
'400': None,
|
|
278
|
+
'413': None,
|
|
277
279
|
'422': None,
|
|
278
280
|
}
|
|
279
281
|
response_data = self.api_client.call_api(
|
|
@@ -309,7 +311,7 @@ class BulkApi:
|
|
|
309
311
|
) -> RESTResponseType:
|
|
310
312
|
"""Stream bulk operation results as CSV
|
|
311
313
|
|
|
312
|
-
Executes a supported operation for multiple key values and streams flattened result rows as CSV.
|
|
314
|
+
Executes a supported operation for multiple key values and streams flattened result rows as CSV. Each key list in the request body is capped (10,000 items by default), as is the body itself (8 MiB by default); split longer requests into several calls.
|
|
313
315
|
|
|
314
316
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
315
317
|
:type currency: str
|
|
@@ -355,6 +357,7 @@ class BulkApi:
|
|
|
355
357
|
_response_types_map: Dict[str, Optional[str]] = {
|
|
356
358
|
'200': "object",
|
|
357
359
|
'400': None,
|
|
360
|
+
'413': None,
|
|
358
361
|
'422': None,
|
|
359
362
|
}
|
|
360
363
|
response_data = self.api_client.call_api(
|
|
@@ -474,7 +477,7 @@ class BulkApi:
|
|
|
474
477
|
) -> object:
|
|
475
478
|
"""Stream bulk operation results as JSON
|
|
476
479
|
|
|
477
|
-
Executes a supported operation for multiple key values and streams flattened result rows as JSON.
|
|
480
|
+
Executes a supported operation for multiple key values and streams flattened result rows as JSON. Each key list in the request body is capped (10,000 items by default), as is the body itself (8 MiB by default); split longer requests into several calls.
|
|
478
481
|
|
|
479
482
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
480
483
|
:type currency: str
|
|
@@ -520,6 +523,7 @@ class BulkApi:
|
|
|
520
523
|
_response_types_map: Dict[str, Optional[str]] = {
|
|
521
524
|
'200': "object",
|
|
522
525
|
'400': None,
|
|
526
|
+
'413': None,
|
|
523
527
|
'422': None,
|
|
524
528
|
}
|
|
525
529
|
response_data = self.api_client.call_api(
|
|
@@ -555,7 +559,7 @@ class BulkApi:
|
|
|
555
559
|
) -> ApiResponse[object]:
|
|
556
560
|
"""Stream bulk operation results as JSON
|
|
557
561
|
|
|
558
|
-
Executes a supported operation for multiple key values and streams flattened result rows as JSON.
|
|
562
|
+
Executes a supported operation for multiple key values and streams flattened result rows as JSON. Each key list in the request body is capped (10,000 items by default), as is the body itself (8 MiB by default); split longer requests into several calls.
|
|
559
563
|
|
|
560
564
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
561
565
|
:type currency: str
|
|
@@ -601,6 +605,7 @@ class BulkApi:
|
|
|
601
605
|
_response_types_map: Dict[str, Optional[str]] = {
|
|
602
606
|
'200': "object",
|
|
603
607
|
'400': None,
|
|
608
|
+
'413': None,
|
|
604
609
|
'422': None,
|
|
605
610
|
}
|
|
606
611
|
response_data = self.api_client.call_api(
|
|
@@ -636,7 +641,7 @@ class BulkApi:
|
|
|
636
641
|
) -> RESTResponseType:
|
|
637
642
|
"""Stream bulk operation results as JSON
|
|
638
643
|
|
|
639
|
-
Executes a supported operation for multiple key values and streams flattened result rows as JSON.
|
|
644
|
+
Executes a supported operation for multiple key values and streams flattened result rows as JSON. Each key list in the request body is capped (10,000 items by default), as is the body itself (8 MiB by default); split longer requests into several calls.
|
|
640
645
|
|
|
641
646
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
642
647
|
:type currency: str
|
|
@@ -682,6 +687,7 @@ class BulkApi:
|
|
|
682
687
|
_response_types_map: Dict[str, Optional[str]] = {
|
|
683
688
|
'200': "object",
|
|
684
689
|
'400': None,
|
|
690
|
+
'413': None,
|
|
685
691
|
'422': None,
|
|
686
692
|
}
|
|
687
693
|
response_data = self.api_client.call_api(
|
|
@@ -1549,7 +1549,7 @@ class ClustersApi:
|
|
|
1549
1549
|
) -> NeighborClusters:
|
|
1550
1550
|
"""(Deprecated) List neighboring clusters
|
|
1551
1551
|
|
|
1552
|
-
Returns neighboring clusters connected to the given cluster in the cluster graph.
|
|
1552
|
+
Returns neighboring clusters connected to the given cluster in the cluster graph. Per neighbor, `value` and `no_txs` are **edge-scoped** (the value transferred and number of transactions between the queried cluster and that neighbor), while the embedded cluster object carries the neighbor's own **lifetime** attributes, not relative to the queried cluster.
|
|
1553
1553
|
|
|
1554
1554
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
1555
1555
|
:type currency: str
|
|
@@ -1655,7 +1655,7 @@ class ClustersApi:
|
|
|
1655
1655
|
) -> ApiResponse[NeighborClusters]:
|
|
1656
1656
|
"""(Deprecated) List neighboring clusters
|
|
1657
1657
|
|
|
1658
|
-
Returns neighboring clusters connected to the given cluster in the cluster graph.
|
|
1658
|
+
Returns neighboring clusters connected to the given cluster in the cluster graph. Per neighbor, `value` and `no_txs` are **edge-scoped** (the value transferred and number of transactions between the queried cluster and that neighbor), while the embedded cluster object carries the neighbor's own **lifetime** attributes, not relative to the queried cluster.
|
|
1659
1659
|
|
|
1660
1660
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
1661
1661
|
:type currency: str
|
|
@@ -1761,7 +1761,7 @@ class ClustersApi:
|
|
|
1761
1761
|
) -> RESTResponseType:
|
|
1762
1762
|
"""(Deprecated) List neighboring clusters
|
|
1763
1763
|
|
|
1764
|
-
Returns neighboring clusters connected to the given cluster in the cluster graph.
|
|
1764
|
+
Returns neighboring clusters connected to the given cluster in the cluster graph. Per neighbor, `value` and `no_txs` are **edge-scoped** (the value transferred and number of transactions between the queried cluster and that neighbor), while the embedded cluster object carries the neighbor's own **lifetime** attributes, not relative to the queried cluster.
|
|
1765
1765
|
|
|
1766
1766
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
1767
1767
|
:type currency: str
|
|
@@ -1555,7 +1555,7 @@ class EntitiesApi:
|
|
|
1555
1555
|
) -> NeighborEntities:
|
|
1556
1556
|
"""(Deprecated) List neighboring entities
|
|
1557
1557
|
|
|
1558
|
-
Returns neighboring entities connected to the given entity in the entity graph.
|
|
1558
|
+
Returns neighboring entities connected to the given entity in the entity graph. Per neighbor, `value` and `no_txs` are **edge-scoped** (the value transferred and number of transactions between the queried entity and that neighbor), while the embedded entity/cluster object carries the neighbor's own **lifetime** attributes, not relative to the queried entity.
|
|
1559
1559
|
|
|
1560
1560
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
1561
1561
|
:type currency: str
|
|
@@ -1661,7 +1661,7 @@ class EntitiesApi:
|
|
|
1661
1661
|
) -> ApiResponse[NeighborEntities]:
|
|
1662
1662
|
"""(Deprecated) List neighboring entities
|
|
1663
1663
|
|
|
1664
|
-
Returns neighboring entities connected to the given entity in the entity graph.
|
|
1664
|
+
Returns neighboring entities connected to the given entity in the entity graph. Per neighbor, `value` and `no_txs` are **edge-scoped** (the value transferred and number of transactions between the queried entity and that neighbor), while the embedded entity/cluster object carries the neighbor's own **lifetime** attributes, not relative to the queried entity.
|
|
1665
1665
|
|
|
1666
1666
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
1667
1667
|
:type currency: str
|
|
@@ -1767,7 +1767,7 @@ class EntitiesApi:
|
|
|
1767
1767
|
) -> RESTResponseType:
|
|
1768
1768
|
"""(Deprecated) List neighboring entities
|
|
1769
1769
|
|
|
1770
|
-
Returns neighboring entities connected to the given entity in the entity graph.
|
|
1770
|
+
Returns neighboring entities connected to the given entity in the entity graph. Per neighbor, `value` and `no_txs` are **edge-scoped** (the value transferred and number of transactions between the queried entity and that neighbor), while the embedded entity/cluster object carries the neighbor's own **lifetime** attributes, not relative to the queried entity.
|
|
1771
1771
|
|
|
1772
1772
|
:param currency: The cryptocurrency code (e.g., btc) (required)
|
|
1773
1773
|
:type currency: str
|
|
@@ -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 = 'graphsense-python/2.15.
|
|
98
|
+
self.user_agent = 'graphsense-python/2.15.1'
|
|
99
99
|
self.client_side_validation = configuration.client_side_validation
|
|
100
100
|
|
|
101
101
|
def __enter__(self):
|
|
@@ -533,8 +533,8 @@ conf = graphsense.Configuration(
|
|
|
533
533
|
return "Python SDK Debug Report:\n"\
|
|
534
534
|
"OS: {env}\n"\
|
|
535
535
|
"Python Version: {pyversion}\n"\
|
|
536
|
-
"Version of the API: 2.15.
|
|
537
|
-
"SDK Package Version: 2.15.
|
|
536
|
+
"Version of the API: 2.15.1\n"\
|
|
537
|
+
"SDK Package Version: 2.15.1".\
|
|
538
538
|
format(env=sys.platform, pyversion=sys.version)
|
|
539
539
|
|
|
540
540
|
def get_host_settings(self) -> List[HostSetting]:
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Decide between per-item calls and the /bulk endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import io
|
|
7
|
+
import sys
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
from typing import Any, Callable, Iterable, Iterator, Optional
|
|
10
|
+
|
|
11
|
+
DEFAULT_BULK_THRESHOLD = 10
|
|
12
|
+
|
|
13
|
+
# The server bounds how many items one bulk request may carry (10,000 by
|
|
14
|
+
# default) and answers a longer list with a 400.
|
|
15
|
+
# Splitting below that bound keeps arbitrarily long key lists working from the
|
|
16
|
+
# client's side. Deployments that lower the server cap need a matching
|
|
17
|
+
# `chunk_size` on the call.
|
|
18
|
+
DEFAULT_BULK_CHUNK_SIZE = 5000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def should_bulk(
|
|
22
|
+
n: int,
|
|
23
|
+
*,
|
|
24
|
+
threshold: int = DEFAULT_BULK_THRESHOLD,
|
|
25
|
+
override: Optional[bool] = None,
|
|
26
|
+
) -> bool:
|
|
27
|
+
"""True if we should go through the bulk endpoint.
|
|
28
|
+
|
|
29
|
+
`override=None`: use threshold.
|
|
30
|
+
`override=True`: always bulk (even for small N).
|
|
31
|
+
`override=False`: never bulk.
|
|
32
|
+
"""
|
|
33
|
+
if override is not None:
|
|
34
|
+
return override
|
|
35
|
+
return n >= threshold
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_parallel(
|
|
39
|
+
call: Callable[[str], Any],
|
|
40
|
+
keys: Iterable[str],
|
|
41
|
+
*,
|
|
42
|
+
max_workers: int = 8,
|
|
43
|
+
) -> list[Any]:
|
|
44
|
+
"""Call `call(k)` for each k in keys across a thread pool, preserving order."""
|
|
45
|
+
items = list(keys)
|
|
46
|
+
with ThreadPoolExecutor(max_workers=max_workers) as pool:
|
|
47
|
+
return list(pool.map(call, items))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def announce_switch(stream=None) -> None:
|
|
51
|
+
"""Emit a one-line stderr notice the first time we switch to bulk.
|
|
52
|
+
|
|
53
|
+
Caller must implement the "first time" logic — this just prints.
|
|
54
|
+
"""
|
|
55
|
+
out = stream if stream is not None else sys.stderr
|
|
56
|
+
print(
|
|
57
|
+
"notice: switching to bulk endpoint "
|
|
58
|
+
"(rows are flat; per-item typed models are available with --no-bulk)",
|
|
59
|
+
file=out,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def chunked(items: Iterable[Any], size: int) -> Iterator[list]:
|
|
64
|
+
"""Yield consecutive lists of at most `size` items."""
|
|
65
|
+
if size <= 0:
|
|
66
|
+
raise ValueError("chunk size must be positive")
|
|
67
|
+
batch: list = []
|
|
68
|
+
for item in items:
|
|
69
|
+
batch.append(item)
|
|
70
|
+
if len(batch) == size:
|
|
71
|
+
yield batch
|
|
72
|
+
batch = []
|
|
73
|
+
if batch:
|
|
74
|
+
yield batch
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def merge_json_chunks(chunks: Iterable[Any]) -> list:
|
|
78
|
+
"""Concatenate the row lists returned by several bulk.json calls."""
|
|
79
|
+
rows: list = []
|
|
80
|
+
for chunk in chunks:
|
|
81
|
+
if chunk is None:
|
|
82
|
+
continue
|
|
83
|
+
rows.extend(chunk if isinstance(chunk, list) else [chunk])
|
|
84
|
+
return rows
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def merge_csv_chunks(chunks: Iterable[str]) -> str:
|
|
88
|
+
"""Concatenate several bulk.csv responses into one CSV document.
|
|
89
|
+
|
|
90
|
+
Each response carries its own header, and the server infers those columns
|
|
91
|
+
from the first rows of that request — so two chunks of the same query can
|
|
92
|
+
legitimately disagree on the column set. Rows are therefore re-keyed onto
|
|
93
|
+
the union of all headers (in first-seen order) rather than pasted together.
|
|
94
|
+
"""
|
|
95
|
+
rows: list[dict] = []
|
|
96
|
+
columns: list[str] = []
|
|
97
|
+
seen = set()
|
|
98
|
+
for chunk in chunks:
|
|
99
|
+
if not chunk or not chunk.strip():
|
|
100
|
+
continue
|
|
101
|
+
reader = csv.DictReader(io.StringIO(chunk))
|
|
102
|
+
for name in reader.fieldnames or []:
|
|
103
|
+
if name not in seen:
|
|
104
|
+
seen.add(name)
|
|
105
|
+
columns.append(name)
|
|
106
|
+
rows.extend(reader)
|
|
107
|
+
|
|
108
|
+
if not columns:
|
|
109
|
+
return ""
|
|
110
|
+
|
|
111
|
+
out = io.StringIO()
|
|
112
|
+
writer = csv.DictWriter(out, columns, restval="", extrasaction="ignore")
|
|
113
|
+
writer.writeheader()
|
|
114
|
+
for row in rows:
|
|
115
|
+
writer.writerow(row)
|
|
116
|
+
return out.getvalue()
|
|
@@ -26,6 +26,7 @@ from dateutil import parser as _dateutil_parser
|
|
|
26
26
|
import graphsense
|
|
27
27
|
from graphsense.api_client import ApiClient
|
|
28
28
|
from graphsense.configuration import Configuration
|
|
29
|
+
from graphsense.ext import bulk as bulk_mod
|
|
29
30
|
from graphsense.ext.deprecation import install as install_deprecation_hook
|
|
30
31
|
|
|
31
32
|
# APIs that expose only deprecated endpoints; hidden from the convenience
|
|
@@ -506,15 +507,31 @@ class GraphSense:
|
|
|
506
507
|
format: str = "json",
|
|
507
508
|
num_pages: int = 1,
|
|
508
509
|
key_field: str = "address",
|
|
510
|
+
chunk_size: int = bulk_mod.DEFAULT_BULK_CHUNK_SIZE,
|
|
509
511
|
) -> Any:
|
|
510
512
|
"""Call the /bulk.<format>/<operation> endpoint with `keys`.
|
|
511
513
|
|
|
512
514
|
`key_field` is the dict key that the bulk operation expects in the
|
|
513
515
|
request body (e.g. "address", "tx_hash", "cluster"). Default is
|
|
514
516
|
"address" which is by far the most common.
|
|
517
|
+
|
|
518
|
+
The server bounds how many items a single bulk request may carry, so a
|
|
519
|
+
longer key list is split into `chunk_size` batches and the responses are
|
|
520
|
+
merged. Pass `chunk_size=0` to send the list in one request regardless.
|
|
515
521
|
"""
|
|
516
522
|
ccy = self._currency(currency)
|
|
517
|
-
|
|
523
|
+
keys = list(keys)
|
|
524
|
+
|
|
525
|
+
def call(batch: list) -> Any:
|
|
526
|
+
body = {key_field: batch}
|
|
527
|
+
if format == "csv":
|
|
528
|
+
return self.raw.bulk.bulk_csv(ccy, operation, num_pages, body)
|
|
529
|
+
return self.raw.bulk.bulk_json(ccy, operation, num_pages, body)
|
|
530
|
+
|
|
531
|
+
if chunk_size <= 0 or len(keys) <= chunk_size:
|
|
532
|
+
return call(keys)
|
|
533
|
+
|
|
534
|
+
results = [call(batch) for batch in bulk_mod.chunked(keys, chunk_size)]
|
|
518
535
|
if format == "csv":
|
|
519
|
-
return
|
|
520
|
-
return
|
|
536
|
+
return bulk_mod.merge_csv_chunks(results)
|
|
537
|
+
return bulk_mod.merge_json_chunks(results)
|
|
@@ -35,8 +35,8 @@ class Address(BaseModel):
|
|
|
35
35
|
balance: Values
|
|
36
36
|
total_received: Values
|
|
37
37
|
total_spent: Values
|
|
38
|
-
first_tx: Optional[TxSummary] = None
|
|
39
|
-
last_tx: Optional[TxSummary] = None
|
|
38
|
+
first_tx: Optional[TxSummary] = Field(default=None, description="First transaction in which this address appears, over its entire history — independent of any neighbor, direction, or date filter. Null if the address has no transactions of its own.")
|
|
39
|
+
last_tx: Optional[TxSummary] = Field(default=None, description="Last transaction in which this address appears, over its entire history — independent of any neighbor, direction, or date filter. Null if the address has no transactions of its own.")
|
|
40
40
|
in_degree: StrictInt
|
|
41
41
|
out_degree: StrictInt
|
|
42
42
|
no_incoming_txs: StrictInt
|
|
@@ -25,7 +25,7 @@ ENTITY_ANY_OF_SCHEMAS = ["Entity", "int"]
|
|
|
25
25
|
|
|
26
26
|
class Entity(BaseModel):
|
|
27
27
|
"""
|
|
28
|
-
Deprecated alias of `cluster`. Use `cluster` instead; this field is retained for backwards compatibility and will be removed in a future release.
|
|
28
|
+
Deprecated alias of `cluster`. Use `cluster` instead; this field is retained for backwards compatibility and will be removed in a future release. When expanded to an object, these are the neighbor cluster's own lifetime attributes, NOT relative to the queried cluster or the edge between them.
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
31
|
# data type: Entity
|
|
@@ -15,7 +15,7 @@ import pprint
|
|
|
15
15
|
import re # noqa: F401
|
|
16
16
|
import json
|
|
17
17
|
|
|
18
|
-
from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
|
|
19
19
|
from typing import Any, ClassVar, Dict, List, Optional
|
|
20
20
|
from graphsense.models.address import Address
|
|
21
21
|
from graphsense.models.values import Values
|
|
@@ -27,10 +27,10 @@ class NeighborAddress(BaseModel):
|
|
|
27
27
|
Neighbor address model.
|
|
28
28
|
""" # noqa: E501
|
|
29
29
|
value: Values
|
|
30
|
-
no_txs: StrictInt
|
|
30
|
+
no_txs: StrictInt = Field(description="Number of transactions on the edge between the queried address and this neighbor (edge-scoped, not the neighbor's lifetime transaction count).")
|
|
31
31
|
address: Address
|
|
32
32
|
labels: Optional[List[StrictStr]] = None
|
|
33
|
-
token_values: Optional[Dict[str, Values]] = None
|
|
33
|
+
token_values: Optional[Dict[str, Values]] = Field(default=None, description="Per-token value transferred on this edge (edge-scoped).")
|
|
34
34
|
__properties: ClassVar[List[str]] = ["value", "no_txs", "address", "labels", "token_values"]
|
|
35
35
|
|
|
36
36
|
model_config = ConfigDict(
|
|
@@ -15,7 +15,7 @@ import pprint
|
|
|
15
15
|
import re # noqa: F401
|
|
16
16
|
import json
|
|
17
17
|
|
|
18
|
-
from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
|
|
19
19
|
from typing import Any, ClassVar, Dict, List, Optional
|
|
20
20
|
from graphsense.models.cluster import Cluster
|
|
21
21
|
from graphsense.models.entity import Entity
|
|
@@ -28,10 +28,10 @@ class NeighborCluster(BaseModel):
|
|
|
28
28
|
Neighbor cluster (canonical name, supersedes `NeighborEntity`).
|
|
29
29
|
""" # noqa: E501
|
|
30
30
|
value: Values
|
|
31
|
-
no_txs: StrictInt
|
|
31
|
+
no_txs: StrictInt = Field(description="Number of transactions on the edge between the queried cluster and this neighbor (edge-scoped, not the neighbor's lifetime transaction count).")
|
|
32
32
|
entity: Optional[Entity] = None
|
|
33
33
|
labels: Optional[List[StrictStr]] = None
|
|
34
|
-
token_values: Optional[Dict[str, Values]] = None
|
|
34
|
+
token_values: Optional[Dict[str, Values]] = Field(default=None, description="Per-token value transferred on this edge (edge-scoped).")
|
|
35
35
|
cluster: Optional[Cluster]
|
|
36
36
|
__properties: ClassVar[List[str]] = ["value", "no_txs", "entity", "labels", "token_values", "cluster"]
|
|
37
37
|
|
|
@@ -15,7 +15,7 @@ import pprint
|
|
|
15
15
|
import re # noqa: F401
|
|
16
16
|
import json
|
|
17
17
|
|
|
18
|
-
from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
|
|
19
19
|
from typing import Any, ClassVar, Dict, List, Optional
|
|
20
20
|
from graphsense.models.cluster import Cluster
|
|
21
21
|
from graphsense.models.entity import Entity
|
|
@@ -28,10 +28,10 @@ class NeighborEntity(BaseModel):
|
|
|
28
28
|
Neighbor cluster model (legacy name: NeighborEntity). Dual-emits the neighbor reference under both `entity` (deprecated) and `cluster` (preferred) keys, mirroring the alias pattern on the top-level `Entity`/`Cluster` models. The value is either an integer ID or a full `Entity`/`Cluster` object; whichever shape `entity` carries, `cluster` carries the same value.
|
|
29
29
|
""" # noqa: E501
|
|
30
30
|
value: Values
|
|
31
|
-
no_txs: StrictInt
|
|
31
|
+
no_txs: StrictInt = Field(description="Number of transactions on the edge between the queried cluster and this neighbor (edge-scoped, not the neighbor's lifetime transaction count).")
|
|
32
32
|
entity: Optional[Entity] = None
|
|
33
33
|
labels: Optional[List[StrictStr]] = None
|
|
34
|
-
token_values: Optional[Dict[str, Values]] = None
|
|
34
|
+
token_values: Optional[Dict[str, Values]] = Field(default=None, description="Per-token value transferred on this edge (edge-scoped).")
|
|
35
35
|
cluster: Optional[Cluster]
|
|
36
36
|
__properties: ClassVar[List[str]] = ["value", "no_txs", "entity", "labels", "token_values", "cluster"]
|
|
37
37
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphsense-python
|
|
3
|
-
Version: 2.15.
|
|
3
|
+
Version: 2.15.1
|
|
4
4
|
Summary: GraphSense API
|
|
5
5
|
Author-email: Iknaio Cryptoasset Analytics GmbH <contact@iknaio.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -63,8 +63,8 @@ connector or tool source; the assistant then handles the rest.
|
|
|
63
63
|
|
|
64
64
|
This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
|
65
65
|
|
|
66
|
-
- API version: 2.15.
|
|
67
|
-
- Package version: 2.15.
|
|
66
|
+
- API version: 2.15.1
|
|
67
|
+
- Package version: 2.15.1
|
|
68
68
|
- Generator version: 7.19.0
|
|
69
69
|
- Build package: org.openapitools.codegen.languages.PythonClientCodegen
|
|
70
70
|
For more information, please visit [https://www.iknaio.com/](https://www.iknaio.com/)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Bulk-vs-per-item dispatch heuristic and GraphSense.bulk()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from graphsense.ext import bulk as bulk_mod
|
|
6
|
+
from graphsense.ext import GraphSense
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_should_bulk_default_threshold():
|
|
10
|
+
assert not bulk_mod.should_bulk(5)
|
|
11
|
+
assert bulk_mod.should_bulk(10)
|
|
12
|
+
assert bulk_mod.should_bulk(100)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_should_bulk_override_true():
|
|
16
|
+
assert bulk_mod.should_bulk(1, override=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_should_bulk_override_false():
|
|
20
|
+
assert not bulk_mod.should_bulk(1000, override=False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_run_parallel_preserves_order():
|
|
24
|
+
def f(x):
|
|
25
|
+
return x * 2
|
|
26
|
+
|
|
27
|
+
assert bulk_mod.run_parallel(f, [1, 2, 3, 4]) == [2, 4, 6, 8]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_gs_bulk_posts_to_json_endpoint(gs: GraphSense, http_mock):
|
|
31
|
+
http_mock.add(
|
|
32
|
+
"POST",
|
|
33
|
+
"/btc/bulk.json/get_address",
|
|
34
|
+
json_body=[{"address": "1A", "balance": 1}],
|
|
35
|
+
)
|
|
36
|
+
gs.bulk("get_address", ["1A", "1B"])
|
|
37
|
+
assert any("/btc/bulk.json/get_address" in c.url for c in http_mock.calls)
|
|
38
|
+
# body should contain the key list
|
|
39
|
+
call = [c for c in http_mock.calls if "/bulk.json" in c.url][0]
|
|
40
|
+
body = call.body
|
|
41
|
+
assert "1A" in str(body)
|
|
42
|
+
assert "1B" in str(body)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_gs_bulk_csv_hits_csv_endpoint(gs: GraphSense, http_mock):
|
|
46
|
+
http_mock.add(
|
|
47
|
+
"POST",
|
|
48
|
+
"/btc/bulk.csv/get_address",
|
|
49
|
+
body="address,balance\n1A,1\n",
|
|
50
|
+
headers={"content-type": "text/csv"},
|
|
51
|
+
)
|
|
52
|
+
gs.bulk("get_address", ["1A"], format="csv")
|
|
53
|
+
assert any("/btc/bulk.csv/get_address" in c.url for c in http_mock.calls)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_chunked_splits_evenly_and_keeps_remainder():
|
|
57
|
+
assert list(bulk_mod.chunked([1, 2, 3, 4, 5], 2)) == [[1, 2], [3, 4], [5]]
|
|
58
|
+
assert list(bulk_mod.chunked([], 2)) == []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_merge_json_chunks_concatenates_rows():
|
|
62
|
+
merged = bulk_mod.merge_json_chunks([[{"a": 1}], [{"a": 2}, {"a": 3}], None])
|
|
63
|
+
assert merged == [{"a": 1}, {"a": 2}, {"a": 3}]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_merge_csv_chunks_unions_headers():
|
|
67
|
+
"""Each chunk gets its own server-side header inference, so a later chunk
|
|
68
|
+
may carry a column the first one never saw. Nothing may be dropped."""
|
|
69
|
+
first = "address,balance\n1A,1\n"
|
|
70
|
+
second = "address,balance,token\n1B,2,usdt\n"
|
|
71
|
+
merged = bulk_mod.merge_csv_chunks([first, second])
|
|
72
|
+
lines = merged.strip().splitlines()
|
|
73
|
+
assert lines[0] == "address,balance,token"
|
|
74
|
+
assert lines[1] == "1A,1,"
|
|
75
|
+
assert lines[2] == "1B,2,usdt"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_gs_bulk_splits_long_key_lists(gs: GraphSense, http_mock):
|
|
79
|
+
"""A key list longer than the server's per-request cap is sent as several
|
|
80
|
+
requests instead of one oversized (and rejected) request."""
|
|
81
|
+
http_mock.add(
|
|
82
|
+
"POST",
|
|
83
|
+
"/btc/bulk.json/get_address",
|
|
84
|
+
json_body=[{"address": "1A", "balance": 1}],
|
|
85
|
+
)
|
|
86
|
+
keys = [f"addr{i}" for i in range(7)]
|
|
87
|
+
result = gs.bulk("get_address", keys, chunk_size=3)
|
|
88
|
+
|
|
89
|
+
calls = [c for c in http_mock.calls if "/bulk.json" in c.url]
|
|
90
|
+
assert len(calls) == 3
|
|
91
|
+
sent = [str(c.body) for c in calls]
|
|
92
|
+
assert "addr0" in sent[0] and "addr2" in sent[0] and "addr3" not in sent[0]
|
|
93
|
+
assert "addr6" in sent[2]
|
|
94
|
+
# every chunk's rows come back concatenated
|
|
95
|
+
assert len(result) == 3
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_gs_bulk_sends_one_request_below_the_chunk_size(gs: GraphSense, http_mock):
|
|
99
|
+
http_mock.add(
|
|
100
|
+
"POST",
|
|
101
|
+
"/btc/bulk.json/get_address",
|
|
102
|
+
json_body=[{"address": "1A", "balance": 1}],
|
|
103
|
+
)
|
|
104
|
+
gs.bulk("get_address", ["1A", "1B"], chunk_size=100)
|
|
105
|
+
assert len([c for c in http_mock.calls if "/bulk.json" in c.url]) == 1
|