polyswarm-api 4.3.0__tar.gz → 4.4.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 (20) hide show
  1. {polyswarm_api-4.3.0/src/polyswarm_api.egg-info → polyswarm_api-4.4.0}/PKG-INFO +1 -1
  2. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/pyproject.toml +2 -2
  3. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/__init__.py +1 -1
  4. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/aio/api.py +47 -5
  5. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/api.py +58 -4
  6. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/core.py +12 -1
  7. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/resources.py +49 -0
  8. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0/src/polyswarm_api.egg-info}/PKG-INFO +1 -1
  9. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/LICENSE +0 -0
  10. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/README.md +0 -0
  11. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/setup.cfg +0 -0
  12. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/aio/__init__.py +0 -0
  13. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/aio/session.py +0 -0
  14. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/exceptions.py +0 -0
  15. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/session.py +0 -0
  16. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/settings.py +0 -0
  17. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api.egg-info/SOURCES.txt +0 -0
  18. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api.egg-info/dependency_links.txt +0 -0
  19. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api.egg-info/requires.txt +0 -0
  20. {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyswarm_api
3
- Version: 4.3.0
3
+ Version: 4.4.0
4
4
  Summary: Client library to simplify interacting with the PolySwarm consumer API
5
5
  Author-email: PolySwarm Developers <info@polyswarm.io>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "polyswarm_api"
7
- version = "4.3.0"
7
+ version = "4.4.0"
8
8
  description = "Client library to simplify interacting with the PolySwarm consumer API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10,<4"
@@ -55,7 +55,7 @@ package-dir = { "" = "src" }
55
55
  where = ["src"]
56
56
 
57
57
  [tool.bumpversion]
58
- current_version = "4.3.0"
58
+ current_version = "4.4.0"
59
59
  commit = true
60
60
  tag = false
61
61
  sign_tags = true
@@ -1,5 +1,5 @@
1
1
  # https://www.python.org/dev/peps/pep-0008/#module-level-dunder-names
2
- __version__ = '4.3.0'
2
+ __version__ = '4.4.0'
3
3
  __release_url__ = 'https://api.github.com/repos/polyswarm/polyswarm-api/releases/latest'
4
4
 
5
5
  from . import api
@@ -18,7 +18,7 @@ import logging
18
18
  import time
19
19
 
20
20
  from polyswarm_api import exceptions, resources, settings
21
- from polyswarm_api.core import PolyswarmRequest
21
+ from polyswarm_api.core import PolyswarmRequest, _as_result_bound
22
22
 
23
23
  from .session import AsyncPolyswarmSession
24
24
 
@@ -502,23 +502,35 @@ class PolySwarmAsyncAPI:
502
502
  return await self._single(resources.LiveYaraRuleset.delete(self, rule_id=rule_id))
503
503
 
504
504
  async def live_feed(self, since=None, rule_name=None, family=None,
505
- polyscore_lower=None, polyscore_upper=None, community=None):
505
+ polyscore_lower=None, polyscore_upper=None, community=None,
506
+ livescan_id=None, max_results=None):
506
507
  """
507
508
  Get live hunts feed
508
509
 
509
- :param since: Fetch results from the last "since" minutes
510
+ :param since: Window in SECONDS (this said "minutes" in earlier releases and was
511
+ wrong). Absent or 0 means no time filter at all.
510
512
  :param rule_name: Filter hunt results on the provided rule name (exact match).
511
513
  :param family: Filter hunt results based on the family name (exact match).
512
514
  :param polyscore_lower: Polyscore lower bound for the hunt results.
513
515
  :param polyscore_upper: Polyscore upper bound for the hunt results.
514
516
  :param community: Community to retrieve live results from, or public/private.
517
+ :param livescan_id: Scope the feed to one live hunt's results.
518
+ :param max_results: Total results to yield, not a page size — paging
519
+ continues in the server's own chunks until the total is reached.
520
+ None, 0 or a negative means no bound: every page, as before.
515
521
  :return: Generator of HuntResult resources
516
522
  """
523
+ bound = _as_result_bound(max_results)
524
+ yielded = 0
517
525
  async for item in self._paginate(resources.LiveHuntResult.list(
518
526
  self, since=since, rule_name=rule_name, family=family,
519
527
  polyscore_lower=polyscore_lower, polyscore_upper=polyscore_upper,
528
+ livescan_id=livescan_id,
520
529
  community=community or self.community)):
521
530
  yield item
531
+ yielded += 1
532
+ if bound is not None and yielded >= bound:
533
+ return
522
534
 
523
535
  async def live_feed_delete(self, result_ids):
524
536
  """
@@ -684,15 +696,45 @@ class PolySwarmAsyncAPI:
684
696
  logger.info('Delete ruleset %s', ruleset_id)
685
697
  return await self._single(resources.YaraRuleset.delete(self, id=ruleset_id, community=self.community))
686
698
 
687
- async def ruleset_list(self):
699
+ async def ruleset_list(self, name=None, status=None, favorites_only=None,
700
+ has_new_results=None):
688
701
  """
689
702
  List all YaraRulesets for the current account.
703
+
704
+ All filters are optional and conjunctive:
705
+ :param name: Case-insensitive substring match on the ruleset name.
706
+ :param status: 'active' returns only rulesets whose live hunt is
707
+ currently running.
708
+ :param favorites_only: True returns only favorited rulesets.
709
+ :param has_new_results: True returns only rulesets whose stored
710
+ new-results counter is positive. The counter (and its window) is
711
+ maintained server-side by a scheduled refresh; rows carry it as
712
+ ``new_results_count`` with ``new_results_counted_at`` marking when
713
+ it was last refreshed. There is no per-request window parameter.
690
714
  :return: A generator of YaraRuleset resources
691
715
  """
692
716
  logger.info('List rulesets')
693
- async for item in self._paginate(resources.YaraRuleset.list(self, community=self.community)):
717
+ async for item in self._paginate(resources.YaraRuleset.list(
718
+ self, name=name, status=status, favorites_only=favorites_only,
719
+ has_new_results=has_new_results, community=self.community)):
694
720
  yield item
695
721
 
722
+ async def ruleset_favorite(self, ruleset_id, favorite=True):
723
+ """
724
+ Favorite or unfavorite a YaraRuleset. Idempotent; works while a live
725
+ hunt is running. Favorites are shared by the whole team and capped
726
+ (the response carries favorites_used / favorites_limit); when the
727
+ budget is exhausted the server refuses with a machine-readable
728
+ FAVORITE_LIMIT error.
729
+
730
+ :param ruleset_id: Id of the ruleset
731
+ :param favorite: True to star, False to unstar
732
+ :return: A YaraRulesetFavorite resource
733
+ """
734
+ logger.info('%s ruleset %s', 'Favorite' if favorite else 'Unfavorite', ruleset_id)
735
+ return await self._single(resources.YaraRulesetFavorite.update(
736
+ self, id=ruleset_id, favorite=favorite, community=self.community))
737
+
696
738
  async def tag_link_get(self, sha256):
697
739
  """
698
740
  Fetch the Tags and Families associated with the given sha256.
@@ -20,7 +20,7 @@ import io
20
20
  import logging
21
21
 
22
22
  from polyswarm_api import exceptions, resources, settings
23
- from polyswarm_api.core import PolyswarmRequest
23
+ from polyswarm_api.core import PolyswarmRequest, _as_result_bound
24
24
 
25
25
  from .session import PolyswarmSession
26
26
 
@@ -568,18 +568,27 @@ class PolyswarmAPI:
568
568
  polyscore_lower=None,
569
569
  polyscore_upper=None,
570
570
  community=None,
571
+ livescan_id=None,
572
+ max_results=None,
571
573
  ):
572
574
  """
573
575
  Get live hunts feed
574
576
 
575
- :param since: Fetch results from the last "since" minutes
577
+ :param since: Window in SECONDS (this said "minutes" in earlier releases and was
578
+ wrong). Absent or 0 means no time filter at all.
576
579
  :param rule_name: Filter hunt results on the provided rule name (exact match).
577
580
  :param family: Filter hunt results based on the family name (exact match).
578
581
  :param polyscore_lower: Polyscore lower bound for the hunt results.
579
582
  :param polyscore_upper: Polyscore upper bound for the hunt results.
580
583
  :param community: Community to retrieve live results from, or public/private.
584
+ :param livescan_id: Scope the feed to one live hunt's results.
585
+ :param max_results: Total results to yield, not a page size — paging
586
+ continues in the server's own chunks until the total is reached.
587
+ None, 0 or a negative means no bound: every page, as before.
581
588
  :return: Generator of HuntResult resources
582
589
  """
590
+ bound = _as_result_bound(max_results)
591
+ yielded = 0
583
592
  for item in self._paginate(
584
593
  resources.LiveHuntResult.list(
585
594
  self,
@@ -588,10 +597,14 @@ class PolyswarmAPI:
588
597
  family=family,
589
598
  polyscore_lower=polyscore_lower,
590
599
  polyscore_upper=polyscore_upper,
600
+ livescan_id=livescan_id,
591
601
  community=community or self.community,
592
602
  )
593
603
  ):
594
604
  yield item
605
+ yielded += 1
606
+ if bound is not None and yielded >= bound:
607
+ return
595
608
 
596
609
  def live_feed_delete(self, result_ids):
597
610
  """
@@ -823,17 +836,58 @@ class PolyswarmAPI:
823
836
  resources.YaraRuleset.delete(self, id=ruleset_id, community=self.community)
824
837
  )
825
838
 
826
- def ruleset_list(self):
839
+ def ruleset_list(
840
+ self, name=None, status=None, favorites_only=None, has_new_results=None
841
+ ):
827
842
  """
828
843
  List all YaraRulesets for the current account.
844
+
845
+ All filters are optional and conjunctive:
846
+ :param name: Case-insensitive substring match on the ruleset name.
847
+ :param status: 'active' returns only rulesets whose live hunt is
848
+ currently running.
849
+ :param favorites_only: True returns only favorited rulesets.
850
+ :param has_new_results: True returns only rulesets whose stored
851
+ new-results counter is positive. The counter (and its window) is
852
+ maintained server-side by a scheduled refresh; rows carry it as
853
+ ``new_results_count`` with ``new_results_counted_at`` marking when
854
+ it was last refreshed. There is no per-request window parameter.
829
855
  :return: A generator of YaraRuleset resources
830
856
  """
831
857
  logger.info("List rulesets")
832
858
  for item in self._paginate(
833
- resources.YaraRuleset.list(self, community=self.community)
859
+ resources.YaraRuleset.list(
860
+ self,
861
+ name=name,
862
+ status=status,
863
+ favorites_only=favorites_only,
864
+ has_new_results=has_new_results,
865
+ community=self.community,
866
+ )
834
867
  ):
835
868
  yield item
836
869
 
870
+ def ruleset_favorite(self, ruleset_id, favorite=True):
871
+ """
872
+ Favorite or unfavorite a YaraRuleset. Idempotent; works while a live
873
+ hunt is running. Favorites are shared by the whole team and capped
874
+ (the response carries favorites_used / favorites_limit); when the
875
+ budget is exhausted the server refuses with a machine-readable
876
+ FAVORITE_LIMIT error.
877
+
878
+ :param ruleset_id: Id of the ruleset
879
+ :param favorite: True to star, False to unstar
880
+ :return: A YaraRulesetFavorite resource
881
+ """
882
+ logger.info(
883
+ "%s ruleset %s", "Favorite" if favorite else "Unfavorite", ruleset_id
884
+ )
885
+ return self._single(
886
+ resources.YaraRulesetFavorite.update(
887
+ self, id=ruleset_id, favorite=favorite, community=self.community
888
+ )
889
+ )
890
+
837
891
  def tag_link_get(self, sha256):
838
892
  """
839
893
  Fetch the Tags and Families associated with the given sha256.
@@ -16,7 +16,8 @@ parsing logic have a single source of truth:
16
16
  builders (``create``, ``get``, ``head``, ``update``, ``delete``,
17
17
  ``list``) construct ``PolyswarmRequest`` descriptors.
18
18
  - ``Hashable`` + ``is_valid_*`` validators, ``parse_isoformat``,
19
- ``_normalise_bool_params``, ``RequestParamsEncoder``.
19
+ ``_normalise_bool_params``, ``RequestParamsEncoder``,
20
+ ``_as_result_bound``.
20
21
 
21
22
  Both transports import from here.
22
23
  """
@@ -668,6 +669,16 @@ class BaseJsonResource(BaseResource):
668
669
  )
669
670
 
670
671
 
672
+ def _as_result_bound(max_results):
673
+ """The caller's result bound, or None when there isn't one.
674
+
675
+ None, 0 and negatives all mean "no bound".
676
+ """
677
+ if not max_results or max_results < 0:
678
+ return None
679
+ return max_results
680
+
681
+
671
682
  # ── Hash helpers ───────────────────────────────────────────────────
672
683
 
673
684
 
@@ -746,12 +746,51 @@ class YaraRuleset(core.BaseJsonResource):
746
746
  self.modified = core.parse_isoformat(content.get('modified'))
747
747
  self.deleted = content.get('deleted')
748
748
  self.yara = content.get('yara')
749
+ # Hunt-page tracking fields. All additive — an older server simply
750
+ # leaves them None.
751
+ self.favorite = content.get('favorite')
752
+ self.favorited_at = core.parse_isoformat(content.get('favorited_at'))
753
+ # Number of rules in the ruleset body. None means "no answer" (the
754
+ # server could not or has not counted), which is different from 0.
755
+ self.rule_count = content.get('rule_count')
756
+ self.historical_hunt_count = content.get('historical_hunt_count')
757
+ # Live results collected in the product window — a STORED counter the
758
+ # server refreshes on a schedule, always present on list rows with a
759
+ # running live hunt. None means "not yet refreshed / no live hunt",
760
+ # which is different from 0 ("refreshed, nothing new").
761
+ self.new_results_count = content.get('new_results_count')
762
+ # When the server last refreshed the counter — the staleness marker
763
+ # that makes a stored count trustworthy (None iff the count is None).
764
+ self.new_results_counted_at = core.parse_isoformat(
765
+ content.get('new_results_counted_at'))
749
766
 
750
767
 
751
768
  class LiveYaraRuleset(YaraRuleset):
752
769
  RESOURCE_ENDPOINT = '/hunt/rule/live'
753
770
 
754
771
 
772
+ class YaraRulesetFavorite(core.BaseJsonResource):
773
+ """The favorite-toggle response: the ruleset's new star state plus the
774
+ team's budget usage, so callers can render "N of M used" without counting
775
+ client-side."""
776
+ RESOURCE_ENDPOINT = '/hunt/rule/favorite'
777
+ # The toggle's payload ({id, favorite}) rides the JSON body — the server
778
+ # reads BOTH from the body — so the default RESOURCE_ID_KEYS (['id'])
779
+ # would move `id` into the query string and the server would 400 with
780
+ # "A valid rule id must be provided". `community` stays a query-string
781
+ # key to match where every ruleset GET/list sends it (the server accepts
782
+ # it from either the query or the body, but never from both at once).
783
+ RESOURCE_ID_KEYS = ['community']
784
+
785
+ def __init__(self, content, api=None):
786
+ super().__init__(content, api=api)
787
+ self.id = content.get('id')
788
+ self.favorite = content.get('favorite')
789
+ self.favorited_at = core.parse_isoformat(content.get('favorited_at'))
790
+ self.favorites_used = content.get('favorites_used')
791
+ self.favorites_limit = content.get('favorites_limit')
792
+
793
+
755
794
  class LiveHuntResult(core.BaseJsonResource):
756
795
  RESOURCE_ENDPOINT = '/hunt/live'
757
796
 
@@ -794,6 +833,16 @@ class HistoricalHunt(core.BaseJsonResource):
794
833
  self.progress = content['progress']
795
834
  self.results_csv_uri = content['results_csv_uri']
796
835
  self.communities = content.get('communities')
836
+ # Source-rule provenance. rule_id is the ruleset this hunt was
837
+ # triggered from (None for raw-yara hunts and hunts predating the
838
+ # tracking); rule_modified is a freeze-time audit timestamp.
839
+ self.rule_id = content.get('rule_id')
840
+ self.rule_modified = core.parse_isoformat(content.get('rule_modified'))
841
+ # Tri-state: has the source ruleset's body changed SINCE THE HUNT
842
+ # FROZE IT? True/False when the server could compare; None means
843
+ # unknown (no source rule, or nothing to compare against) — not
844
+ # "unchanged".
845
+ self.source_rule_changed = content.get('source_rule_changed')
797
846
 
798
847
 
799
848
  class HistoricalHuntList(HistoricalHunt):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyswarm_api
3
- Version: 4.3.0
3
+ Version: 4.4.0
4
4
  Summary: Client library to simplify interacting with the PolySwarm consumer API
5
5
  Author-email: PolySwarm Developers <info@polyswarm.io>
6
6
  License: MIT
File without changes
File without changes
File without changes