From 5b5a31985526e3d87a125fe917a6a67208e317b1 Mon Sep 17 00:00:00 2001 From: Ian Later Date: Wed, 10 Sep 2025 18:23:43 -0700 Subject: [PATCH 01/12] python(feat): Add reports and tags types and clients + clean up rules as a result. --- .../rule_evaluation/v1/rule_evaluation.proto | 10 - protos/sift/rules/v1/rules.proto | 125 ++++++- .../rule_evaluation/v1/rule_evaluation_pb2.py | 18 +- .../v1/rule_evaluation_pb2.pyi | 39 +- python/lib/sift/rules/v1/rules_pb2.py | 338 ++++++++++------- python/lib/sift/rules/v1/rules_pb2.pyi | 160 +++++++- python/lib/sift/rules/v1/rules_pb2_grpc.py | 148 +++++++- python/lib/sift/rules/v1/rules_pb2_grpc.pyi | 128 ++++++- .../_internal/low_level_wrappers/__init__.py | 2 + .../_internal/low_level_wrappers/reports.py | 179 +++++++++ .../_internal/low_level_wrappers/rules.py | 107 +++++- .../_internal/low_level_wrappers/tags.py | 117 ++++++ .../sift_client/_tests/integrated/reports.py | 149 ++++++++ .../sift_client/_tests/integrated/rules.py | 23 +- .../lib/sift_client/_tests/integrated/runs.py | 48 +-- python/lib/sift_client/client.py | 15 +- python/lib/sift_client/resources/__init__.py | 10 + python/lib/sift_client/resources/reports.py | 310 +++++++++++++++ python/lib/sift_client/resources/rules.py | 73 +++- python/lib/sift_client/resources/runs.py | 61 ++- .../resources/sync_stubs/__init__.py | 6 +- .../resources/sync_stubs/__init__.pyi | 352 ++++++++++++++++-- python/lib/sift_client/resources/tags.py | 144 +++++++ python/lib/sift_client/sift_types/__init__.py | 8 + python/lib/sift_client/sift_types/report.py | 170 +++++++++ python/lib/sift_client/sift_types/rule.py | 13 +- python/lib/sift_client/sift_types/tag.py | 56 +++ python/lib/sift_client/util/cel_utils.py | 4 +- python/lib/sift_client/util/util.py | 15 +- 29 files changed, 2521 insertions(+), 307 deletions(-) create mode 100644 python/lib/sift_client/_internal/low_level_wrappers/reports.py create mode 100644 python/lib/sift_client/_internal/low_level_wrappers/tags.py create mode 100644 python/lib/sift_client/_tests/integrated/reports.py create mode 100644 python/lib/sift_client/resources/reports.py create mode 100644 python/lib/sift_client/resources/tags.py create mode 100644 python/lib/sift_client/sift_types/report.py create mode 100644 python/lib/sift_client/sift_types/tag.py diff --git a/protos/sift/rule_evaluation/v1/rule_evaluation.proto b/protos/sift/rule_evaluation/v1/rule_evaluation.proto index 413f67121..e426dedd6 100644 --- a/protos/sift/rule_evaluation/v1/rule_evaluation.proto +++ b/protos/sift/rule_evaluation/v1/rule_evaluation.proto @@ -130,18 +130,8 @@ message EvaluateRulesFromRuleConfigs { repeated sift.rules.v1.UpdateRuleRequest configs = 1 [(google.api.field_behavior) = REQUIRED]; } -message RulePreviewOutput { - string rule_name = 1 [(google.api.field_behavior) = REQUIRED]; - string rule_id = 2; - string rule_version_id = 3; - string asset_id = 4 [(google.api.field_behavior) = REQUIRED]; - int32 exit_code = 5 [(google.api.field_behavior) = REQUIRED]; - string stdout = 6; - string stderr = 7; -} message EvaluateRulesPreviewResponse { int32 created_annotation_count = 1 [(google.api.field_behavior) = REQUIRED]; repeated sift.rules.v1.DryRunAnnotation dry_run_annotations = 2; - repeated RulePreviewOutput rule_outputs = 3; } diff --git a/protos/sift/rules/v1/rules.proto b/protos/sift/rules/v1/rules.proto index 6a97ec44f..13012a91f 100644 --- a/protos/sift/rules/v1/rules.proto +++ b/protos/sift/rules/v1/rules.proto @@ -101,51 +101,107 @@ service RuleService { }; } - // Deletes a rule + // Deprecated - use ArchiveRule instead. + // Archives a rule. rpc DeleteRule(DeleteRuleRequest) returns (DeleteRuleResponse) { + option deprecated = true; option (google.api.http) = { post: "/api/v1/rules/delete" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "DeleteRule" - description: "Deletes a rule." + description: "Archives a rule. Deprecated: Use ArchiveRule instead." + }; + } + + // Archives a rule. + rpc ArchiveRule(ArchiveRuleRequest) returns (ArchiveRuleResponse) { + option (google.api.http) = { + post: "/api/v1/rules/archive" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "ArchiveRule" + description: "Archives a rule." }; } - // Deletes multiple rules + // Deprecated - use BatchArchiveRules instead. + // Archives multiple rules. rpc BatchDeleteRules(BatchDeleteRulesRequest) returns (BatchDeleteRulesResponse) { + option deprecated = true; option (google.api.http) = { post: "/api/v1/rules/batchDelete" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "BatchDeleteRules" - description: "Deletes multiple rules." + description: "Archives multiple rules. Deprecated: Use BatchArchiveRules instead." }; } - // Undeletes a rule + // Batch archives rules. + rpc BatchArchiveRules(BatchArchiveRulesRequest) returns (BatchArchiveRulesResponse) { + option (google.api.http) = { + post: "/api/v1/rules/batchArchive" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "BatchArchiveRules" + description: "Batch archives rules." + }; + } + + // Unarchives a rule. + rpc UnarchiveRule(UnarchiveRuleRequest) returns (UnarchiveRuleResponse) { + option (google.api.http) = { + post: "/api/v1/rules/unarchive" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "UnarchiveRule" + description: "Unarchives a rule." + }; + } + + // Batch unarchives rules. + rpc BatchUnarchiveRules(BatchUnarchiveRulesRequest) returns (BatchUnarchiveRulesResponse) { + option (google.api.http) = { + post: "/api/v1/rules/batchUnarchive" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "BatchUnarchiveRules" + description: "Batch unarchives rules." + }; + } + + // Deprecated - use UnarchiveRule instead. + // Unarchives a rule rpc UndeleteRule(UndeleteRuleRequest) returns (UndeleteRuleResponse) { + option deprecated = true; option (google.api.http) = { post: "/api/v1/rules/undelete" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "UndeleteRule" - description: "Undeletes a rule." + description: "Deprecated: Use UnarchiveRule instead." }; } - // Undeletes multiple rules + // Deprecated - use BatchUnarchiveRules instead. + // Unarchives multiple rules rpc BatchUndeleteRules(BatchUndeleteRulesRequest) returns (BatchUndeleteRulesResponse) { + option deprecated = true; option (google.api.http) = { post: "/api/v1/rules/batchUndelete" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "BatchUndeleteRules" - description: "Undeletes multiple rules." + description: "Deprecated: Use BatchUnarchiveRules instead." }; } @@ -284,9 +340,18 @@ message Rule { RuleAssetConfiguration asset_configuration = 15 [(google.api.field_behavior) = REQUIRED]; ContextualChannels contextual_channels = 16 [(google.api.field_behavior) = REQUIRED]; - google.protobuf.Timestamp deleted_date = 17 [(google.api.field_behavior) = OPTIONAL]; + google.protobuf.Timestamp deleted_date = 17 [ + (google.api.field_behavior) = OPTIONAL, + deprecated = true + ]; bool is_external = 18 [(google.api.field_behavior) = REQUIRED]; repeated sift.metadata.v1.MetadataValue metadata = 19 [(google.api.field_behavior) = REQUIRED]; + + // archived_date indicates when the rule was archived + google.protobuf.Timestamp archived_date = 20 [(google.api.field_behavior) = OPTIONAL]; + + // is_archived is inferred from when archived_date is not null + bool is_archived = 21 [(google.api.field_behavior) = REQUIRED]; } message RuleCondition { // 4 is reserved from old "status" column that lived directly on rule condition @@ -410,6 +475,7 @@ message UpdateRuleRequest { ContextualChannels contextual_channels = 11; bool is_external = 12; repeated sift.metadata.v1.MetadataValue metadata = 13 [(google.api.field_behavior) = REQUIRED]; + bool is_archived = 14 [(google.api.field_behavior) = OPTIONAL]; } message UpdateConditionRequest { // 2 is reserved from old status update support on rule condition directly @@ -496,6 +562,20 @@ message BatchDeleteRulesRequest { } message BatchDeleteRulesResponse {} +// ArchiveRuleRequest is used to archive a rule by rule_id or client_key. If both are provided, only rule_id will be used. +message ArchiveRuleRequest { + string rule_id = 1 [(google.api.field_behavior) = OPTIONAL]; + string client_key = 2 [(google.api.field_behavior) = OPTIONAL]; +} +message ArchiveRuleResponse {} + +// BatchArchiveRulesRequest is used to archive a rule by rule_id or client_key. For each rule if both are provided, only rule_id will be used. +message BatchArchiveRulesRequest { + repeated string rule_ids = 1 [(google.api.field_behavior) = OPTIONAL]; + repeated string client_keys = 2 [(google.api.field_behavior) = OPTIONAL]; +} +message BatchArchiveRulesResponse {} + // UndeleteRuleRequest is used to undelete a rule by rule_id or client_key. If both are provided, only rule_id will be used. message UndeleteRuleRequest { string rule_id = 1 [(google.api.field_behavior) = OPTIONAL]; @@ -510,6 +590,20 @@ message BatchUndeleteRulesRequest { } message BatchUndeleteRulesResponse {} +// UnarchiveRuleRequest is used to unarchive a rule by rule_id or client_key. If both are provided, only rule_id will be used. +message UnarchiveRuleRequest { + string rule_id = 1 [(google.api.field_behavior) = OPTIONAL]; + string client_key = 2 [(google.api.field_behavior) = OPTIONAL]; +} +message UnarchiveRuleResponse {} + +// BatchUnarchiveRulesRequest is used to unarchive a rule by rule_id or client_key. For each rule if both are provided, only rule_id will be used. +message BatchUnarchiveRulesRequest { + repeated string rule_ids = 1 [(google.api.field_behavior) = OPTIONAL]; + repeated string client_keys = 2 [(google.api.field_behavior) = OPTIONAL]; +} +message BatchUnarchiveRulesResponse {} + // Deprecated - use ViewJsonRulesRequest. message ViewHumanFriendlyRulesRequest { option deprecated = true; @@ -591,7 +685,8 @@ message ListRulesRequest { string page_token = 2 [(google.api.field_behavior) = OPTIONAL]; // A [Common Expression Language (CEL)](https://github.com/google/cel-spec) filter string. - // Available fields to filter by are `rule_id`, `client_key`, `name`, `description`, `asset_id`, `tag_id`, and `deleted_date`. + // Available fields to filter by are `rule_id`, `client_key`, `name`, `description`, `is_external`, `asset_id`, `tag_id`, + // `created_date`, `created_by_user_id`, `modified_date`, `modified_by_user_id`, `deleted_date`, `is_archived`, and `archived_date`. // For further information about how to use CELs, please refer to [this guide](https://github.com/google/cel-spec/blob/master/doc/langdef.md#standard-definitions). // Optional. string filter = 3 [(google.api.field_behavior) = OPTIONAL]; @@ -636,7 +731,15 @@ message RuleVersion { string created_by_user_id = 5 [(google.api.field_behavior) = REQUIRED]; string version_notes = 6 [(google.api.field_behavior) = REQUIRED]; string generated_change_message = 7 [(google.api.field_behavior) = REQUIRED]; - google.protobuf.Timestamp deleted_date = 8 [(google.api.field_behavior) = OPTIONAL]; + google.protobuf.Timestamp deleted_date = 8 [ + (google.api.field_behavior) = OPTIONAL, + deprecated = true + ]; + // archived_date indicates when the rule version was archived + google.protobuf.Timestamp archived_date = 9 [(google.api.field_behavior) = OPTIONAL]; + + // is_archived is inferred from when archived_date is not null + bool is_archived = 10 [(google.api.field_behavior) = REQUIRED]; } message ListRuleVersionsResponse { diff --git a/python/lib/sift/rule_evaluation/v1/rule_evaluation_pb2.py b/python/lib/sift/rule_evaluation/v1/rule_evaluation_pb2.py index eb347e47a..13694f06d 100644 --- a/python/lib/sift/rule_evaluation/v1/rule_evaluation_pb2.py +++ b/python/lib/sift/rule_evaluation/v1/rule_evaluation_pb2.py @@ -20,7 +20,7 @@ from sift.rules.v1 import rules_pb2 as sift_dot_rules_dot_v1_dot_rules__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-sift/rule_evaluation/v1/rule_evaluation.proto\x12\x17sift.rule_evaluation.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\x1a-sift/common/type/v1/resource_identifier.proto\x1a\x19sift/rules/v1/rules.proto\"\x96\x06\n\x14\x45valuateRulesRequest\x12;\n\x03run\x18\x01 \x01(\x0b\x32\'.sift.common.type.v1.ResourceIdentifierH\x00R\x03run\x12\x42\n\x06\x61ssets\x18\x02 \x01(\x0b\x32(.sift.rule_evaluation.v1.AssetsTimeRangeH\x00R\x06\x61ssets\x12M\n\x0erun_time_range\x18\t \x01(\x0b\x32%.sift.rule_evaluation.v1.RunTimeRangeH\x00R\x0crunTimeRange\x12U\n\x05rules\x18\x03 \x01(\x0b\x32=.sift.rule_evaluation.v1.EvaluateRulesFromCurrentRuleVersionsH\x01R\x05rules\x12]\n\rrule_versions\x18\x04 \x01(\x0b\x32\x36.sift.rule_evaluation.v1.EvaluateRulesFromRuleVersionsH\x01R\x0cruleVersions\x12\x63\n\x0freport_template\x18\x05 \x01(\x0b\x32\x38.sift.rule_evaluation.v1.EvaluateRulesFromReportTemplateH\x01R\x0ereportTemplate\x12\x32\n\x14\x61ll_applicable_rules\x18\n \x01(\x08H\x01R\x12\x61llApplicableRules\x12\x66\n\x12\x61nnotation_options\x18\x06 \x01(\x0b\x32\x37.sift.rule_evaluation.v1.EvaluateRulesAnnotationOptionsR\x11\x61nnotationOptions\x12,\n\x0forganization_id\x18\x07 \x01(\tB\x03\xe0\x41\x01R\x0eorganizationId\x12)\n\x0breport_name\x18\x08 \x01(\tB\x03\xe0\x41\x01H\x02R\nreportName\x88\x01\x01\x42\x06\n\x04timeB\x06\n\x04modeB\x0e\n\x0c_report_name\"\xeb\x01\n\x0cRunTimeRange\x12\x39\n\x03run\x18\x01 \x01(\x0b\x32\'.sift.common.type.v1.ResourceIdentifierR\x03run\x12\x43\n\nstart_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x01H\x00R\tstartTime\x88\x01\x01\x12?\n\x08\x65nd_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x01H\x01R\x07\x65ndTime\x88\x01\x01\x42\r\n\x0b_start_timeB\x0b\n\t_end_time\"\xcf\x01\n\x0f\x41ssetsTimeRange\x12@\n\x06\x61ssets\x18\x01 \x01(\x0b\x32#.sift.common.type.v1.NamedResourcesB\x03\xe0\x41\x02R\x06\x61ssets\x12>\n\nstart_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\tstartTime\x12:\n\x08\x65nd_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x07\x65ndTime\"k\n$EvaluateRulesFromCurrentRuleVersions\x12\x43\n\x05rules\x18\x01 \x01(\x0b\x32(.sift.common.type.v1.ResourceIdentifiersB\x03\xe0\x41\x02R\x05rules\"x\n\x1f\x45valuateRulesFromReportTemplate\x12U\n\x0freport_template\x18\x01 \x01(\x0b\x32\'.sift.common.type.v1.ResourceIdentifierB\x03\xe0\x41\x02R\x0ereportTemplate\"N\n\x1d\x45valuateRulesFromRuleVersions\x12-\n\x10rule_version_ids\x18\x01 \x03(\tB\x03\xe0\x41\x02R\x0eruleVersionIds\"^\n\x1e\x45valuateRulesAnnotationOptions\x12<\n\x04tags\x18\x01 \x01(\x0b\x32#.sift.common.type.v1.NamedResourcesB\x03\xe0\x41\x02R\x04tags\"\xb7\x01\n\x15\x45valuateRulesResponse\x12=\n\x18\x63reated_annotation_count\x18\x01 \x01(\x05\x42\x03\xe0\x41\x02R\x16\x63reatedAnnotationCount\x12%\n\treport_id\x18\x02 \x01(\tB\x03\xe0\x41\x01H\x00R\x08reportId\x88\x01\x01\x12\x1f\n\x06job_id\x18\x03 \x01(\tB\x03\xe0\x41\x01H\x01R\x05jobId\x88\x01\x01\x42\x0c\n\n_report_idB\t\n\x07_job_id\"\xde\x04\n\x1b\x45valuateRulesPreviewRequest\x12;\n\x03run\x18\x01 \x01(\x0b\x32\'.sift.common.type.v1.ResourceIdentifierH\x00R\x03run\x12M\n\x0erun_time_range\x18\x08 \x01(\x0b\x32%.sift.rule_evaluation.v1.RunTimeRangeH\x00R\x0crunTimeRange\x12U\n\x05rules\x18\x03 \x01(\x0b\x32=.sift.rule_evaluation.v1.EvaluateRulesFromCurrentRuleVersionsH\x01R\x05rules\x12]\n\rrule_versions\x18\x04 \x01(\x0b\x32\x36.sift.rule_evaluation.v1.EvaluateRulesFromRuleVersionsH\x01R\x0cruleVersions\x12\x63\n\x0freport_template\x18\x05 \x01(\x0b\x32\x38.sift.rule_evaluation.v1.EvaluateRulesFromReportTemplateH\x01R\x0ereportTemplate\x12Z\n\x0crule_configs\x18\x06 \x01(\x0b\x32\x35.sift.rule_evaluation.v1.EvaluateRulesFromRuleConfigsH\x01R\x0bruleConfigs\x12,\n\x0forganization_id\x18\x07 \x01(\tB\x03\xe0\x41\x01R\x0eorganizationIdB\x06\n\x04timeB\x06\n\x04mode\"_\n\x1c\x45valuateRulesFromRuleConfigs\x12?\n\x07\x63onfigs\x18\x01 \x03(\x0b\x32 .sift.rules.v1.UpdateRuleRequestB\x03\xe0\x41\x02R\x07\x63onfigs\"\xe8\x01\n\x11RulePreviewOutput\x12 \n\trule_name\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x08ruleName\x12\x17\n\x07rule_id\x18\x02 \x01(\tR\x06ruleId\x12&\n\x0frule_version_id\x18\x03 \x01(\tR\rruleVersionId\x12\x1e\n\x08\x61sset_id\x18\x04 \x01(\tB\x03\xe0\x41\x02R\x07\x61ssetId\x12 \n\texit_code\x18\x05 \x01(\x05\x42\x03\xe0\x41\x02R\x08\x65xitCode\x12\x16\n\x06stdout\x18\x06 \x01(\tR\x06stdout\x12\x16\n\x06stderr\x18\x07 \x01(\tR\x06stderr\"\xfd\x01\n\x1c\x45valuateRulesPreviewResponse\x12=\n\x18\x63reated_annotation_count\x18\x01 \x01(\x05\x42\x03\xe0\x41\x02R\x16\x63reatedAnnotationCount\x12O\n\x13\x64ry_run_annotations\x18\x02 \x03(\x0b\x32\x1f.sift.rules.v1.DryRunAnnotationR\x11\x64ryRunAnnotations\x12M\n\x0crule_outputs\x18\x03 \x03(\x0b\x32*.sift.rule_evaluation.v1.RulePreviewOutputR\x0bruleOutputs2\xd8\x06\n\x15RuleEvaluationService\x12\xc3\x02\n\rEvaluateRules\x12-.sift.rule_evaluation.v1.EvaluateRulesRequest\x1a..sift.rule_evaluation.v1.EvaluateRulesResponse\"\xd2\x01\x92\x41\xa7\x01\x12\rEvaluateRules\x1a\x95\x01\x45valuate rules from a designated source against a run or asset and return the total amount of annotations created and the ID of the generated report.\x82\xd3\xe4\x93\x02!\"\x1c/api/v1/rules/evaluate-rules:\x01*\x12\xda\x02\n\x14\x45valuateRulesPreview\x12\x34.sift.rule_evaluation.v1.EvaluateRulesPreviewRequest\x1a\x35.sift.rule_evaluation.v1.EvaluateRulesPreviewResponse\"\xd4\x01\x92\x41\xa1\x01\x12\x14\x45valuateRulesPreview\x1a\x88\x01Perform a dry run evaluation for existing rules or rule configurations against a run and return the annotations that would be generated.\x82\xd3\xe4\x93\x02)\"$/api/v1/rules/evaluate-rules:preview:\x01*\x1a\x9b\x01\x92\x41\x97\x01\x12\x1aService to evaluate rules.\x1ay\n\x1fRead more about what rules are.\x12Vhttps://customer.support.siftstack.com/servicedesk/customer/portal/2/article/265421102B\xca\x01\n\x1b\x63om.sift.rule_evaluation.v1B\x13RuleEvaluationProtoP\x01\xa2\x02\x03SRX\xaa\x02\x16Sift.RuleEvaluation.V1\xca\x02\x16Sift\\RuleEvaluation\\V1\xe2\x02\"Sift\\RuleEvaluation\\V1\\GPBMetadata\xea\x02\x18Sift::RuleEvaluation::V1\x92\x41\x1b\x12\x19\n\x17Rule Evaluation Serviceb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-sift/rule_evaluation/v1/rule_evaluation.proto\x12\x17sift.rule_evaluation.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\x1a-sift/common/type/v1/resource_identifier.proto\x1a\x19sift/rules/v1/rules.proto\"\x96\x06\n\x14\x45valuateRulesRequest\x12;\n\x03run\x18\x01 \x01(\x0b\x32\'.sift.common.type.v1.ResourceIdentifierH\x00R\x03run\x12\x42\n\x06\x61ssets\x18\x02 \x01(\x0b\x32(.sift.rule_evaluation.v1.AssetsTimeRangeH\x00R\x06\x61ssets\x12M\n\x0erun_time_range\x18\t \x01(\x0b\x32%.sift.rule_evaluation.v1.RunTimeRangeH\x00R\x0crunTimeRange\x12U\n\x05rules\x18\x03 \x01(\x0b\x32=.sift.rule_evaluation.v1.EvaluateRulesFromCurrentRuleVersionsH\x01R\x05rules\x12]\n\rrule_versions\x18\x04 \x01(\x0b\x32\x36.sift.rule_evaluation.v1.EvaluateRulesFromRuleVersionsH\x01R\x0cruleVersions\x12\x63\n\x0freport_template\x18\x05 \x01(\x0b\x32\x38.sift.rule_evaluation.v1.EvaluateRulesFromReportTemplateH\x01R\x0ereportTemplate\x12\x32\n\x14\x61ll_applicable_rules\x18\n \x01(\x08H\x01R\x12\x61llApplicableRules\x12\x66\n\x12\x61nnotation_options\x18\x06 \x01(\x0b\x32\x37.sift.rule_evaluation.v1.EvaluateRulesAnnotationOptionsR\x11\x61nnotationOptions\x12,\n\x0forganization_id\x18\x07 \x01(\tB\x03\xe0\x41\x01R\x0eorganizationId\x12)\n\x0breport_name\x18\x08 \x01(\tB\x03\xe0\x41\x01H\x02R\nreportName\x88\x01\x01\x42\x06\n\x04timeB\x06\n\x04modeB\x0e\n\x0c_report_name\"\xeb\x01\n\x0cRunTimeRange\x12\x39\n\x03run\x18\x01 \x01(\x0b\x32\'.sift.common.type.v1.ResourceIdentifierR\x03run\x12\x43\n\nstart_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x01H\x00R\tstartTime\x88\x01\x01\x12?\n\x08\x65nd_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x01H\x01R\x07\x65ndTime\x88\x01\x01\x42\r\n\x0b_start_timeB\x0b\n\t_end_time\"\xcf\x01\n\x0f\x41ssetsTimeRange\x12@\n\x06\x61ssets\x18\x01 \x01(\x0b\x32#.sift.common.type.v1.NamedResourcesB\x03\xe0\x41\x02R\x06\x61ssets\x12>\n\nstart_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\tstartTime\x12:\n\x08\x65nd_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x07\x65ndTime\"k\n$EvaluateRulesFromCurrentRuleVersions\x12\x43\n\x05rules\x18\x01 \x01(\x0b\x32(.sift.common.type.v1.ResourceIdentifiersB\x03\xe0\x41\x02R\x05rules\"x\n\x1f\x45valuateRulesFromReportTemplate\x12U\n\x0freport_template\x18\x01 \x01(\x0b\x32\'.sift.common.type.v1.ResourceIdentifierB\x03\xe0\x41\x02R\x0ereportTemplate\"N\n\x1d\x45valuateRulesFromRuleVersions\x12-\n\x10rule_version_ids\x18\x01 \x03(\tB\x03\xe0\x41\x02R\x0eruleVersionIds\"^\n\x1e\x45valuateRulesAnnotationOptions\x12<\n\x04tags\x18\x01 \x01(\x0b\x32#.sift.common.type.v1.NamedResourcesB\x03\xe0\x41\x02R\x04tags\"\xb7\x01\n\x15\x45valuateRulesResponse\x12=\n\x18\x63reated_annotation_count\x18\x01 \x01(\x05\x42\x03\xe0\x41\x02R\x16\x63reatedAnnotationCount\x12%\n\treport_id\x18\x02 \x01(\tB\x03\xe0\x41\x01H\x00R\x08reportId\x88\x01\x01\x12\x1f\n\x06job_id\x18\x03 \x01(\tB\x03\xe0\x41\x01H\x01R\x05jobId\x88\x01\x01\x42\x0c\n\n_report_idB\t\n\x07_job_id\"\xde\x04\n\x1b\x45valuateRulesPreviewRequest\x12;\n\x03run\x18\x01 \x01(\x0b\x32\'.sift.common.type.v1.ResourceIdentifierH\x00R\x03run\x12M\n\x0erun_time_range\x18\x08 \x01(\x0b\x32%.sift.rule_evaluation.v1.RunTimeRangeH\x00R\x0crunTimeRange\x12U\n\x05rules\x18\x03 \x01(\x0b\x32=.sift.rule_evaluation.v1.EvaluateRulesFromCurrentRuleVersionsH\x01R\x05rules\x12]\n\rrule_versions\x18\x04 \x01(\x0b\x32\x36.sift.rule_evaluation.v1.EvaluateRulesFromRuleVersionsH\x01R\x0cruleVersions\x12\x63\n\x0freport_template\x18\x05 \x01(\x0b\x32\x38.sift.rule_evaluation.v1.EvaluateRulesFromReportTemplateH\x01R\x0ereportTemplate\x12Z\n\x0crule_configs\x18\x06 \x01(\x0b\x32\x35.sift.rule_evaluation.v1.EvaluateRulesFromRuleConfigsH\x01R\x0bruleConfigs\x12,\n\x0forganization_id\x18\x07 \x01(\tB\x03\xe0\x41\x01R\x0eorganizationIdB\x06\n\x04timeB\x06\n\x04mode\"_\n\x1c\x45valuateRulesFromRuleConfigs\x12?\n\x07\x63onfigs\x18\x01 \x03(\x0b\x32 .sift.rules.v1.UpdateRuleRequestB\x03\xe0\x41\x02R\x07\x63onfigs\"\xae\x01\n\x1c\x45valuateRulesPreviewResponse\x12=\n\x18\x63reated_annotation_count\x18\x01 \x01(\x05\x42\x03\xe0\x41\x02R\x16\x63reatedAnnotationCount\x12O\n\x13\x64ry_run_annotations\x18\x02 \x03(\x0b\x32\x1f.sift.rules.v1.DryRunAnnotationR\x11\x64ryRunAnnotations2\xd8\x06\n\x15RuleEvaluationService\x12\xc3\x02\n\rEvaluateRules\x12-.sift.rule_evaluation.v1.EvaluateRulesRequest\x1a..sift.rule_evaluation.v1.EvaluateRulesResponse\"\xd2\x01\x92\x41\xa7\x01\x12\rEvaluateRules\x1a\x95\x01\x45valuate rules from a designated source against a run or asset and return the total amount of annotations created and the ID of the generated report.\x82\xd3\xe4\x93\x02!\"\x1c/api/v1/rules/evaluate-rules:\x01*\x12\xda\x02\n\x14\x45valuateRulesPreview\x12\x34.sift.rule_evaluation.v1.EvaluateRulesPreviewRequest\x1a\x35.sift.rule_evaluation.v1.EvaluateRulesPreviewResponse\"\xd4\x01\x92\x41\xa1\x01\x12\x14\x45valuateRulesPreview\x1a\x88\x01Perform a dry run evaluation for existing rules or rule configurations against a run and return the annotations that would be generated.\x82\xd3\xe4\x93\x02)\"$/api/v1/rules/evaluate-rules:preview:\x01*\x1a\x9b\x01\x92\x41\x97\x01\x12\x1aService to evaluate rules.\x1ay\n\x1fRead more about what rules are.\x12Vhttps://customer.support.siftstack.com/servicedesk/customer/portal/2/article/265421102B\xca\x01\n\x1b\x63om.sift.rule_evaluation.v1B\x13RuleEvaluationProtoP\x01\xa2\x02\x03SRX\xaa\x02\x16Sift.RuleEvaluation.V1\xca\x02\x16Sift\\RuleEvaluation\\V1\xe2\x02\"Sift\\RuleEvaluation\\V1\\GPBMetadata\xea\x02\x18Sift::RuleEvaluation::V1\x92\x41\x1b\x12\x19\n\x17Rule Evaluation Serviceb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -60,12 +60,6 @@ _globals['_EVALUATERULESPREVIEWREQUEST'].fields_by_name['organization_id']._serialized_options = b'\340A\001' _globals['_EVALUATERULESFROMRULECONFIGS'].fields_by_name['configs']._loaded_options = None _globals['_EVALUATERULESFROMRULECONFIGS'].fields_by_name['configs']._serialized_options = b'\340A\002' - _globals['_RULEPREVIEWOUTPUT'].fields_by_name['rule_name']._loaded_options = None - _globals['_RULEPREVIEWOUTPUT'].fields_by_name['rule_name']._serialized_options = b'\340A\002' - _globals['_RULEPREVIEWOUTPUT'].fields_by_name['asset_id']._loaded_options = None - _globals['_RULEPREVIEWOUTPUT'].fields_by_name['asset_id']._serialized_options = b'\340A\002' - _globals['_RULEPREVIEWOUTPUT'].fields_by_name['exit_code']._loaded_options = None - _globals['_RULEPREVIEWOUTPUT'].fields_by_name['exit_code']._serialized_options = b'\340A\002' _globals['_EVALUATERULESPREVIEWRESPONSE'].fields_by_name['created_annotation_count']._loaded_options = None _globals['_EVALUATERULESPREVIEWRESPONSE'].fields_by_name['created_annotation_count']._serialized_options = b'\340A\002' _globals['_RULEEVALUATIONSERVICE']._loaded_options = None @@ -94,10 +88,8 @@ _globals['_EVALUATERULESPREVIEWREQUEST']._serialized_end=2733 _globals['_EVALUATERULESFROMRULECONFIGS']._serialized_start=2735 _globals['_EVALUATERULESFROMRULECONFIGS']._serialized_end=2830 - _globals['_RULEPREVIEWOUTPUT']._serialized_start=2833 - _globals['_RULEPREVIEWOUTPUT']._serialized_end=3065 - _globals['_EVALUATERULESPREVIEWRESPONSE']._serialized_start=3068 - _globals['_EVALUATERULESPREVIEWRESPONSE']._serialized_end=3321 - _globals['_RULEEVALUATIONSERVICE']._serialized_start=3324 - _globals['_RULEEVALUATIONSERVICE']._serialized_end=4180 + _globals['_EVALUATERULESPREVIEWRESPONSE']._serialized_start=2833 + _globals['_EVALUATERULESPREVIEWRESPONSE']._serialized_end=3007 + _globals['_RULEEVALUATIONSERVICE']._serialized_start=3010 + _globals['_RULEEVALUATIONSERVICE']._serialized_end=3866 # @@protoc_insertion_point(module_scope) diff --git a/python/lib/sift/rule_evaluation/v1/rule_evaluation_pb2.pyi b/python/lib/sift/rule_evaluation/v1/rule_evaluation_pb2.pyi index c349c3b81..234a76ead 100644 --- a/python/lib/sift/rule_evaluation/v1/rule_evaluation_pb2.pyi +++ b/python/lib/sift/rule_evaluation/v1/rule_evaluation_pb2.pyi @@ -283,58 +283,21 @@ class EvaluateRulesFromRuleConfigs(google.protobuf.message.Message): global___EvaluateRulesFromRuleConfigs = EvaluateRulesFromRuleConfigs -@typing.final -class RulePreviewOutput(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RULE_NAME_FIELD_NUMBER: builtins.int - RULE_ID_FIELD_NUMBER: builtins.int - RULE_VERSION_ID_FIELD_NUMBER: builtins.int - ASSET_ID_FIELD_NUMBER: builtins.int - EXIT_CODE_FIELD_NUMBER: builtins.int - STDOUT_FIELD_NUMBER: builtins.int - STDERR_FIELD_NUMBER: builtins.int - rule_name: builtins.str - rule_id: builtins.str - rule_version_id: builtins.str - asset_id: builtins.str - exit_code: builtins.int - stdout: builtins.str - stderr: builtins.str - def __init__( - self, - *, - rule_name: builtins.str = ..., - rule_id: builtins.str = ..., - rule_version_id: builtins.str = ..., - asset_id: builtins.str = ..., - exit_code: builtins.int = ..., - stdout: builtins.str = ..., - stderr: builtins.str = ..., - ) -> None: ... - def ClearField(self, field_name: typing.Literal["asset_id", b"asset_id", "exit_code", b"exit_code", "rule_id", b"rule_id", "rule_name", b"rule_name", "rule_version_id", b"rule_version_id", "stderr", b"stderr", "stdout", b"stdout"]) -> None: ... - -global___RulePreviewOutput = RulePreviewOutput - @typing.final class EvaluateRulesPreviewResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor CREATED_ANNOTATION_COUNT_FIELD_NUMBER: builtins.int DRY_RUN_ANNOTATIONS_FIELD_NUMBER: builtins.int - RULE_OUTPUTS_FIELD_NUMBER: builtins.int created_annotation_count: builtins.int @property def dry_run_annotations(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[sift.rules.v1.rules_pb2.DryRunAnnotation]: ... - @property - def rule_outputs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___RulePreviewOutput]: ... def __init__( self, *, created_annotation_count: builtins.int = ..., dry_run_annotations: collections.abc.Iterable[sift.rules.v1.rules_pb2.DryRunAnnotation] | None = ..., - rule_outputs: collections.abc.Iterable[global___RulePreviewOutput] | None = ..., ) -> None: ... - def ClearField(self, field_name: typing.Literal["created_annotation_count", b"created_annotation_count", "dry_run_annotations", b"dry_run_annotations", "rule_outputs", b"rule_outputs"]) -> None: ... + def ClearField(self, field_name: typing.Literal["created_annotation_count", b"created_annotation_count", "dry_run_annotations", b"dry_run_annotations"]) -> None: ... global___EvaluateRulesPreviewResponse = EvaluateRulesPreviewResponse diff --git a/python/lib/sift/rules/v1/rules_pb2.py b/python/lib/sift/rules/v1/rules_pb2.py index 9ddf730c5..b8aa63505 100644 --- a/python/lib/sift/rules/v1/rules_pb2.py +++ b/python/lib/sift/rules/v1/rules_pb2.py @@ -22,7 +22,7 @@ from sift.metadata.v1 import metadata_pb2 as sift_dot_metadata_dot_v1_dot_metadata__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19sift/rules/v1/rules.proto\x12\rsift.rules.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\x1a%sift/annotations/v1/annotations.proto\x1a-sift/common/type/v1/resource_identifier.proto\x1a\x30sift/common/type/v1/user_defined_functions.proto\x1a\x1fsift/metadata/v1/metadata.proto\"\xdb\x07\n\x04Rule\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\x12 \n\x08\x61sset_id\x18\x02 \x01(\tB\x05\x18\x01\xe0\x41\x02R\x07\x61ssetId\x12\x17\n\x04name\x18\x03 \x01(\tB\x03\xe0\x41\x02R\x04name\x12%\n\x0b\x64\x65scription\x18\x04 \x01(\tB\x03\xe0\x41\x02R\x0b\x64\x65scription\x12\"\n\nis_enabled\x18\x06 \x01(\x08\x42\x03\xe0\x41\x02R\tisEnabled\x12\x42\n\x0c\x63reated_date\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0b\x63reatedDate\x12\x44\n\rmodified_date\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0cmodifiedDate\x12\x30\n\x12\x63reated_by_user_id\x18\t \x01(\tB\x03\xe0\x41\x02R\x0f\x63reatedByUserId\x12\x32\n\x13modified_by_user_id\x18\n \x01(\tB\x03\xe0\x41\x02R\x10modifiedByUserId\x12,\n\x0forganization_id\x18\x0b \x01(\tB\x03\xe0\x41\x02R\x0eorganizationId\x12\x41\n\nconditions\x18\x0c \x03(\x0b\x32\x1c.sift.rules.v1.RuleConditionB\x03\xe0\x41\x02R\nconditions\x12\x42\n\x0crule_version\x18\r \x01(\x0b\x32\x1a.sift.rules.v1.RuleVersionB\x03\xe0\x41\x02R\x0bruleVersion\x12\"\n\nclient_key\x18\x0e \x01(\tB\x03\xe0\x41\x01R\tclientKey\x12[\n\x13\x61sset_configuration\x18\x0f \x01(\x0b\x32%.sift.rules.v1.RuleAssetConfigurationB\x03\xe0\x41\x02R\x12\x61ssetConfiguration\x12W\n\x13\x63ontextual_channels\x18\x10 \x01(\x0b\x32!.sift.rules.v1.ContextualChannelsB\x03\xe0\x41\x02R\x12\x63ontextualChannels\x12\x42\n\x0c\x64\x65leted_date\x18\x11 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x01R\x0b\x64\x65letedDate\x12$\n\x0bis_external\x18\x12 \x01(\x08\x42\x03\xe0\x41\x02R\nisExternal\x12@\n\x08metadata\x18\x13 \x03(\x0b\x32\x1f.sift.metadata.v1.MetadataValueB\x03\xe0\x41\x02R\x08metadataJ\x04\x08\x05\x10\x06\"\x9b\x04\n\rRuleCondition\x12/\n\x11rule_condition_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x0fruleConditionId\x12\x1c\n\x07rule_id\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\x12K\n\nexpression\x18\x03 \x01(\x0b\x32&.sift.rules.v1.RuleConditionExpressionB\x03\xe0\x41\x02R\nexpression\x12\x42\n\x0c\x63reated_date\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0b\x63reatedDate\x12\x44\n\rmodified_date\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0cmodifiedDate\x12\x30\n\x12\x63reated_by_user_id\x18\x07 \x01(\tB\x03\xe0\x41\x02R\x0f\x63reatedByUserId\x12\x32\n\x13modified_by_user_id\x18\x08 \x01(\tB\x03\xe0\x41\x02R\x10modifiedByUserId\x12\x38\n\x07\x61\x63tions\x18\t \x03(\x0b\x32\x19.sift.rules.v1.RuleActionB\x03\xe0\x41\x02R\x07\x61\x63tions\x12>\n\x19rule_condition_version_id\x18\n \x01(\tB\x03\xe0\x41\x02R\x16ruleConditionVersionIdJ\x04\x08\x04\x10\x05\"\xa6\x04\n\nRuleAction\x12)\n\x0erule_action_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x0cruleActionId\x12/\n\x11rule_condition_id\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x0fruleConditionId\x12?\n\x0b\x61\x63tion_type\x18\x03 \x01(\x0e\x32\x19.sift.rules.v1.ActionKindB\x03\xe0\x41\x02R\nactionType\x12Q\n\rconfiguration\x18\x04 \x01(\x0b\x32&.sift.rules.v1.RuleActionConfigurationB\x03\xe0\x41\x02R\rconfiguration\x12\x42\n\x0c\x63reated_date\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0b\x63reatedDate\x12\x44\n\rmodified_date\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0cmodifiedDate\x12\x30\n\x12\x63reated_by_user_id\x18\x07 \x01(\tB\x03\xe0\x41\x02R\x0f\x63reatedByUserId\x12\x32\n\x13modified_by_user_id\x18\x08 \x01(\tB\x03\xe0\x41\x02R\x10modifiedByUserId\x12\x38\n\x16rule_action_version_id\x18\t \x01(\tB\x03\xe0\x41\x02R\x13ruleActionVersionId\"N\n\x16RuleAssetConfiguration\x12\x1b\n\tasset_ids\x18\x01 \x03(\tR\x08\x61ssetIds\x12\x17\n\x07tag_ids\x18\x02 \x03(\tR\x06tagIds\"V\n\x12\x43ontextualChannels\x12@\n\x08\x63hannels\x18\x01 \x03(\x0b\x32\x1f.sift.rules.v1.ChannelReferenceB\x03\xe0\x41\x02R\x08\x63hannels\"\xb6\x01\n\x1f\x41ssetExpressionValidationResult\x12\x1e\n\x08\x61sset_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x07\x61ssetId\x12\"\n\nasset_name\x18\x02 \x01(\tB\x03\xe0\x41\x02R\tassetName\x12%\n\x0c\x61sset_tag_id\x18\x03 \x01(\tB\x03\xe0\x41\x01R\nassetTagId\x12\x1e\n\x05\x65rror\x18\x04 \x01(\tB\x03\xe0\x41\x01H\x00R\x05\x65rror\x88\x01\x01\x42\x08\n\x06_error\"\xc6\x03\n\x12SearchRulesRequest\x12\x19\n\x05limit\x18\x01 \x01(\rH\x00R\x05limit\x88\x01\x01\x12\x16\n\x06offset\x18\x02 \x01(\rR\x06offset\x12\x35\n\x05order\x18\x03 \x01(\x0e\x32\x1a.sift.rules.v1.SearchOrderH\x01R\x05order\x88\x01\x01\x12!\n\x0cname_matches\x18\x04 \x01(\tR\x0bnameMatches\x12%\n\x0e\x63\x61se_sensitive\x18\x05 \x01(\x08R\rcaseSensitive\x12\x16\n\x06regexp\x18\x06 \x01(\x08R\x06regexp\x12\x1e\n\x08order_by\x18\x07 \x01(\tH\x02R\x07orderBy\x88\x01\x01\x12\x19\n\x08rule_ids\x18\x08 \x03(\tR\x07ruleIds\x12\x1b\n\tasset_ids\x18\t \x03(\tR\x08\x61ssetIds\x12\'\n\x0finclude_deleted\x18\n \x01(\x08R\x0eincludeDeleted\x12\x42\n\nasset_tags\x18\x0b \x01(\x0b\x32#.sift.common.type.v1.NamedResourcesR\tassetTagsB\x08\n\x06_limitB\x08\n\x06_orderB\x0b\n\t_order_by\"`\n\x13SearchRulesResponse\x12\x19\n\x05\x63ount\x18\x01 \x01(\rB\x03\xe0\x41\x02R\x05\x63ount\x12.\n\x05rules\x18\x02 \x03(\x0b\x32\x13.sift.rules.v1.RuleB\x03\xe0\x41\x02R\x05rules\"R\n\x0eGetRuleRequest\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x01R\x06ruleId\x12\"\n\nclient_key\x18\x02 \x01(\tB\x03\xe0\x41\x01R\tclientKey\"?\n\x0fGetRuleResponse\x12,\n\x04rule\x18\x01 \x01(\x0b\x32\x13.sift.rules.v1.RuleB\x03\xe0\x41\x02R\x04rule\"\\\n\x14\x42\x61tchGetRulesRequest\x12\x1e\n\x08rule_ids\x18\x01 \x03(\tB\x03\xe0\x41\x01R\x07ruleIds\x12$\n\x0b\x63lient_keys\x18\x02 \x03(\tB\x03\xe0\x41\x01R\nclientKeys\"G\n\x15\x42\x61tchGetRulesResponse\x12.\n\x05rules\x18\x01 \x03(\x0b\x32\x13.sift.rules.v1.RuleB\x03\xe0\x41\x02R\x05rules\"R\n\x11\x43reateRuleRequest\x12=\n\x06update\x18\x01 \x01(\x0b\x32 .sift.rules.v1.UpdateRuleRequestB\x03\xe0\x41\x02R\x06update\"2\n\x12\x43reateRuleResponse\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\"\xa1\x05\n\x11UpdateRuleRequest\x12\x1c\n\x07rule_id\x18\x01 \x01(\tH\x00R\x06ruleId\x88\x01\x01\x12\x17\n\x04name\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x04name\x12%\n\x0b\x64\x65scription\x18\x03 \x01(\tB\x03\xe0\x41\x02R\x0b\x64\x65scription\x12 \n\x08\x61sset_id\x18\x04 \x01(\tB\x05\x18\x01\xe0\x41\x01R\x07\x61ssetId\x12$\n\nis_enabled\x18\x05 \x01(\x08\x42\x05\x18\x01\xe0\x41\x01R\tisEnabled\x12J\n\nconditions\x18\x06 \x03(\x0b\x32%.sift.rules.v1.UpdateConditionRequestB\x03\xe0\x41\x02R\nconditions\x12\'\n\x0forganization_id\x18\x07 \x01(\tR\x0eorganizationId\x12#\n\rversion_notes\x18\x08 \x01(\tR\x0cversionNotes\x12\"\n\nclient_key\x18\t \x01(\tH\x01R\tclientKey\x88\x01\x01\x12V\n\x13\x61sset_configuration\x18\n \x01(\x0b\x32%.sift.rules.v1.RuleAssetConfigurationR\x12\x61ssetConfiguration\x12R\n\x13\x63ontextual_channels\x18\x0b \x01(\x0b\x32!.sift.rules.v1.ContextualChannelsR\x12\x63ontextualChannels\x12\x1f\n\x0bis_external\x18\x0c \x01(\x08R\nisExternal\x12@\n\x08metadata\x18\r \x03(\x0b\x32\x1f.sift.metadata.v1.MetadataValueB\x03\xe0\x41\x02R\x08metadataB\n\n\x08_rule_idB\r\n\x0b_client_key\"\xf5\x01\n\x16UpdateConditionRequest\x12/\n\x11rule_condition_id\x18\x01 \x01(\tH\x00R\x0fruleConditionId\x88\x01\x01\x12K\n\nexpression\x18\x03 \x01(\x0b\x32&.sift.rules.v1.RuleConditionExpressionB\x03\xe0\x41\x02R\nexpression\x12\x41\n\x07\x61\x63tions\x18\x04 \x03(\x0b\x32\".sift.rules.v1.UpdateActionRequestB\x03\xe0\x41\x02R\x07\x61\x63tionsB\x14\n\x12_rule_condition_idJ\x04\x08\x02\x10\x03\"\xe7\x01\n\x13UpdateActionRequest\x12)\n\x0erule_action_id\x18\x01 \x01(\tH\x00R\x0cruleActionId\x88\x01\x01\x12?\n\x0b\x61\x63tion_type\x18\x02 \x01(\x0e\x32\x19.sift.rules.v1.ActionKindB\x03\xe0\x41\x02R\nactionType\x12Q\n\rconfiguration\x18\x03 \x01(\x0b\x32&.sift.rules.v1.RuleActionConfigurationB\x03\xe0\x41\x02R\rconfigurationB\x11\n\x0f_rule_action_id\"2\n\x12UpdateRuleResponse\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\"\x83\x02\n\x10ValidationResult\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x01R\x06ruleId\x12\"\n\nclient_key\x18\x02 \x01(\tB\x03\xe0\x41\x01R\tclientKey\x12\x82\x01\n#asset_expression_validation_results\x18\x03 \x03(\x0b\x32..sift.rules.v1.AssetExpressionValidationResultB\x03\xe0\x41\x02R assetExpressionValidationResults\x12\x1e\n\x05\x65rror\x18\x04 \x01(\tB\x03\xe0\x41\x01H\x00R\x05\x65rror\x88\x01\x01\x42\x08\n\x06_error\"\xcb\x01\n\x17\x42\x61tchUpdateRulesRequest\x12;\n\x05rules\x18\x01 \x03(\x0b\x32 .sift.rules.v1.UpdateRuleRequestB\x03\xe0\x41\x02R\x05rules\x12(\n\rvalidate_only\x18\x02 \x01(\x08\x42\x03\xe0\x41\x01R\x0cvalidateOnly\x12I\n\x1eoverride_expression_validation\x18\x03 \x01(\x08\x42\x03\xe0\x41\x02R\x1coverrideExpressionValidation\"\x9d\x04\n\x18\x42\x61tchUpdateRulesResponse\x12\x1d\n\x07success\x18\x01 \x01(\x08\x42\x03\xe0\x41\x02R\x07success\x12\x33\n\x13rules_created_count\x18\x02 \x01(\x05\x42\x03\xe0\x41\x02R\x11rulesCreatedCount\x12\x33\n\x13rules_updated_count\x18\x03 \x01(\x05\x42\x03\xe0\x41\x02R\x11rulesUpdatedCount\x12(\n\rvalidate_only\x18\x04 \x01(\x08\x42\x03\xe0\x41\x02R\x0cvalidateOnly\x12S\n\x12validation_results\x18\x05 \x03(\x0b\x32\x1f.sift.rules.v1.ValidationResultB\x03\xe0\x41\x02R\x11validationResults\x12v\n\x18\x63reated_rule_identifiers\x18\x06 \x03(\x0b\x32\x37.sift.rules.v1.BatchUpdateRulesResponse.RuleIdentifiersB\x03\xe0\x41\x02R\x16\x63reatedRuleIdentifiers\x1a\x80\x01\n\x0fRuleIdentifiers\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\x12\x17\n\x04name\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x04name\x12\'\n\nclient_key\x18\x03 \x01(\tB\x03\xe0\x41\x01H\x00R\tclientKey\x88\x01\x01\x42\r\n\x0b_client_key\"U\n\x11\x44\x65leteRuleRequest\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x01R\x06ruleId\x12\"\n\nclient_key\x18\x02 \x01(\tB\x03\xe0\x41\x01R\tclientKey\"\x14\n\x12\x44\x65leteRuleResponse\"_\n\x17\x42\x61tchDeleteRulesRequest\x12\x1e\n\x08rule_ids\x18\x01 \x03(\tB\x03\xe0\x41\x01R\x07ruleIds\x12$\n\x0b\x63lient_keys\x18\x02 \x03(\tB\x03\xe0\x41\x01R\nclientKeys\"\x1a\n\x18\x42\x61tchDeleteRulesResponse\"W\n\x13UndeleteRuleRequest\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x01R\x06ruleId\x12\"\n\nclient_key\x18\x02 \x01(\tB\x03\xe0\x41\x01R\tclientKey\"\x16\n\x14UndeleteRuleResponse\"a\n\x19\x42\x61tchUndeleteRulesRequest\x12\x1e\n\x08rule_ids\x18\x01 \x03(\tB\x03\xe0\x41\x01R\x07ruleIds\x12$\n\x0b\x63lient_keys\x18\x02 \x03(\tB\x03\xe0\x41\x01R\nclientKeys\"\x1c\n\x1a\x42\x61tchUndeleteRulesResponse\"C\n\x1dViewHumanFriendlyRulesRequest\x12\x1e\n\x08\x61sset_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x07\x61ssetId:\x02\x18\x01\"H\n\x1eViewHumanFriendlyRulesResponse\x12\"\n\nrules_json\x18\x01 \x01(\tB\x03\xe0\x41\x02R\trulesJson:\x02\x18\x01\"\x97\x01\n\x1fUpdateHumanFriendlyRulesRequest\x12\x1e\n\x08\x61sset_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x07\x61ssetId\x12\"\n\nrules_json\x18\x02 \x01(\tB\x03\xe0\x41\x02R\trulesJson\x12,\n\x0forganization_id\x18\x03 \x01(\tB\x03\xe0\x41\x02R\x0eorganizationId:\x02\x18\x01\"\x8c\x01\n UpdateHumanFriendlyRulesResponse\x12\x1d\n\x07success\x18\x01 \x01(\x08\x42\x03\xe0\x41\x02R\x07success\x12$\n\x0brules_count\x18\x02 \x01(\x05\x42\x03\xe0\x41\x02R\nrulesCount\x12\x1f\n\x08messages\x18\x03 \x01(\tB\x03\xe0\x41\x02R\x08messages:\x02\x18\x01\"6\n\x14ViewJsonRulesRequest\x12\x1e\n\x08\x61sset_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x07\x61ssetId\";\n\x15ViewJsonRulesResponse\x12\"\n\nrules_json\x18\x01 \x01(\tB\x03\xe0\x41\x02R\trulesJson\"\x84\x01\n\x10JsonRulesRequest\x12\x1e\n\x08\x61sset_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x07\x61ssetId\x12\"\n\nrules_json\x18\x02 \x01(\tB\x03\xe0\x41\x02R\trulesJson\x12,\n\x0forganization_id\x18\x03 \x01(\tB\x03\xe0\x41\x02R\x0eorganizationId\"\xc1\x02\n\x11JsonRulesResponse\x12\x1d\n\x07success\x18\x01 \x01(\x08\x42\x03\xe0\x41\x02R\x07success\x12/\n\x11total_rules_count\x18\x02 \x01(\x05\x42\x03\xe0\x41\x02R\x0ftotalRulesCount\x12\x33\n\x13rules_created_count\x18\x03 \x01(\x05\x42\x03\xe0\x41\x02R\x11rulesCreatedCount\x12\x33\n\x13rules_updated_count\x18\x04 \x01(\x05\x42\x03\xe0\x41\x02R\x11rulesUpdatedCount\x12\x33\n\x13rules_deleted_count\x18\x05 \x01(\x05\x42\x03\xe0\x41\x02R\x11rulesDeletedCount\x12*\n\x0e\x65rror_messages\x18\x06 \x01(\tH\x00R\rerrorMessages\x88\x01\x01\x42\x11\n\x0f_error_messages\"Z\n\x18ValidateJsonRulesRequest\x12>\n\x07request\x18\x01 \x01(\x0b\x32\x1f.sift.rules.v1.JsonRulesRequestB\x03\xe0\x41\x02R\x07request\"^\n\x19ValidateJsonRulesResponse\x12\x41\n\x08response\x18\x01 \x01(\x0b\x32 .sift.rules.v1.JsonRulesResponseB\x03\xe0\x41\x02R\x08response\"X\n\x16UpdateJsonRulesRequest\x12>\n\x07request\x18\x01 \x01(\x0b\x32\x1f.sift.rules.v1.JsonRulesRequestB\x03\xe0\x41\x02R\x07request\"\\\n\x17UpdateJsonRulesResponse\x12\x41\n\x08response\x18\x01 \x01(\x0b\x32 .sift.rules.v1.JsonRulesResponseB\x03\xe0\x41\x02R\x08response\"\x95\x01\n\x10ListRulesRequest\x12 \n\tpage_size\x18\x01 \x01(\rB\x03\xe0\x41\x01R\x08pageSize\x12\"\n\npage_token\x18\x02 \x01(\tB\x03\xe0\x41\x01R\tpageToken\x12\x1b\n\x06\x66ilter\x18\x03 \x01(\tB\x03\xe0\x41\x01R\x06\x66ilter\x12\x1e\n\x08order_by\x18\x04 \x01(\tB\x03\xe0\x41\x01R\x07orderBy\"k\n\x11ListRulesResponse\x12.\n\x05rules\x18\x01 \x03(\x0b\x32\x13.sift.rules.v1.RuleB\x03\xe0\x41\x02R\x05rules\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x95\x01\n\x17ListRuleVersionsRequest\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\x12 \n\tpage_size\x18\x02 \x01(\rB\x03\xe0\x41\x01R\x08pageSize\x12\"\n\npage_token\x18\x03 \x01(\tB\x03\xe0\x41\x01R\tpageToken\x12\x16\n\x06\x66ilter\x18\x04 \x01(\tR\x06\x66ilter\"\x9a\x03\n\x0bRuleVersion\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\x12+\n\x0frule_version_id\x18\x02 \x01(\tB\x03\xe0\x41\x02R\rruleVersionId\x12\x1d\n\x07version\x18\x03 \x01(\tB\x03\xe0\x41\x02R\x07version\x12\x42\n\x0c\x63reated_date\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0b\x63reatedDate\x12\x30\n\x12\x63reated_by_user_id\x18\x05 \x01(\tB\x03\xe0\x41\x02R\x0f\x63reatedByUserId\x12(\n\rversion_notes\x18\x06 \x01(\tB\x03\xe0\x41\x02R\x0cversionNotes\x12=\n\x18generated_change_message\x18\x07 \x01(\tB\x03\xe0\x41\x02R\x16generatedChangeMessage\x12\x42\n\x0c\x64\x65leted_date\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x01R\x0b\x64\x65letedDate\"\x88\x01\n\x18ListRuleVersionsResponse\x12\x44\n\rrule_versions\x18\x01 \x03(\x0b\x32\x1a.sift.rules.v1.RuleVersionB\x03\xe0\x41\x02R\x0cruleVersions\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"D\n\x15GetRuleVersionRequest\x12+\n\x0frule_version_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\rruleVersionId\"F\n\x16GetRuleVersionResponse\x12,\n\x04rule\x18\x01 \x01(\x0b\x32\x13.sift.rules.v1.RuleB\x03\xe0\x41\x02R\x04rule\"L\n\x1b\x42\x61tchGetRuleVersionsRequest\x12-\n\x10rule_version_ids\x18\x01 \x03(\tB\x03\xe0\x41\x02R\x0eruleVersionIds\"N\n\x1c\x42\x61tchGetRuleVersionsResponse\x12.\n\x05rules\x18\x01 \x03(\x0b\x32\x13.sift.rules.v1.RuleB\x03\xe0\x41\x02R\x05rules\"\xf4\x01\n\x17RuleConditionExpression\x12r\n\x19single_channel_comparison\x18\x01 \x01(\x0b\x32\x30.sift.rules.v1.SingleChannelComparisonExpressionB\x02\x18\x01H\x00R\x17singleChannelComparison\x12W\n\x12\x63\x61lculated_channel\x18\x02 \x01(\x0b\x32&.sift.rules.v1.CalculatedChannelConfigH\x00R\x11\x63\x61lculatedChannelB\x0c\n\nexpression\"\xcb\x02\n!SingleChannelComparisonExpression\x12\x30\n\x11\x63hannel_component\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x10\x63hannelComponent\x12&\n\x0c\x63hannel_name\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x0b\x63hannelName\x12G\n\ncomparator\x18\x03 \x01(\x0e\x32\".sift.rules.v1.ConditionComparatorB\x03\xe0\x41\x02R\ncomparator\x12\x18\n\x06\x64ouble\x18\x04 \x01(\x01H\x00R\x06\x64ouble\x12\x18\n\x06string\x18\x05 \x01(\tH\x00R\x06string\x12\x42\n\nlast_value\x18\x06 \x01(\x0b\x32!.sift.rules.v1.LastValueThresholdH\x00R\tlastValueB\x0b\n\tthreshold\"\x14\n\x12LastValueThreshold\"\xfb\x02\n\x17\x43\x61lculatedChannelConfig\x12q\n\x12\x63hannel_references\x18\x01 \x03(\x0b\x32=.sift.rules.v1.CalculatedChannelConfig.ChannelReferencesEntryB\x03\xe0\x41\x02R\x11\x63hannelReferences\x12#\n\nexpression\x18\x02 \x01(\tB\x03\xe0\x41\x02R\nexpression\x12\x61\n\x15\x66unction_dependencies\x18\x03 \x03(\x0b\x32\'.sift.common.type.v1.FunctionDependencyB\x03\xe0\x41\x01R\x14\x66unctionDependencies\x1a\x65\n\x16\x43hannelReferencesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32\x1f.sift.rules.v1.ChannelReferenceR\x05value:\x02\x38\x01\"N\n\x10\x43hannelReference\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x04name\x12!\n\tcomponent\x18\x02 \x01(\tB\x03\xe0\x41\x02R\tcomponent\"\xd0\x01\n\x17RuleActionConfiguration\x12T\n\x0cnotification\x18\x01 \x01(\x0b\x32..sift.rules.v1.NotificationActionConfigurationH\x00R\x0cnotification\x12N\n\nannotation\x18\x02 \x01(\x0b\x32,.sift.rules.v1.AnnotationActionConfigurationH\x00R\nannotationB\x0f\n\rconfiguration\"T\n\x1fNotificationActionConfiguration\x12\x31\n\x12recipient_user_ids\x18\x01 \x03(\tB\x03\xe0\x41\x02R\x10recipientUserIds\"\x94\x02\n\x1d\x41nnotationActionConfiguration\x12\x1c\n\x07tag_ids\x18\x01 \x03(\tB\x03\xe0\x41\x02R\x06tagIds\x12L\n\x0f\x61nnotation_type\x18\x02 \x01(\x0e\x32#.sift.annotations.v1.AnnotationTypeR\x0e\x61nnotationType\x12\x32\n\x13\x61ssigned_to_user_id\x18\x03 \x01(\tH\x00R\x10\x61ssignedToUserId\x88\x01\x01\x12;\n\x08metadata\x18\x04 \x03(\x0b\x32\x1f.sift.metadata.v1.MetadataValueR\x08metadataB\x16\n\x14_assigned_to_user_id\"\x8e\x02\n\x14\x45valuateRulesRequest\x12\x1e\n\x08rule_ids\x18\x01 \x03(\tB\x03\xe0\x41\x02R\x07ruleIds\x12X\n\x12\x61nnotation_options\x18\x02 \x01(\x0b\x32).sift.rules.v1.EvaluatedAnnotationOptionsR\x11\x61nnotationOptions\x12\x17\n\x06run_id\x18\x03 \x01(\tH\x00R\x05runId\x12>\n\ntime_range\x18\x04 \x01(\x0b\x32\x1d.sift.rules.v1.TimeRangeQueryH\x00R\ttimeRange\x12\x17\n\x07\x64ry_run\x18\x05 \x01(\x08R\x06\x64ryRun:\x02\x18\x01\x42\x06\n\x04time\"5\n\x1a\x45valuatedAnnotationOptions\x12\x17\n\x07tag_ids\x18\x01 \x03(\tR\x06tagIds\"\x82\x01\n\x0eTimeRangeQuery\x12\x39\n\nstart_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tstartTime\x12\x35\n\x08\x65nd_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x07\x65ndTime\"\xfd\x01\n\x15\x45valuateRulesResponse\x12\x38\n\x18\x63reated_annotation_count\x18\x01 \x01(\x05R\x16\x63reatedAnnotationCount\x12O\n\x13\x64ry_run_annotations\x18\x02 \x03(\x0b\x32\x1f.sift.rules.v1.DryRunAnnotationR\x11\x64ryRunAnnotations\x12\x1a\n\x06job_id\x18\x03 \x01(\tH\x00R\x05jobId\x88\x01\x01\x12 \n\treport_id\x18\x04 \x01(\tH\x01R\x08reportId\x88\x01\x01:\x02\x18\x01\x42\t\n\x07_job_idB\x0c\n\n_report_id\"\xf7\x01\n\x10\x44ryRunAnnotation\x12!\n\x0c\x63ondition_id\x18\x01 \x01(\tR\x0b\x63onditionId\x12\x12\n\x04name\x18\x02 \x01(\tR\x04name\x12>\n\nstart_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\tstartTime\x12:\n\x08\x65nd_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x07\x65ndTime\x12\x30\n\x14\x63ondition_version_id\x18\x05 \x01(\tR\x12\x63onditionVersionId*\\\n\x0bSearchOrder\x12 \n\x18SEARCH_ORDER_UNSPECIFIED\x10\x00\x1a\x02\x08\x01\x12\x14\n\x10SEARCH_ORDER_ASC\x10\x01\x12\x15\n\x11SEARCH_ORDER_DESC\x10\x02*\\\n\nActionKind\x12\x1f\n\x17\x41\x43TION_KIND_UNSPECIFIED\x10\x00\x1a\x02\x08\x01\x12\x10\n\x0cNOTIFICATION\x10\x01\x12\x0e\n\nANNOTATION\x10\x02\x12\x0b\n\x07WEBHOOK\x10\x03*\xad\x01\n\x13\x43onditionComparator\x12(\n CONDITION_COMPARATOR_UNSPECIFIED\x10\x00\x1a\x02\x08\x01\x12\r\n\tLESS_THAN\x10\x01\x12\x16\n\x12LESS_THAN_OR_EQUAL\x10\x02\x12\x10\n\x0cGREATER_THAN\x10\x03\x12\x19\n\x15GREATER_THAN_OR_EQUAL\x10\x04\x12\t\n\x05\x45QUAL\x10\x05\x12\r\n\tNOT_EQUAL\x10\x06\x32\xd0 \n\x0bRuleService\x12\xb9\x01\n\x0bSearchRules\x12!.sift.rules.v1.SearchRulesRequest\x1a\".sift.rules.v1.SearchRulesResponse\"c\x92\x41\x41\x12\x0bSearchRules\x1a\x32Queries rules based on provided search parameters.\x82\xd3\xe4\x93\x02\x19\"\x14/api/v1/rules/search:\x01*\x12\x94\x01\n\x07GetRule\x12\x1d.sift.rules.v1.GetRuleRequest\x1a\x1e.sift.rules.v1.GetRuleResponse\"J\x92\x41\x32\x12\x07GetRule\x1a\'Retrieves the latest version of a rule.\x82\xd3\xe4\x93\x02\x0f\x12\r/api/v1/rules\x12\xaa\x01\n\rBatchGetRules\x12#.sift.rules.v1.BatchGetRulesRequest\x1a$.sift.rules.v1.BatchGetRulesResponse\"N\x92\x41*\x12\rBatchGetRules\x1a\x19Retrieves multiple rules.\x82\xd3\xe4\x93\x02\x1b\"\x16/api/v1/rules:batchGet:\x01*\x12\x8b\x01\n\nCreateRule\x12 .sift.rules.v1.CreateRuleRequest\x1a!.sift.rules.v1.CreateRuleResponse\"8\x92\x41\x1d\x12\nCreateRule\x1a\x0f\x43reates a rule.\x82\xd3\xe4\x93\x02\x12\"\r/api/v1/rules:\x01*\x12\x95\x01\n\nUpdateRule\x12 .sift.rules.v1.UpdateRuleRequest\x1a!.sift.rules.v1.UpdateRuleResponse\"B\x92\x41\'\x12\nUpdateRule\x1a\x19Updates an existing rule.\x82\xd3\xe4\x93\x02\x12\x1a\r/api/v1/rules:\x01*\x12\xda\x01\n\x10\x42\x61tchUpdateRules\x12&.sift.rules.v1.BatchUpdateRulesRequest\x1a\'.sift.rules.v1.BatchUpdateRulesResponse\"u\x92\x41N\x12\x10\x42\x61tchUpdateRules\x1a:Updates existing rules or creates rules that do not exist.\x82\xd3\xe4\x93\x02\x1e\x1a\x19/api/v1/rules:batchUpdate:\x01*\x12\x92\x01\n\nDeleteRule\x12 .sift.rules.v1.DeleteRuleRequest\x1a!.sift.rules.v1.DeleteRuleResponse\"?\x92\x41\x1d\x12\nDeleteRule\x1a\x0f\x44\x65letes a rule.\x82\xd3\xe4\x93\x02\x19\"\x14/api/v1/rules/delete:\x01*\x12\xb7\x01\n\x10\x42\x61tchDeleteRules\x12&.sift.rules.v1.BatchDeleteRulesRequest\x1a\'.sift.rules.v1.BatchDeleteRulesResponse\"R\x92\x41+\x12\x10\x42\x61tchDeleteRules\x1a\x17\x44\x65letes multiple rules.\x82\xd3\xe4\x93\x02\x1e\"\x19/api/v1/rules/batchDelete:\x01*\x12\x9e\x01\n\x0cUndeleteRule\x12\".sift.rules.v1.UndeleteRuleRequest\x1a#.sift.rules.v1.UndeleteRuleResponse\"E\x92\x41!\x12\x0cUndeleteRule\x1a\x11Undeletes a rule.\x82\xd3\xe4\x93\x02\x1b\"\x16/api/v1/rules/undelete:\x01*\x12\xc3\x01\n\x12\x42\x61tchUndeleteRules\x12(.sift.rules.v1.BatchUndeleteRulesRequest\x1a).sift.rules.v1.BatchUndeleteRulesResponse\"X\x92\x41/\x12\x12\x42\x61tchUndeleteRules\x1a\x19Undeletes multiple rules.\x82\xd3\xe4\x93\x02 \"\x1b/api/v1/rules/batchUndelete:\x01*\x12\xdf\x01\n\rEvaluateRules\x12#.sift.rules.v1.EvaluateRulesRequest\x1a$.sift.rules.v1.EvaluateRulesResponse\"\x82\x01\x88\x02\x01\x92\x41[\x12\rEvaluateRules\x1aJEvaluates the provided rules and generate annotations based on the result.\x82\xd3\xe4\x93\x02\x1b\"\x16/api/v1/rules/evaluate:\x01*\x12\x84\x02\n\x16ViewHumanFriendlyRules\x12,.sift.rules.v1.ViewHumanFriendlyRulesRequest\x1a-.sift.rules.v1.ViewHumanFriendlyRulesResponse\"\x8c\x01\x88\x02\x01\x92\x41_\x12\x16ViewHumanFriendlyRules\x1a\x45Retrieve a JSON object containing all of the rules for a given asset.\x82\xd3\xe4\x93\x02!\x12\x1f/api/v1/rules:viewHumanFriendly\x12\xd6\x01\n\rViewJsonRules\x12#.sift.rules.v1.ViewJsonRulesRequest\x1a$.sift.rules.v1.ViewJsonRulesResponse\"z\x88\x02\x01\x92\x41V\x12\rViewJsonRules\x1a\x45Retrieve a JSON object containing all of the rules for a given asset.\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/rules:viewJson\x12\x94\x02\n\x18UpdateHumanFriendlyRules\x12..sift.rules.v1.UpdateHumanFriendlyRulesRequest\x1a/.sift.rules.v1.UpdateHumanFriendlyRulesResponse\"\x96\x01\x88\x02\x01\x92\x41\x64\x12\x18UpdateHumanFriendlyRules\x1aHBatch update rules given the `rules_json` which is a JSON list of rules.\x82\xd3\xe4\x93\x02&\"!/api/v1/rules:updateHumanFriendly:\x01*\x12\x80\x02\n\x11ValidateJsonRules\x12\'.sift.rules.v1.ValidateJsonRulesRequest\x1a(.sift.rules.v1.ValidateJsonRulesResponse\"\x97\x01\x88\x02\x01\x92\x41l\x12\x11ValidateJsonRules\x1aWValidate a batch update for rules given the `rules_json` which is a JSON list of rules.\x82\xd3\xe4\x93\x02\x1f\"\x1a/api/v1/rules:validateJson:\x01*\x12\xe7\x01\n\x0fUpdateJsonRules\x12%.sift.rules.v1.UpdateJsonRulesRequest\x1a&.sift.rules.v1.UpdateJsonRulesResponse\"\x84\x01\x88\x02\x01\x92\x41[\x12\x0fUpdateJsonRules\x1aHBatch update rules given the `rules_json` which is a JSON list of rules.\x82\xd3\xe4\x93\x02\x1d\"\x18/api/v1/rules:updateJson:\x01*\x12\x94\x01\n\tListRules\x12\x1f.sift.rules.v1.ListRulesRequest\x1a .sift.rules.v1.ListRulesResponse\"D\x92\x41\'\x12\tListRules\x1a\x1aRetrieves a list of rules.\x82\xd3\xe4\x93\x02\x14\x12\x12/api/v1/rules:list\x12\xd9\x01\n\x10ListRuleVersions\x12&.sift.rules.v1.ListRuleVersionsRequest\x1a\'.sift.rules.v1.ListRuleVersionsResponse\"t\x92\x41I\x12\x10ListRuleVersions\x1a\x35Retrieves a list of rule versions for the given rule.\x82\xd3\xe4\x93\x02\"\x12 /api/v1/rules/{rule_id}/versions\x12\xb8\x01\n\x0eGetRuleVersion\x12$.sift.rules.v1.GetRuleVersionRequest\x1a%.sift.rules.v1.GetRuleVersionResponse\"Y\x92\x41\x39\x12\x0eGetRuleVersion\x1a\'Retrieves a specific version of a rule.\x82\xd3\xe4\x93\x02\x17\x12\x15/api/v1/rules:version\x12\xdf\x01\n\x14\x42\x61tchGetRuleVersions\x12*.sift.rules.v1.BatchGetRuleVersionsRequest\x1a+.sift.rules.v1.BatchGetRuleVersionsResponse\"n\x92\x41\x42\x12\x14\x42\x61tchGetRuleVersions\x1a*Retrieves multiple rules by rule versions.\x82\xd3\xe4\x93\x02#\"\x1e/api/v1/rules:batchGetVersions:\x01*\x1a\xb1\x01\x92\x41\xad\x01\x12\x30Service to programmatically interact with rules.\x1ay\n\x1fRead more about what rules are.\x12Vhttps://customer.support.siftstack.com/servicedesk/customer/portal/2/article/265421102B\x88\x01\n\x11\x63om.sift.rules.v1B\nRulesProtoP\x01\xa2\x02\x03SRX\xaa\x02\rSift.Rules.V1\xca\x02\rSift\\Rules\\V1\xe2\x02\x19Sift\\Rules\\V1\\GPBMetadata\xea\x02\x0fSift::Rules::V1\x92\x41\x10\x12\x0e\n\x0cRule Serviceb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19sift/rules/v1/rules.proto\x12\rsift.rules.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\x1a%sift/annotations/v1/annotations.proto\x1a-sift/common/type/v1/resource_identifier.proto\x1a\x30sift/common/type/v1/user_defined_functions.proto\x1a\x1fsift/metadata/v1/metadata.proto\"\xc9\x08\n\x04Rule\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\x12 \n\x08\x61sset_id\x18\x02 \x01(\tB\x05\x18\x01\xe0\x41\x02R\x07\x61ssetId\x12\x17\n\x04name\x18\x03 \x01(\tB\x03\xe0\x41\x02R\x04name\x12%\n\x0b\x64\x65scription\x18\x04 \x01(\tB\x03\xe0\x41\x02R\x0b\x64\x65scription\x12\"\n\nis_enabled\x18\x06 \x01(\x08\x42\x03\xe0\x41\x02R\tisEnabled\x12\x42\n\x0c\x63reated_date\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0b\x63reatedDate\x12\x44\n\rmodified_date\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0cmodifiedDate\x12\x30\n\x12\x63reated_by_user_id\x18\t \x01(\tB\x03\xe0\x41\x02R\x0f\x63reatedByUserId\x12\x32\n\x13modified_by_user_id\x18\n \x01(\tB\x03\xe0\x41\x02R\x10modifiedByUserId\x12,\n\x0forganization_id\x18\x0b \x01(\tB\x03\xe0\x41\x02R\x0eorganizationId\x12\x41\n\nconditions\x18\x0c \x03(\x0b\x32\x1c.sift.rules.v1.RuleConditionB\x03\xe0\x41\x02R\nconditions\x12\x42\n\x0crule_version\x18\r \x01(\x0b\x32\x1a.sift.rules.v1.RuleVersionB\x03\xe0\x41\x02R\x0bruleVersion\x12\"\n\nclient_key\x18\x0e \x01(\tB\x03\xe0\x41\x01R\tclientKey\x12[\n\x13\x61sset_configuration\x18\x0f \x01(\x0b\x32%.sift.rules.v1.RuleAssetConfigurationB\x03\xe0\x41\x02R\x12\x61ssetConfiguration\x12W\n\x13\x63ontextual_channels\x18\x10 \x01(\x0b\x32!.sift.rules.v1.ContextualChannelsB\x03\xe0\x41\x02R\x12\x63ontextualChannels\x12\x44\n\x0c\x64\x65leted_date\x18\x11 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x05\x18\x01\xe0\x41\x01R\x0b\x64\x65letedDate\x12$\n\x0bis_external\x18\x12 \x01(\x08\x42\x03\xe0\x41\x02R\nisExternal\x12@\n\x08metadata\x18\x13 \x03(\x0b\x32\x1f.sift.metadata.v1.MetadataValueB\x03\xe0\x41\x02R\x08metadata\x12\x44\n\rarchived_date\x18\x14 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x01R\x0c\x61rchivedDate\x12$\n\x0bis_archived\x18\x15 \x01(\x08\x42\x03\xe0\x41\x02R\nisArchivedJ\x04\x08\x05\x10\x06\"\x9b\x04\n\rRuleCondition\x12/\n\x11rule_condition_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x0fruleConditionId\x12\x1c\n\x07rule_id\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\x12K\n\nexpression\x18\x03 \x01(\x0b\x32&.sift.rules.v1.RuleConditionExpressionB\x03\xe0\x41\x02R\nexpression\x12\x42\n\x0c\x63reated_date\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0b\x63reatedDate\x12\x44\n\rmodified_date\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0cmodifiedDate\x12\x30\n\x12\x63reated_by_user_id\x18\x07 \x01(\tB\x03\xe0\x41\x02R\x0f\x63reatedByUserId\x12\x32\n\x13modified_by_user_id\x18\x08 \x01(\tB\x03\xe0\x41\x02R\x10modifiedByUserId\x12\x38\n\x07\x61\x63tions\x18\t \x03(\x0b\x32\x19.sift.rules.v1.RuleActionB\x03\xe0\x41\x02R\x07\x61\x63tions\x12>\n\x19rule_condition_version_id\x18\n \x01(\tB\x03\xe0\x41\x02R\x16ruleConditionVersionIdJ\x04\x08\x04\x10\x05\"\xa6\x04\n\nRuleAction\x12)\n\x0erule_action_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x0cruleActionId\x12/\n\x11rule_condition_id\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x0fruleConditionId\x12?\n\x0b\x61\x63tion_type\x18\x03 \x01(\x0e\x32\x19.sift.rules.v1.ActionKindB\x03\xe0\x41\x02R\nactionType\x12Q\n\rconfiguration\x18\x04 \x01(\x0b\x32&.sift.rules.v1.RuleActionConfigurationB\x03\xe0\x41\x02R\rconfiguration\x12\x42\n\x0c\x63reated_date\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0b\x63reatedDate\x12\x44\n\rmodified_date\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0cmodifiedDate\x12\x30\n\x12\x63reated_by_user_id\x18\x07 \x01(\tB\x03\xe0\x41\x02R\x0f\x63reatedByUserId\x12\x32\n\x13modified_by_user_id\x18\x08 \x01(\tB\x03\xe0\x41\x02R\x10modifiedByUserId\x12\x38\n\x16rule_action_version_id\x18\t \x01(\tB\x03\xe0\x41\x02R\x13ruleActionVersionId\"N\n\x16RuleAssetConfiguration\x12\x1b\n\tasset_ids\x18\x01 \x03(\tR\x08\x61ssetIds\x12\x17\n\x07tag_ids\x18\x02 \x03(\tR\x06tagIds\"V\n\x12\x43ontextualChannels\x12@\n\x08\x63hannels\x18\x01 \x03(\x0b\x32\x1f.sift.rules.v1.ChannelReferenceB\x03\xe0\x41\x02R\x08\x63hannels\"\xb6\x01\n\x1f\x41ssetExpressionValidationResult\x12\x1e\n\x08\x61sset_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x07\x61ssetId\x12\"\n\nasset_name\x18\x02 \x01(\tB\x03\xe0\x41\x02R\tassetName\x12%\n\x0c\x61sset_tag_id\x18\x03 \x01(\tB\x03\xe0\x41\x01R\nassetTagId\x12\x1e\n\x05\x65rror\x18\x04 \x01(\tB\x03\xe0\x41\x01H\x00R\x05\x65rror\x88\x01\x01\x42\x08\n\x06_error\"\xc6\x03\n\x12SearchRulesRequest\x12\x19\n\x05limit\x18\x01 \x01(\rH\x00R\x05limit\x88\x01\x01\x12\x16\n\x06offset\x18\x02 \x01(\rR\x06offset\x12\x35\n\x05order\x18\x03 \x01(\x0e\x32\x1a.sift.rules.v1.SearchOrderH\x01R\x05order\x88\x01\x01\x12!\n\x0cname_matches\x18\x04 \x01(\tR\x0bnameMatches\x12%\n\x0e\x63\x61se_sensitive\x18\x05 \x01(\x08R\rcaseSensitive\x12\x16\n\x06regexp\x18\x06 \x01(\x08R\x06regexp\x12\x1e\n\x08order_by\x18\x07 \x01(\tH\x02R\x07orderBy\x88\x01\x01\x12\x19\n\x08rule_ids\x18\x08 \x03(\tR\x07ruleIds\x12\x1b\n\tasset_ids\x18\t \x03(\tR\x08\x61ssetIds\x12\'\n\x0finclude_deleted\x18\n \x01(\x08R\x0eincludeDeleted\x12\x42\n\nasset_tags\x18\x0b \x01(\x0b\x32#.sift.common.type.v1.NamedResourcesR\tassetTagsB\x08\n\x06_limitB\x08\n\x06_orderB\x0b\n\t_order_by\"`\n\x13SearchRulesResponse\x12\x19\n\x05\x63ount\x18\x01 \x01(\rB\x03\xe0\x41\x02R\x05\x63ount\x12.\n\x05rules\x18\x02 \x03(\x0b\x32\x13.sift.rules.v1.RuleB\x03\xe0\x41\x02R\x05rules\"R\n\x0eGetRuleRequest\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x01R\x06ruleId\x12\"\n\nclient_key\x18\x02 \x01(\tB\x03\xe0\x41\x01R\tclientKey\"?\n\x0fGetRuleResponse\x12,\n\x04rule\x18\x01 \x01(\x0b\x32\x13.sift.rules.v1.RuleB\x03\xe0\x41\x02R\x04rule\"\\\n\x14\x42\x61tchGetRulesRequest\x12\x1e\n\x08rule_ids\x18\x01 \x03(\tB\x03\xe0\x41\x01R\x07ruleIds\x12$\n\x0b\x63lient_keys\x18\x02 \x03(\tB\x03\xe0\x41\x01R\nclientKeys\"G\n\x15\x42\x61tchGetRulesResponse\x12.\n\x05rules\x18\x01 \x03(\x0b\x32\x13.sift.rules.v1.RuleB\x03\xe0\x41\x02R\x05rules\"R\n\x11\x43reateRuleRequest\x12=\n\x06update\x18\x01 \x01(\x0b\x32 .sift.rules.v1.UpdateRuleRequestB\x03\xe0\x41\x02R\x06update\"2\n\x12\x43reateRuleResponse\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\"\xc7\x05\n\x11UpdateRuleRequest\x12\x1c\n\x07rule_id\x18\x01 \x01(\tH\x00R\x06ruleId\x88\x01\x01\x12\x17\n\x04name\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x04name\x12%\n\x0b\x64\x65scription\x18\x03 \x01(\tB\x03\xe0\x41\x02R\x0b\x64\x65scription\x12 \n\x08\x61sset_id\x18\x04 \x01(\tB\x05\x18\x01\xe0\x41\x01R\x07\x61ssetId\x12$\n\nis_enabled\x18\x05 \x01(\x08\x42\x05\x18\x01\xe0\x41\x01R\tisEnabled\x12J\n\nconditions\x18\x06 \x03(\x0b\x32%.sift.rules.v1.UpdateConditionRequestB\x03\xe0\x41\x02R\nconditions\x12\'\n\x0forganization_id\x18\x07 \x01(\tR\x0eorganizationId\x12#\n\rversion_notes\x18\x08 \x01(\tR\x0cversionNotes\x12\"\n\nclient_key\x18\t \x01(\tH\x01R\tclientKey\x88\x01\x01\x12V\n\x13\x61sset_configuration\x18\n \x01(\x0b\x32%.sift.rules.v1.RuleAssetConfigurationR\x12\x61ssetConfiguration\x12R\n\x13\x63ontextual_channels\x18\x0b \x01(\x0b\x32!.sift.rules.v1.ContextualChannelsR\x12\x63ontextualChannels\x12\x1f\n\x0bis_external\x18\x0c \x01(\x08R\nisExternal\x12@\n\x08metadata\x18\r \x03(\x0b\x32\x1f.sift.metadata.v1.MetadataValueB\x03\xe0\x41\x02R\x08metadata\x12$\n\x0bis_archived\x18\x0e \x01(\x08\x42\x03\xe0\x41\x01R\nisArchivedB\n\n\x08_rule_idB\r\n\x0b_client_key\"\xf5\x01\n\x16UpdateConditionRequest\x12/\n\x11rule_condition_id\x18\x01 \x01(\tH\x00R\x0fruleConditionId\x88\x01\x01\x12K\n\nexpression\x18\x03 \x01(\x0b\x32&.sift.rules.v1.RuleConditionExpressionB\x03\xe0\x41\x02R\nexpression\x12\x41\n\x07\x61\x63tions\x18\x04 \x03(\x0b\x32\".sift.rules.v1.UpdateActionRequestB\x03\xe0\x41\x02R\x07\x61\x63tionsB\x14\n\x12_rule_condition_idJ\x04\x08\x02\x10\x03\"\xe7\x01\n\x13UpdateActionRequest\x12)\n\x0erule_action_id\x18\x01 \x01(\tH\x00R\x0cruleActionId\x88\x01\x01\x12?\n\x0b\x61\x63tion_type\x18\x02 \x01(\x0e\x32\x19.sift.rules.v1.ActionKindB\x03\xe0\x41\x02R\nactionType\x12Q\n\rconfiguration\x18\x03 \x01(\x0b\x32&.sift.rules.v1.RuleActionConfigurationB\x03\xe0\x41\x02R\rconfigurationB\x11\n\x0f_rule_action_id\"2\n\x12UpdateRuleResponse\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\"\x83\x02\n\x10ValidationResult\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x01R\x06ruleId\x12\"\n\nclient_key\x18\x02 \x01(\tB\x03\xe0\x41\x01R\tclientKey\x12\x82\x01\n#asset_expression_validation_results\x18\x03 \x03(\x0b\x32..sift.rules.v1.AssetExpressionValidationResultB\x03\xe0\x41\x02R assetExpressionValidationResults\x12\x1e\n\x05\x65rror\x18\x04 \x01(\tB\x03\xe0\x41\x01H\x00R\x05\x65rror\x88\x01\x01\x42\x08\n\x06_error\"\xcb\x01\n\x17\x42\x61tchUpdateRulesRequest\x12;\n\x05rules\x18\x01 \x03(\x0b\x32 .sift.rules.v1.UpdateRuleRequestB\x03\xe0\x41\x02R\x05rules\x12(\n\rvalidate_only\x18\x02 \x01(\x08\x42\x03\xe0\x41\x01R\x0cvalidateOnly\x12I\n\x1eoverride_expression_validation\x18\x03 \x01(\x08\x42\x03\xe0\x41\x02R\x1coverrideExpressionValidation\"\x9d\x04\n\x18\x42\x61tchUpdateRulesResponse\x12\x1d\n\x07success\x18\x01 \x01(\x08\x42\x03\xe0\x41\x02R\x07success\x12\x33\n\x13rules_created_count\x18\x02 \x01(\x05\x42\x03\xe0\x41\x02R\x11rulesCreatedCount\x12\x33\n\x13rules_updated_count\x18\x03 \x01(\x05\x42\x03\xe0\x41\x02R\x11rulesUpdatedCount\x12(\n\rvalidate_only\x18\x04 \x01(\x08\x42\x03\xe0\x41\x02R\x0cvalidateOnly\x12S\n\x12validation_results\x18\x05 \x03(\x0b\x32\x1f.sift.rules.v1.ValidationResultB\x03\xe0\x41\x02R\x11validationResults\x12v\n\x18\x63reated_rule_identifiers\x18\x06 \x03(\x0b\x32\x37.sift.rules.v1.BatchUpdateRulesResponse.RuleIdentifiersB\x03\xe0\x41\x02R\x16\x63reatedRuleIdentifiers\x1a\x80\x01\n\x0fRuleIdentifiers\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\x12\x17\n\x04name\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x04name\x12\'\n\nclient_key\x18\x03 \x01(\tB\x03\xe0\x41\x01H\x00R\tclientKey\x88\x01\x01\x42\r\n\x0b_client_key\"U\n\x11\x44\x65leteRuleRequest\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x01R\x06ruleId\x12\"\n\nclient_key\x18\x02 \x01(\tB\x03\xe0\x41\x01R\tclientKey\"\x14\n\x12\x44\x65leteRuleResponse\"_\n\x17\x42\x61tchDeleteRulesRequest\x12\x1e\n\x08rule_ids\x18\x01 \x03(\tB\x03\xe0\x41\x01R\x07ruleIds\x12$\n\x0b\x63lient_keys\x18\x02 \x03(\tB\x03\xe0\x41\x01R\nclientKeys\"\x1a\n\x18\x42\x61tchDeleteRulesResponse\"V\n\x12\x41rchiveRuleRequest\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x01R\x06ruleId\x12\"\n\nclient_key\x18\x02 \x01(\tB\x03\xe0\x41\x01R\tclientKey\"\x15\n\x13\x41rchiveRuleResponse\"`\n\x18\x42\x61tchArchiveRulesRequest\x12\x1e\n\x08rule_ids\x18\x01 \x03(\tB\x03\xe0\x41\x01R\x07ruleIds\x12$\n\x0b\x63lient_keys\x18\x02 \x03(\tB\x03\xe0\x41\x01R\nclientKeys\"\x1b\n\x19\x42\x61tchArchiveRulesResponse\"W\n\x13UndeleteRuleRequest\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x01R\x06ruleId\x12\"\n\nclient_key\x18\x02 \x01(\tB\x03\xe0\x41\x01R\tclientKey\"\x16\n\x14UndeleteRuleResponse\"a\n\x19\x42\x61tchUndeleteRulesRequest\x12\x1e\n\x08rule_ids\x18\x01 \x03(\tB\x03\xe0\x41\x01R\x07ruleIds\x12$\n\x0b\x63lient_keys\x18\x02 \x03(\tB\x03\xe0\x41\x01R\nclientKeys\"\x1c\n\x1a\x42\x61tchUndeleteRulesResponse\"X\n\x14UnarchiveRuleRequest\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x01R\x06ruleId\x12\"\n\nclient_key\x18\x02 \x01(\tB\x03\xe0\x41\x01R\tclientKey\"\x17\n\x15UnarchiveRuleResponse\"b\n\x1a\x42\x61tchUnarchiveRulesRequest\x12\x1e\n\x08rule_ids\x18\x01 \x03(\tB\x03\xe0\x41\x01R\x07ruleIds\x12$\n\x0b\x63lient_keys\x18\x02 \x03(\tB\x03\xe0\x41\x01R\nclientKeys\"\x1d\n\x1b\x42\x61tchUnarchiveRulesResponse\"C\n\x1dViewHumanFriendlyRulesRequest\x12\x1e\n\x08\x61sset_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x07\x61ssetId:\x02\x18\x01\"H\n\x1eViewHumanFriendlyRulesResponse\x12\"\n\nrules_json\x18\x01 \x01(\tB\x03\xe0\x41\x02R\trulesJson:\x02\x18\x01\"\x97\x01\n\x1fUpdateHumanFriendlyRulesRequest\x12\x1e\n\x08\x61sset_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x07\x61ssetId\x12\"\n\nrules_json\x18\x02 \x01(\tB\x03\xe0\x41\x02R\trulesJson\x12,\n\x0forganization_id\x18\x03 \x01(\tB\x03\xe0\x41\x02R\x0eorganizationId:\x02\x18\x01\"\x8c\x01\n UpdateHumanFriendlyRulesResponse\x12\x1d\n\x07success\x18\x01 \x01(\x08\x42\x03\xe0\x41\x02R\x07success\x12$\n\x0brules_count\x18\x02 \x01(\x05\x42\x03\xe0\x41\x02R\nrulesCount\x12\x1f\n\x08messages\x18\x03 \x01(\tB\x03\xe0\x41\x02R\x08messages:\x02\x18\x01\"6\n\x14ViewJsonRulesRequest\x12\x1e\n\x08\x61sset_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x07\x61ssetId\";\n\x15ViewJsonRulesResponse\x12\"\n\nrules_json\x18\x01 \x01(\tB\x03\xe0\x41\x02R\trulesJson\"\x84\x01\n\x10JsonRulesRequest\x12\x1e\n\x08\x61sset_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x07\x61ssetId\x12\"\n\nrules_json\x18\x02 \x01(\tB\x03\xe0\x41\x02R\trulesJson\x12,\n\x0forganization_id\x18\x03 \x01(\tB\x03\xe0\x41\x02R\x0eorganizationId\"\xc1\x02\n\x11JsonRulesResponse\x12\x1d\n\x07success\x18\x01 \x01(\x08\x42\x03\xe0\x41\x02R\x07success\x12/\n\x11total_rules_count\x18\x02 \x01(\x05\x42\x03\xe0\x41\x02R\x0ftotalRulesCount\x12\x33\n\x13rules_created_count\x18\x03 \x01(\x05\x42\x03\xe0\x41\x02R\x11rulesCreatedCount\x12\x33\n\x13rules_updated_count\x18\x04 \x01(\x05\x42\x03\xe0\x41\x02R\x11rulesUpdatedCount\x12\x33\n\x13rules_deleted_count\x18\x05 \x01(\x05\x42\x03\xe0\x41\x02R\x11rulesDeletedCount\x12*\n\x0e\x65rror_messages\x18\x06 \x01(\tH\x00R\rerrorMessages\x88\x01\x01\x42\x11\n\x0f_error_messages\"Z\n\x18ValidateJsonRulesRequest\x12>\n\x07request\x18\x01 \x01(\x0b\x32\x1f.sift.rules.v1.JsonRulesRequestB\x03\xe0\x41\x02R\x07request\"^\n\x19ValidateJsonRulesResponse\x12\x41\n\x08response\x18\x01 \x01(\x0b\x32 .sift.rules.v1.JsonRulesResponseB\x03\xe0\x41\x02R\x08response\"X\n\x16UpdateJsonRulesRequest\x12>\n\x07request\x18\x01 \x01(\x0b\x32\x1f.sift.rules.v1.JsonRulesRequestB\x03\xe0\x41\x02R\x07request\"\\\n\x17UpdateJsonRulesResponse\x12\x41\n\x08response\x18\x01 \x01(\x0b\x32 .sift.rules.v1.JsonRulesResponseB\x03\xe0\x41\x02R\x08response\"\x95\x01\n\x10ListRulesRequest\x12 \n\tpage_size\x18\x01 \x01(\rB\x03\xe0\x41\x01R\x08pageSize\x12\"\n\npage_token\x18\x02 \x01(\tB\x03\xe0\x41\x01R\tpageToken\x12\x1b\n\x06\x66ilter\x18\x03 \x01(\tB\x03\xe0\x41\x01R\x06\x66ilter\x12\x1e\n\x08order_by\x18\x04 \x01(\tB\x03\xe0\x41\x01R\x07orderBy\"k\n\x11ListRulesResponse\x12.\n\x05rules\x18\x01 \x03(\x0b\x32\x13.sift.rules.v1.RuleB\x03\xe0\x41\x02R\x05rules\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x95\x01\n\x17ListRuleVersionsRequest\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\x12 \n\tpage_size\x18\x02 \x01(\rB\x03\xe0\x41\x01R\x08pageSize\x12\"\n\npage_token\x18\x03 \x01(\tB\x03\xe0\x41\x01R\tpageToken\x12\x16\n\x06\x66ilter\x18\x04 \x01(\tR\x06\x66ilter\"\x88\x04\n\x0bRuleVersion\x12\x1c\n\x07rule_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06ruleId\x12+\n\x0frule_version_id\x18\x02 \x01(\tB\x03\xe0\x41\x02R\rruleVersionId\x12\x1d\n\x07version\x18\x03 \x01(\tB\x03\xe0\x41\x02R\x07version\x12\x42\n\x0c\x63reated_date\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x0b\x63reatedDate\x12\x30\n\x12\x63reated_by_user_id\x18\x05 \x01(\tB\x03\xe0\x41\x02R\x0f\x63reatedByUserId\x12(\n\rversion_notes\x18\x06 \x01(\tB\x03\xe0\x41\x02R\x0cversionNotes\x12=\n\x18generated_change_message\x18\x07 \x01(\tB\x03\xe0\x41\x02R\x16generatedChangeMessage\x12\x44\n\x0c\x64\x65leted_date\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x05\x18\x01\xe0\x41\x01R\x0b\x64\x65letedDate\x12\x44\n\rarchived_date\x18\t \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x01R\x0c\x61rchivedDate\x12$\n\x0bis_archived\x18\n \x01(\x08\x42\x03\xe0\x41\x02R\nisArchived\"\x88\x01\n\x18ListRuleVersionsResponse\x12\x44\n\rrule_versions\x18\x01 \x03(\x0b\x32\x1a.sift.rules.v1.RuleVersionB\x03\xe0\x41\x02R\x0cruleVersions\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"D\n\x15GetRuleVersionRequest\x12+\n\x0frule_version_id\x18\x01 \x01(\tB\x03\xe0\x41\x02R\rruleVersionId\"F\n\x16GetRuleVersionResponse\x12,\n\x04rule\x18\x01 \x01(\x0b\x32\x13.sift.rules.v1.RuleB\x03\xe0\x41\x02R\x04rule\"L\n\x1b\x42\x61tchGetRuleVersionsRequest\x12-\n\x10rule_version_ids\x18\x01 \x03(\tB\x03\xe0\x41\x02R\x0eruleVersionIds\"N\n\x1c\x42\x61tchGetRuleVersionsResponse\x12.\n\x05rules\x18\x01 \x03(\x0b\x32\x13.sift.rules.v1.RuleB\x03\xe0\x41\x02R\x05rules\"\xf4\x01\n\x17RuleConditionExpression\x12r\n\x19single_channel_comparison\x18\x01 \x01(\x0b\x32\x30.sift.rules.v1.SingleChannelComparisonExpressionB\x02\x18\x01H\x00R\x17singleChannelComparison\x12W\n\x12\x63\x61lculated_channel\x18\x02 \x01(\x0b\x32&.sift.rules.v1.CalculatedChannelConfigH\x00R\x11\x63\x61lculatedChannelB\x0c\n\nexpression\"\xcb\x02\n!SingleChannelComparisonExpression\x12\x30\n\x11\x63hannel_component\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x10\x63hannelComponent\x12&\n\x0c\x63hannel_name\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x0b\x63hannelName\x12G\n\ncomparator\x18\x03 \x01(\x0e\x32\".sift.rules.v1.ConditionComparatorB\x03\xe0\x41\x02R\ncomparator\x12\x18\n\x06\x64ouble\x18\x04 \x01(\x01H\x00R\x06\x64ouble\x12\x18\n\x06string\x18\x05 \x01(\tH\x00R\x06string\x12\x42\n\nlast_value\x18\x06 \x01(\x0b\x32!.sift.rules.v1.LastValueThresholdH\x00R\tlastValueB\x0b\n\tthreshold\"\x14\n\x12LastValueThreshold\"\xfb\x02\n\x17\x43\x61lculatedChannelConfig\x12q\n\x12\x63hannel_references\x18\x01 \x03(\x0b\x32=.sift.rules.v1.CalculatedChannelConfig.ChannelReferencesEntryB\x03\xe0\x41\x02R\x11\x63hannelReferences\x12#\n\nexpression\x18\x02 \x01(\tB\x03\xe0\x41\x02R\nexpression\x12\x61\n\x15\x66unction_dependencies\x18\x03 \x03(\x0b\x32\'.sift.common.type.v1.FunctionDependencyB\x03\xe0\x41\x01R\x14\x66unctionDependencies\x1a\x65\n\x16\x43hannelReferencesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32\x1f.sift.rules.v1.ChannelReferenceR\x05value:\x02\x38\x01\"N\n\x10\x43hannelReference\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x04name\x12!\n\tcomponent\x18\x02 \x01(\tB\x03\xe0\x41\x02R\tcomponent\"\xd0\x01\n\x17RuleActionConfiguration\x12T\n\x0cnotification\x18\x01 \x01(\x0b\x32..sift.rules.v1.NotificationActionConfigurationH\x00R\x0cnotification\x12N\n\nannotation\x18\x02 \x01(\x0b\x32,.sift.rules.v1.AnnotationActionConfigurationH\x00R\nannotationB\x0f\n\rconfiguration\"T\n\x1fNotificationActionConfiguration\x12\x31\n\x12recipient_user_ids\x18\x01 \x03(\tB\x03\xe0\x41\x02R\x10recipientUserIds\"\x94\x02\n\x1d\x41nnotationActionConfiguration\x12\x1c\n\x07tag_ids\x18\x01 \x03(\tB\x03\xe0\x41\x02R\x06tagIds\x12L\n\x0f\x61nnotation_type\x18\x02 \x01(\x0e\x32#.sift.annotations.v1.AnnotationTypeR\x0e\x61nnotationType\x12\x32\n\x13\x61ssigned_to_user_id\x18\x03 \x01(\tH\x00R\x10\x61ssignedToUserId\x88\x01\x01\x12;\n\x08metadata\x18\x04 \x03(\x0b\x32\x1f.sift.metadata.v1.MetadataValueR\x08metadataB\x16\n\x14_assigned_to_user_id\"\x8e\x02\n\x14\x45valuateRulesRequest\x12\x1e\n\x08rule_ids\x18\x01 \x03(\tB\x03\xe0\x41\x02R\x07ruleIds\x12X\n\x12\x61nnotation_options\x18\x02 \x01(\x0b\x32).sift.rules.v1.EvaluatedAnnotationOptionsR\x11\x61nnotationOptions\x12\x17\n\x06run_id\x18\x03 \x01(\tH\x00R\x05runId\x12>\n\ntime_range\x18\x04 \x01(\x0b\x32\x1d.sift.rules.v1.TimeRangeQueryH\x00R\ttimeRange\x12\x17\n\x07\x64ry_run\x18\x05 \x01(\x08R\x06\x64ryRun:\x02\x18\x01\x42\x06\n\x04time\"5\n\x1a\x45valuatedAnnotationOptions\x12\x17\n\x07tag_ids\x18\x01 \x03(\tR\x06tagIds\"\x82\x01\n\x0eTimeRangeQuery\x12\x39\n\nstart_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tstartTime\x12\x35\n\x08\x65nd_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x07\x65ndTime\"\xfd\x01\n\x15\x45valuateRulesResponse\x12\x38\n\x18\x63reated_annotation_count\x18\x01 \x01(\x05R\x16\x63reatedAnnotationCount\x12O\n\x13\x64ry_run_annotations\x18\x02 \x03(\x0b\x32\x1f.sift.rules.v1.DryRunAnnotationR\x11\x64ryRunAnnotations\x12\x1a\n\x06job_id\x18\x03 \x01(\tH\x00R\x05jobId\x88\x01\x01\x12 \n\treport_id\x18\x04 \x01(\tH\x01R\x08reportId\x88\x01\x01:\x02\x18\x01\x42\t\n\x07_job_idB\x0c\n\n_report_id\"\xf7\x01\n\x10\x44ryRunAnnotation\x12!\n\x0c\x63ondition_id\x18\x01 \x01(\tR\x0b\x63onditionId\x12\x12\n\x04name\x18\x02 \x01(\tR\x04name\x12>\n\nstart_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\tstartTime\x12:\n\x08\x65nd_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x02R\x07\x65ndTime\x12\x30\n\x14\x63ondition_version_id\x18\x05 \x01(\tR\x12\x63onditionVersionId*\\\n\x0bSearchOrder\x12 \n\x18SEARCH_ORDER_UNSPECIFIED\x10\x00\x1a\x02\x08\x01\x12\x14\n\x10SEARCH_ORDER_ASC\x10\x01\x12\x15\n\x11SEARCH_ORDER_DESC\x10\x02*\\\n\nActionKind\x12\x1f\n\x17\x41\x43TION_KIND_UNSPECIFIED\x10\x00\x1a\x02\x08\x01\x12\x10\n\x0cNOTIFICATION\x10\x01\x12\x0e\n\nANNOTATION\x10\x02\x12\x0b\n\x07WEBHOOK\x10\x03*\xad\x01\n\x13\x43onditionComparator\x12(\n CONDITION_COMPARATOR_UNSPECIFIED\x10\x00\x1a\x02\x08\x01\x12\r\n\tLESS_THAN\x10\x01\x12\x16\n\x12LESS_THAN_OR_EQUAL\x10\x02\x12\x10\n\x0cGREATER_THAN\x10\x03\x12\x19\n\x15GREATER_THAN_OR_EQUAL\x10\x04\x12\t\n\x05\x45QUAL\x10\x05\x12\r\n\tNOT_EQUAL\x10\x06\x32\x9f\'\n\x0bRuleService\x12\xb9\x01\n\x0bSearchRules\x12!.sift.rules.v1.SearchRulesRequest\x1a\".sift.rules.v1.SearchRulesResponse\"c\x92\x41\x41\x12\x0bSearchRules\x1a\x32Queries rules based on provided search parameters.\x82\xd3\xe4\x93\x02\x19\"\x14/api/v1/rules/search:\x01*\x12\x94\x01\n\x07GetRule\x12\x1d.sift.rules.v1.GetRuleRequest\x1a\x1e.sift.rules.v1.GetRuleResponse\"J\x92\x41\x32\x12\x07GetRule\x1a\'Retrieves the latest version of a rule.\x82\xd3\xe4\x93\x02\x0f\x12\r/api/v1/rules\x12\xaa\x01\n\rBatchGetRules\x12#.sift.rules.v1.BatchGetRulesRequest\x1a$.sift.rules.v1.BatchGetRulesResponse\"N\x92\x41*\x12\rBatchGetRules\x1a\x19Retrieves multiple rules.\x82\xd3\xe4\x93\x02\x1b\"\x16/api/v1/rules:batchGet:\x01*\x12\x8b\x01\n\nCreateRule\x12 .sift.rules.v1.CreateRuleRequest\x1a!.sift.rules.v1.CreateRuleResponse\"8\x92\x41\x1d\x12\nCreateRule\x1a\x0f\x43reates a rule.\x82\xd3\xe4\x93\x02\x12\"\r/api/v1/rules:\x01*\x12\x95\x01\n\nUpdateRule\x12 .sift.rules.v1.UpdateRuleRequest\x1a!.sift.rules.v1.UpdateRuleResponse\"B\x92\x41\'\x12\nUpdateRule\x1a\x19Updates an existing rule.\x82\xd3\xe4\x93\x02\x12\x1a\r/api/v1/rules:\x01*\x12\xda\x01\n\x10\x42\x61tchUpdateRules\x12&.sift.rules.v1.BatchUpdateRulesRequest\x1a\'.sift.rules.v1.BatchUpdateRulesResponse\"u\x92\x41N\x12\x10\x42\x61tchUpdateRules\x1a:Updates existing rules or creates rules that do not exist.\x82\xd3\xe4\x93\x02\x1e\x1a\x19/api/v1/rules:batchUpdate:\x01*\x12\xbb\x01\n\nDeleteRule\x12 .sift.rules.v1.DeleteRuleRequest\x1a!.sift.rules.v1.DeleteRuleResponse\"h\x88\x02\x01\x92\x41\x43\x12\nDeleteRule\x1a\x35\x41rchives a rule. Deprecated: Use ArchiveRule instead.\x82\xd3\xe4\x93\x02\x19\"\x14/api/v1/rules/delete:\x01*\x12\x98\x01\n\x0b\x41rchiveRule\x12!.sift.rules.v1.ArchiveRuleRequest\x1a\".sift.rules.v1.ArchiveRuleResponse\"B\x92\x41\x1f\x12\x0b\x41rchiveRule\x1a\x10\x41rchives a rule.\x82\xd3\xe4\x93\x02\x1a\"\x15/api/v1/rules/archive:\x01*\x12\xe7\x01\n\x10\x42\x61tchDeleteRules\x12&.sift.rules.v1.BatchDeleteRulesRequest\x1a\'.sift.rules.v1.BatchDeleteRulesResponse\"\x81\x01\x88\x02\x01\x92\x41W\x12\x10\x42\x61tchDeleteRules\x1a\x43\x41rchives multiple rules. Deprecated: Use BatchArchiveRules instead.\x82\xd3\xe4\x93\x02\x1e\"\x19/api/v1/rules/batchDelete:\x01*\x12\xba\x01\n\x11\x42\x61tchArchiveRules\x12\'.sift.rules.v1.BatchArchiveRulesRequest\x1a(.sift.rules.v1.BatchArchiveRulesResponse\"R\x92\x41*\x12\x11\x42\x61tchArchiveRules\x1a\x15\x42\x61tch archives rules.\x82\xd3\xe4\x93\x02\x1f\"\x1a/api/v1/rules/batchArchive:\x01*\x12\xa4\x01\n\rUnarchiveRule\x12#.sift.rules.v1.UnarchiveRuleRequest\x1a$.sift.rules.v1.UnarchiveRuleResponse\"H\x92\x41#\x12\rUnarchiveRule\x1a\x12Unarchives a rule.\x82\xd3\xe4\x93\x02\x1c\"\x17/api/v1/rules/unarchive:\x01*\x12\xc6\x01\n\x13\x42\x61tchUnarchiveRules\x12).sift.rules.v1.BatchUnarchiveRulesRequest\x1a*.sift.rules.v1.BatchUnarchiveRulesResponse\"X\x92\x41.\x12\x13\x42\x61tchUnarchiveRules\x1a\x17\x42\x61tch unarchives rules.\x82\xd3\xe4\x93\x02!\"\x1c/api/v1/rules/batchUnarchive:\x01*\x12\xb6\x01\n\x0cUndeleteRule\x12\".sift.rules.v1.UndeleteRuleRequest\x1a#.sift.rules.v1.UndeleteRuleResponse\"]\x88\x02\x01\x92\x41\x36\x12\x0cUndeleteRule\x1a&Deprecated: Use UnarchiveRule instead.\x82\xd3\xe4\x93\x02\x1b\"\x16/api/v1/rules/undelete:\x01*\x12\xd9\x01\n\x12\x42\x61tchUndeleteRules\x12(.sift.rules.v1.BatchUndeleteRulesRequest\x1a).sift.rules.v1.BatchUndeleteRulesResponse\"n\x88\x02\x01\x92\x41\x42\x12\x12\x42\x61tchUndeleteRules\x1a,Deprecated: Use BatchUnarchiveRules instead.\x82\xd3\xe4\x93\x02 \"\x1b/api/v1/rules/batchUndelete:\x01*\x12\xdf\x01\n\rEvaluateRules\x12#.sift.rules.v1.EvaluateRulesRequest\x1a$.sift.rules.v1.EvaluateRulesResponse\"\x82\x01\x88\x02\x01\x92\x41[\x12\rEvaluateRules\x1aJEvaluates the provided rules and generate annotations based on the result.\x82\xd3\xe4\x93\x02\x1b\"\x16/api/v1/rules/evaluate:\x01*\x12\x84\x02\n\x16ViewHumanFriendlyRules\x12,.sift.rules.v1.ViewHumanFriendlyRulesRequest\x1a-.sift.rules.v1.ViewHumanFriendlyRulesResponse\"\x8c\x01\x88\x02\x01\x92\x41_\x12\x16ViewHumanFriendlyRules\x1a\x45Retrieve a JSON object containing all of the rules for a given asset.\x82\xd3\xe4\x93\x02!\x12\x1f/api/v1/rules:viewHumanFriendly\x12\xd6\x01\n\rViewJsonRules\x12#.sift.rules.v1.ViewJsonRulesRequest\x1a$.sift.rules.v1.ViewJsonRulesResponse\"z\x88\x02\x01\x92\x41V\x12\rViewJsonRules\x1a\x45Retrieve a JSON object containing all of the rules for a given asset.\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/rules:viewJson\x12\x94\x02\n\x18UpdateHumanFriendlyRules\x12..sift.rules.v1.UpdateHumanFriendlyRulesRequest\x1a/.sift.rules.v1.UpdateHumanFriendlyRulesResponse\"\x96\x01\x88\x02\x01\x92\x41\x64\x12\x18UpdateHumanFriendlyRules\x1aHBatch update rules given the `rules_json` which is a JSON list of rules.\x82\xd3\xe4\x93\x02&\"!/api/v1/rules:updateHumanFriendly:\x01*\x12\x80\x02\n\x11ValidateJsonRules\x12\'.sift.rules.v1.ValidateJsonRulesRequest\x1a(.sift.rules.v1.ValidateJsonRulesResponse\"\x97\x01\x88\x02\x01\x92\x41l\x12\x11ValidateJsonRules\x1aWValidate a batch update for rules given the `rules_json` which is a JSON list of rules.\x82\xd3\xe4\x93\x02\x1f\"\x1a/api/v1/rules:validateJson:\x01*\x12\xe7\x01\n\x0fUpdateJsonRules\x12%.sift.rules.v1.UpdateJsonRulesRequest\x1a&.sift.rules.v1.UpdateJsonRulesResponse\"\x84\x01\x88\x02\x01\x92\x41[\x12\x0fUpdateJsonRules\x1aHBatch update rules given the `rules_json` which is a JSON list of rules.\x82\xd3\xe4\x93\x02\x1d\"\x18/api/v1/rules:updateJson:\x01*\x12\x94\x01\n\tListRules\x12\x1f.sift.rules.v1.ListRulesRequest\x1a .sift.rules.v1.ListRulesResponse\"D\x92\x41\'\x12\tListRules\x1a\x1aRetrieves a list of rules.\x82\xd3\xe4\x93\x02\x14\x12\x12/api/v1/rules:list\x12\xd9\x01\n\x10ListRuleVersions\x12&.sift.rules.v1.ListRuleVersionsRequest\x1a\'.sift.rules.v1.ListRuleVersionsResponse\"t\x92\x41I\x12\x10ListRuleVersions\x1a\x35Retrieves a list of rule versions for the given rule.\x82\xd3\xe4\x93\x02\"\x12 /api/v1/rules/{rule_id}/versions\x12\xb8\x01\n\x0eGetRuleVersion\x12$.sift.rules.v1.GetRuleVersionRequest\x1a%.sift.rules.v1.GetRuleVersionResponse\"Y\x92\x41\x39\x12\x0eGetRuleVersion\x1a\'Retrieves a specific version of a rule.\x82\xd3\xe4\x93\x02\x17\x12\x15/api/v1/rules:version\x12\xdf\x01\n\x14\x42\x61tchGetRuleVersions\x12*.sift.rules.v1.BatchGetRuleVersionsRequest\x1a+.sift.rules.v1.BatchGetRuleVersionsResponse\"n\x92\x41\x42\x12\x14\x42\x61tchGetRuleVersions\x1a*Retrieves multiple rules by rule versions.\x82\xd3\xe4\x93\x02#\"\x1e/api/v1/rules:batchGetVersions:\x01*\x1a\xb1\x01\x92\x41\xad\x01\x12\x30Service to programmatically interact with rules.\x1ay\n\x1fRead more about what rules are.\x12Vhttps://customer.support.siftstack.com/servicedesk/customer/portal/2/article/265421102B\x88\x01\n\x11\x63om.sift.rules.v1B\nRulesProtoP\x01\xa2\x02\x03SRX\xaa\x02\rSift.Rules.V1\xca\x02\rSift\\Rules\\V1\xe2\x02\x19Sift\\Rules\\V1\\GPBMetadata\xea\x02\x0fSift::Rules::V1\x92\x41\x10\x12\x0e\n\x0cRule Serviceb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -67,11 +67,15 @@ _globals['_RULE'].fields_by_name['contextual_channels']._loaded_options = None _globals['_RULE'].fields_by_name['contextual_channels']._serialized_options = b'\340A\002' _globals['_RULE'].fields_by_name['deleted_date']._loaded_options = None - _globals['_RULE'].fields_by_name['deleted_date']._serialized_options = b'\340A\001' + _globals['_RULE'].fields_by_name['deleted_date']._serialized_options = b'\030\001\340A\001' _globals['_RULE'].fields_by_name['is_external']._loaded_options = None _globals['_RULE'].fields_by_name['is_external']._serialized_options = b'\340A\002' _globals['_RULE'].fields_by_name['metadata']._loaded_options = None _globals['_RULE'].fields_by_name['metadata']._serialized_options = b'\340A\002' + _globals['_RULE'].fields_by_name['archived_date']._loaded_options = None + _globals['_RULE'].fields_by_name['archived_date']._serialized_options = b'\340A\001' + _globals['_RULE'].fields_by_name['is_archived']._loaded_options = None + _globals['_RULE'].fields_by_name['is_archived']._serialized_options = b'\340A\002' _globals['_RULECONDITION'].fields_by_name['rule_condition_id']._loaded_options = None _globals['_RULECONDITION'].fields_by_name['rule_condition_id']._serialized_options = b'\340A\002' _globals['_RULECONDITION'].fields_by_name['rule_id']._loaded_options = None @@ -150,6 +154,8 @@ _globals['_UPDATERULEREQUEST'].fields_by_name['conditions']._serialized_options = b'\340A\002' _globals['_UPDATERULEREQUEST'].fields_by_name['metadata']._loaded_options = None _globals['_UPDATERULEREQUEST'].fields_by_name['metadata']._serialized_options = b'\340A\002' + _globals['_UPDATERULEREQUEST'].fields_by_name['is_archived']._loaded_options = None + _globals['_UPDATERULEREQUEST'].fields_by_name['is_archived']._serialized_options = b'\340A\001' _globals['_UPDATECONDITIONREQUEST'].fields_by_name['expression']._loaded_options = None _globals['_UPDATECONDITIONREQUEST'].fields_by_name['expression']._serialized_options = b'\340A\002' _globals['_UPDATECONDITIONREQUEST'].fields_by_name['actions']._loaded_options = None @@ -200,6 +206,14 @@ _globals['_BATCHDELETERULESREQUEST'].fields_by_name['rule_ids']._serialized_options = b'\340A\001' _globals['_BATCHDELETERULESREQUEST'].fields_by_name['client_keys']._loaded_options = None _globals['_BATCHDELETERULESREQUEST'].fields_by_name['client_keys']._serialized_options = b'\340A\001' + _globals['_ARCHIVERULEREQUEST'].fields_by_name['rule_id']._loaded_options = None + _globals['_ARCHIVERULEREQUEST'].fields_by_name['rule_id']._serialized_options = b'\340A\001' + _globals['_ARCHIVERULEREQUEST'].fields_by_name['client_key']._loaded_options = None + _globals['_ARCHIVERULEREQUEST'].fields_by_name['client_key']._serialized_options = b'\340A\001' + _globals['_BATCHARCHIVERULESREQUEST'].fields_by_name['rule_ids']._loaded_options = None + _globals['_BATCHARCHIVERULESREQUEST'].fields_by_name['rule_ids']._serialized_options = b'\340A\001' + _globals['_BATCHARCHIVERULESREQUEST'].fields_by_name['client_keys']._loaded_options = None + _globals['_BATCHARCHIVERULESREQUEST'].fields_by_name['client_keys']._serialized_options = b'\340A\001' _globals['_UNDELETERULEREQUEST'].fields_by_name['rule_id']._loaded_options = None _globals['_UNDELETERULEREQUEST'].fields_by_name['rule_id']._serialized_options = b'\340A\001' _globals['_UNDELETERULEREQUEST'].fields_by_name['client_key']._loaded_options = None @@ -208,6 +222,14 @@ _globals['_BATCHUNDELETERULESREQUEST'].fields_by_name['rule_ids']._serialized_options = b'\340A\001' _globals['_BATCHUNDELETERULESREQUEST'].fields_by_name['client_keys']._loaded_options = None _globals['_BATCHUNDELETERULESREQUEST'].fields_by_name['client_keys']._serialized_options = b'\340A\001' + _globals['_UNARCHIVERULEREQUEST'].fields_by_name['rule_id']._loaded_options = None + _globals['_UNARCHIVERULEREQUEST'].fields_by_name['rule_id']._serialized_options = b'\340A\001' + _globals['_UNARCHIVERULEREQUEST'].fields_by_name['client_key']._loaded_options = None + _globals['_UNARCHIVERULEREQUEST'].fields_by_name['client_key']._serialized_options = b'\340A\001' + _globals['_BATCHUNARCHIVERULESREQUEST'].fields_by_name['rule_ids']._loaded_options = None + _globals['_BATCHUNARCHIVERULESREQUEST'].fields_by_name['rule_ids']._serialized_options = b'\340A\001' + _globals['_BATCHUNARCHIVERULESREQUEST'].fields_by_name['client_keys']._loaded_options = None + _globals['_BATCHUNARCHIVERULESREQUEST'].fields_by_name['client_keys']._serialized_options = b'\340A\001' _globals['_VIEWHUMANFRIENDLYRULESREQUEST'].fields_by_name['asset_id']._loaded_options = None _globals['_VIEWHUMANFRIENDLYRULESREQUEST'].fields_by_name['asset_id']._serialized_options = b'\340A\002' _globals['_VIEWHUMANFRIENDLYRULESREQUEST']._loaded_options = None @@ -291,7 +313,11 @@ _globals['_RULEVERSION'].fields_by_name['generated_change_message']._loaded_options = None _globals['_RULEVERSION'].fields_by_name['generated_change_message']._serialized_options = b'\340A\002' _globals['_RULEVERSION'].fields_by_name['deleted_date']._loaded_options = None - _globals['_RULEVERSION'].fields_by_name['deleted_date']._serialized_options = b'\340A\001' + _globals['_RULEVERSION'].fields_by_name['deleted_date']._serialized_options = b'\030\001\340A\001' + _globals['_RULEVERSION'].fields_by_name['archived_date']._loaded_options = None + _globals['_RULEVERSION'].fields_by_name['archived_date']._serialized_options = b'\340A\001' + _globals['_RULEVERSION'].fields_by_name['is_archived']._loaded_options = None + _globals['_RULEVERSION'].fields_by_name['is_archived']._serialized_options = b'\340A\002' _globals['_LISTRULEVERSIONSRESPONSE'].fields_by_name['rule_versions']._loaded_options = None _globals['_LISTRULEVERSIONSRESPONSE'].fields_by_name['rule_versions']._serialized_options = b'\340A\002' _globals['_GETRULEVERSIONREQUEST'].fields_by_name['rule_version_id']._loaded_options = None @@ -351,13 +377,21 @@ _globals['_RULESERVICE'].methods_by_name['BatchUpdateRules']._loaded_options = None _globals['_RULESERVICE'].methods_by_name['BatchUpdateRules']._serialized_options = b'\222AN\022\020BatchUpdateRules\032:Updates existing rules or creates rules that do not exist.\202\323\344\223\002\036\032\031/api/v1/rules:batchUpdate:\001*' _globals['_RULESERVICE'].methods_by_name['DeleteRule']._loaded_options = None - _globals['_RULESERVICE'].methods_by_name['DeleteRule']._serialized_options = b'\222A\035\022\nDeleteRule\032\017Deletes a rule.\202\323\344\223\002\031\"\024/api/v1/rules/delete:\001*' + _globals['_RULESERVICE'].methods_by_name['DeleteRule']._serialized_options = b'\210\002\001\222AC\022\nDeleteRule\0325Archives a rule. Deprecated: Use ArchiveRule instead.\202\323\344\223\002\031\"\024/api/v1/rules/delete:\001*' + _globals['_RULESERVICE'].methods_by_name['ArchiveRule']._loaded_options = None + _globals['_RULESERVICE'].methods_by_name['ArchiveRule']._serialized_options = b'\222A\037\022\013ArchiveRule\032\020Archives a rule.\202\323\344\223\002\032\"\025/api/v1/rules/archive:\001*' _globals['_RULESERVICE'].methods_by_name['BatchDeleteRules']._loaded_options = None - _globals['_RULESERVICE'].methods_by_name['BatchDeleteRules']._serialized_options = b'\222A+\022\020BatchDeleteRules\032\027Deletes multiple rules.\202\323\344\223\002\036\"\031/api/v1/rules/batchDelete:\001*' + _globals['_RULESERVICE'].methods_by_name['BatchDeleteRules']._serialized_options = b'\210\002\001\222AW\022\020BatchDeleteRules\032CArchives multiple rules. Deprecated: Use BatchArchiveRules instead.\202\323\344\223\002\036\"\031/api/v1/rules/batchDelete:\001*' + _globals['_RULESERVICE'].methods_by_name['BatchArchiveRules']._loaded_options = None + _globals['_RULESERVICE'].methods_by_name['BatchArchiveRules']._serialized_options = b'\222A*\022\021BatchArchiveRules\032\025Batch archives rules.\202\323\344\223\002\037\"\032/api/v1/rules/batchArchive:\001*' + _globals['_RULESERVICE'].methods_by_name['UnarchiveRule']._loaded_options = None + _globals['_RULESERVICE'].methods_by_name['UnarchiveRule']._serialized_options = b'\222A#\022\rUnarchiveRule\032\022Unarchives a rule.\202\323\344\223\002\034\"\027/api/v1/rules/unarchive:\001*' + _globals['_RULESERVICE'].methods_by_name['BatchUnarchiveRules']._loaded_options = None + _globals['_RULESERVICE'].methods_by_name['BatchUnarchiveRules']._serialized_options = b'\222A.\022\023BatchUnarchiveRules\032\027Batch unarchives rules.\202\323\344\223\002!\"\034/api/v1/rules/batchUnarchive:\001*' _globals['_RULESERVICE'].methods_by_name['UndeleteRule']._loaded_options = None - _globals['_RULESERVICE'].methods_by_name['UndeleteRule']._serialized_options = b'\222A!\022\014UndeleteRule\032\021Undeletes a rule.\202\323\344\223\002\033\"\026/api/v1/rules/undelete:\001*' + _globals['_RULESERVICE'].methods_by_name['UndeleteRule']._serialized_options = b'\210\002\001\222A6\022\014UndeleteRule\032&Deprecated: Use UnarchiveRule instead.\202\323\344\223\002\033\"\026/api/v1/rules/undelete:\001*' _globals['_RULESERVICE'].methods_by_name['BatchUndeleteRules']._loaded_options = None - _globals['_RULESERVICE'].methods_by_name['BatchUndeleteRules']._serialized_options = b'\222A/\022\022BatchUndeleteRules\032\031Undeletes multiple rules.\202\323\344\223\002 \"\033/api/v1/rules/batchUndelete:\001*' + _globals['_RULESERVICE'].methods_by_name['BatchUndeleteRules']._serialized_options = b'\210\002\001\222AB\022\022BatchUndeleteRules\032,Deprecated: Use BatchUnarchiveRules instead.\202\323\344\223\002 \"\033/api/v1/rules/batchUndelete:\001*' _globals['_RULESERVICE'].methods_by_name['EvaluateRules']._loaded_options = None _globals['_RULESERVICE'].methods_by_name['EvaluateRules']._serialized_options = b'\210\002\001\222A[\022\rEvaluateRules\032JEvaluates the provided rules and generate annotations based on the result.\202\323\344\223\002\033\"\026/api/v1/rules/evaluate:\001*' _globals['_RULESERVICE'].methods_by_name['ViewHumanFriendlyRules']._loaded_options = None @@ -378,142 +412,158 @@ _globals['_RULESERVICE'].methods_by_name['GetRuleVersion']._serialized_options = b'\222A9\022\016GetRuleVersion\032\'Retrieves a specific version of a rule.\202\323\344\223\002\027\022\025/api/v1/rules:version' _globals['_RULESERVICE'].methods_by_name['BatchGetRuleVersions']._loaded_options = None _globals['_RULESERVICE'].methods_by_name['BatchGetRuleVersions']._serialized_options = b'\222AB\022\024BatchGetRuleVersions\032*Retrieves multiple rules by rule versions.\202\323\344\223\002#\"\036/api/v1/rules:batchGetVersions:\001*' - _globals['_SEARCHORDER']._serialized_start=11761 - _globals['_SEARCHORDER']._serialized_end=11853 - _globals['_ACTIONKIND']._serialized_start=11855 - _globals['_ACTIONKIND']._serialized_end=11947 - _globals['_CONDITIONCOMPARATOR']._serialized_start=11950 - _globals['_CONDITIONCOMPARATOR']._serialized_end=12123 + _globals['_SEARCHORDER']._serialized_start=12503 + _globals['_SEARCHORDER']._serialized_end=12595 + _globals['_ACTIONKIND']._serialized_start=12597 + _globals['_ACTIONKIND']._serialized_end=12689 + _globals['_CONDITIONCOMPARATOR']._serialized_start=12692 + _globals['_CONDITIONCOMPARATOR']._serialized_end=12865 _globals['_RULE']._serialized_start=358 - _globals['_RULE']._serialized_end=1345 - _globals['_RULECONDITION']._serialized_start=1348 - _globals['_RULECONDITION']._serialized_end=1887 - _globals['_RULEACTION']._serialized_start=1890 - _globals['_RULEACTION']._serialized_end=2440 - _globals['_RULEASSETCONFIGURATION']._serialized_start=2442 - _globals['_RULEASSETCONFIGURATION']._serialized_end=2520 - _globals['_CONTEXTUALCHANNELS']._serialized_start=2522 - _globals['_CONTEXTUALCHANNELS']._serialized_end=2608 - _globals['_ASSETEXPRESSIONVALIDATIONRESULT']._serialized_start=2611 - _globals['_ASSETEXPRESSIONVALIDATIONRESULT']._serialized_end=2793 - _globals['_SEARCHRULESREQUEST']._serialized_start=2796 - _globals['_SEARCHRULESREQUEST']._serialized_end=3250 - _globals['_SEARCHRULESRESPONSE']._serialized_start=3252 - _globals['_SEARCHRULESRESPONSE']._serialized_end=3348 - _globals['_GETRULEREQUEST']._serialized_start=3350 - _globals['_GETRULEREQUEST']._serialized_end=3432 - _globals['_GETRULERESPONSE']._serialized_start=3434 - _globals['_GETRULERESPONSE']._serialized_end=3497 - _globals['_BATCHGETRULESREQUEST']._serialized_start=3499 - _globals['_BATCHGETRULESREQUEST']._serialized_end=3591 - _globals['_BATCHGETRULESRESPONSE']._serialized_start=3593 - _globals['_BATCHGETRULESRESPONSE']._serialized_end=3664 - _globals['_CREATERULEREQUEST']._serialized_start=3666 - _globals['_CREATERULEREQUEST']._serialized_end=3748 - _globals['_CREATERULERESPONSE']._serialized_start=3750 - _globals['_CREATERULERESPONSE']._serialized_end=3800 - _globals['_UPDATERULEREQUEST']._serialized_start=3803 - _globals['_UPDATERULEREQUEST']._serialized_end=4476 - _globals['_UPDATECONDITIONREQUEST']._serialized_start=4479 - _globals['_UPDATECONDITIONREQUEST']._serialized_end=4724 - _globals['_UPDATEACTIONREQUEST']._serialized_start=4727 - _globals['_UPDATEACTIONREQUEST']._serialized_end=4958 - _globals['_UPDATERULERESPONSE']._serialized_start=4960 - _globals['_UPDATERULERESPONSE']._serialized_end=5010 - _globals['_VALIDATIONRESULT']._serialized_start=5013 - _globals['_VALIDATIONRESULT']._serialized_end=5272 - _globals['_BATCHUPDATERULESREQUEST']._serialized_start=5275 - _globals['_BATCHUPDATERULESREQUEST']._serialized_end=5478 - _globals['_BATCHUPDATERULESRESPONSE']._serialized_start=5481 - _globals['_BATCHUPDATERULESRESPONSE']._serialized_end=6022 - _globals['_BATCHUPDATERULESRESPONSE_RULEIDENTIFIERS']._serialized_start=5894 - _globals['_BATCHUPDATERULESRESPONSE_RULEIDENTIFIERS']._serialized_end=6022 - _globals['_DELETERULEREQUEST']._serialized_start=6024 - _globals['_DELETERULEREQUEST']._serialized_end=6109 - _globals['_DELETERULERESPONSE']._serialized_start=6111 - _globals['_DELETERULERESPONSE']._serialized_end=6131 - _globals['_BATCHDELETERULESREQUEST']._serialized_start=6133 - _globals['_BATCHDELETERULESREQUEST']._serialized_end=6228 - _globals['_BATCHDELETERULESRESPONSE']._serialized_start=6230 - _globals['_BATCHDELETERULESRESPONSE']._serialized_end=6256 - _globals['_UNDELETERULEREQUEST']._serialized_start=6258 - _globals['_UNDELETERULEREQUEST']._serialized_end=6345 - _globals['_UNDELETERULERESPONSE']._serialized_start=6347 - _globals['_UNDELETERULERESPONSE']._serialized_end=6369 - _globals['_BATCHUNDELETERULESREQUEST']._serialized_start=6371 - _globals['_BATCHUNDELETERULESREQUEST']._serialized_end=6468 - _globals['_BATCHUNDELETERULESRESPONSE']._serialized_start=6470 - _globals['_BATCHUNDELETERULESRESPONSE']._serialized_end=6498 - _globals['_VIEWHUMANFRIENDLYRULESREQUEST']._serialized_start=6500 - _globals['_VIEWHUMANFRIENDLYRULESREQUEST']._serialized_end=6567 - _globals['_VIEWHUMANFRIENDLYRULESRESPONSE']._serialized_start=6569 - _globals['_VIEWHUMANFRIENDLYRULESRESPONSE']._serialized_end=6641 - _globals['_UPDATEHUMANFRIENDLYRULESREQUEST']._serialized_start=6644 - _globals['_UPDATEHUMANFRIENDLYRULESREQUEST']._serialized_end=6795 - _globals['_UPDATEHUMANFRIENDLYRULESRESPONSE']._serialized_start=6798 - _globals['_UPDATEHUMANFRIENDLYRULESRESPONSE']._serialized_end=6938 - _globals['_VIEWJSONRULESREQUEST']._serialized_start=6940 - _globals['_VIEWJSONRULESREQUEST']._serialized_end=6994 - _globals['_VIEWJSONRULESRESPONSE']._serialized_start=6996 - _globals['_VIEWJSONRULESRESPONSE']._serialized_end=7055 - _globals['_JSONRULESREQUEST']._serialized_start=7058 - _globals['_JSONRULESREQUEST']._serialized_end=7190 - _globals['_JSONRULESRESPONSE']._serialized_start=7193 - _globals['_JSONRULESRESPONSE']._serialized_end=7514 - _globals['_VALIDATEJSONRULESREQUEST']._serialized_start=7516 - _globals['_VALIDATEJSONRULESREQUEST']._serialized_end=7606 - _globals['_VALIDATEJSONRULESRESPONSE']._serialized_start=7608 - _globals['_VALIDATEJSONRULESRESPONSE']._serialized_end=7702 - _globals['_UPDATEJSONRULESREQUEST']._serialized_start=7704 - _globals['_UPDATEJSONRULESREQUEST']._serialized_end=7792 - _globals['_UPDATEJSONRULESRESPONSE']._serialized_start=7794 - _globals['_UPDATEJSONRULESRESPONSE']._serialized_end=7886 - _globals['_LISTRULESREQUEST']._serialized_start=7889 - _globals['_LISTRULESREQUEST']._serialized_end=8038 - _globals['_LISTRULESRESPONSE']._serialized_start=8040 - _globals['_LISTRULESRESPONSE']._serialized_end=8147 - _globals['_LISTRULEVERSIONSREQUEST']._serialized_start=8150 - _globals['_LISTRULEVERSIONSREQUEST']._serialized_end=8299 - _globals['_RULEVERSION']._serialized_start=8302 - _globals['_RULEVERSION']._serialized_end=8712 - _globals['_LISTRULEVERSIONSRESPONSE']._serialized_start=8715 - _globals['_LISTRULEVERSIONSRESPONSE']._serialized_end=8851 - _globals['_GETRULEVERSIONREQUEST']._serialized_start=8853 - _globals['_GETRULEVERSIONREQUEST']._serialized_end=8921 - _globals['_GETRULEVERSIONRESPONSE']._serialized_start=8923 - _globals['_GETRULEVERSIONRESPONSE']._serialized_end=8993 - _globals['_BATCHGETRULEVERSIONSREQUEST']._serialized_start=8995 - _globals['_BATCHGETRULEVERSIONSREQUEST']._serialized_end=9071 - _globals['_BATCHGETRULEVERSIONSRESPONSE']._serialized_start=9073 - _globals['_BATCHGETRULEVERSIONSRESPONSE']._serialized_end=9151 - _globals['_RULECONDITIONEXPRESSION']._serialized_start=9154 - _globals['_RULECONDITIONEXPRESSION']._serialized_end=9398 - _globals['_SINGLECHANNELCOMPARISONEXPRESSION']._serialized_start=9401 - _globals['_SINGLECHANNELCOMPARISONEXPRESSION']._serialized_end=9732 - _globals['_LASTVALUETHRESHOLD']._serialized_start=9734 - _globals['_LASTVALUETHRESHOLD']._serialized_end=9754 - _globals['_CALCULATEDCHANNELCONFIG']._serialized_start=9757 - _globals['_CALCULATEDCHANNELCONFIG']._serialized_end=10136 - _globals['_CALCULATEDCHANNELCONFIG_CHANNELREFERENCESENTRY']._serialized_start=10035 - _globals['_CALCULATEDCHANNELCONFIG_CHANNELREFERENCESENTRY']._serialized_end=10136 - _globals['_CHANNELREFERENCE']._serialized_start=10138 - _globals['_CHANNELREFERENCE']._serialized_end=10216 - _globals['_RULEACTIONCONFIGURATION']._serialized_start=10219 - _globals['_RULEACTIONCONFIGURATION']._serialized_end=10427 - _globals['_NOTIFICATIONACTIONCONFIGURATION']._serialized_start=10429 - _globals['_NOTIFICATIONACTIONCONFIGURATION']._serialized_end=10513 - _globals['_ANNOTATIONACTIONCONFIGURATION']._serialized_start=10516 - _globals['_ANNOTATIONACTIONCONFIGURATION']._serialized_end=10792 - _globals['_EVALUATERULESREQUEST']._serialized_start=10795 - _globals['_EVALUATERULESREQUEST']._serialized_end=11065 - _globals['_EVALUATEDANNOTATIONOPTIONS']._serialized_start=11067 - _globals['_EVALUATEDANNOTATIONOPTIONS']._serialized_end=11120 - _globals['_TIMERANGEQUERY']._serialized_start=11123 - _globals['_TIMERANGEQUERY']._serialized_end=11253 - _globals['_EVALUATERULESRESPONSE']._serialized_start=11256 - _globals['_EVALUATERULESRESPONSE']._serialized_end=11509 - _globals['_DRYRUNANNOTATION']._serialized_start=11512 - _globals['_DRYRUNANNOTATION']._serialized_end=11759 - _globals['_RULESERVICE']._serialized_start=12126 - _globals['_RULESERVICE']._serialized_end=16302 + _globals['_RULE']._serialized_end=1455 + _globals['_RULECONDITION']._serialized_start=1458 + _globals['_RULECONDITION']._serialized_end=1997 + _globals['_RULEACTION']._serialized_start=2000 + _globals['_RULEACTION']._serialized_end=2550 + _globals['_RULEASSETCONFIGURATION']._serialized_start=2552 + _globals['_RULEASSETCONFIGURATION']._serialized_end=2630 + _globals['_CONTEXTUALCHANNELS']._serialized_start=2632 + _globals['_CONTEXTUALCHANNELS']._serialized_end=2718 + _globals['_ASSETEXPRESSIONVALIDATIONRESULT']._serialized_start=2721 + _globals['_ASSETEXPRESSIONVALIDATIONRESULT']._serialized_end=2903 + _globals['_SEARCHRULESREQUEST']._serialized_start=2906 + _globals['_SEARCHRULESREQUEST']._serialized_end=3360 + _globals['_SEARCHRULESRESPONSE']._serialized_start=3362 + _globals['_SEARCHRULESRESPONSE']._serialized_end=3458 + _globals['_GETRULEREQUEST']._serialized_start=3460 + _globals['_GETRULEREQUEST']._serialized_end=3542 + _globals['_GETRULERESPONSE']._serialized_start=3544 + _globals['_GETRULERESPONSE']._serialized_end=3607 + _globals['_BATCHGETRULESREQUEST']._serialized_start=3609 + _globals['_BATCHGETRULESREQUEST']._serialized_end=3701 + _globals['_BATCHGETRULESRESPONSE']._serialized_start=3703 + _globals['_BATCHGETRULESRESPONSE']._serialized_end=3774 + _globals['_CREATERULEREQUEST']._serialized_start=3776 + _globals['_CREATERULEREQUEST']._serialized_end=3858 + _globals['_CREATERULERESPONSE']._serialized_start=3860 + _globals['_CREATERULERESPONSE']._serialized_end=3910 + _globals['_UPDATERULEREQUEST']._serialized_start=3913 + _globals['_UPDATERULEREQUEST']._serialized_end=4624 + _globals['_UPDATECONDITIONREQUEST']._serialized_start=4627 + _globals['_UPDATECONDITIONREQUEST']._serialized_end=4872 + _globals['_UPDATEACTIONREQUEST']._serialized_start=4875 + _globals['_UPDATEACTIONREQUEST']._serialized_end=5106 + _globals['_UPDATERULERESPONSE']._serialized_start=5108 + _globals['_UPDATERULERESPONSE']._serialized_end=5158 + _globals['_VALIDATIONRESULT']._serialized_start=5161 + _globals['_VALIDATIONRESULT']._serialized_end=5420 + _globals['_BATCHUPDATERULESREQUEST']._serialized_start=5423 + _globals['_BATCHUPDATERULESREQUEST']._serialized_end=5626 + _globals['_BATCHUPDATERULESRESPONSE']._serialized_start=5629 + _globals['_BATCHUPDATERULESRESPONSE']._serialized_end=6170 + _globals['_BATCHUPDATERULESRESPONSE_RULEIDENTIFIERS']._serialized_start=6042 + _globals['_BATCHUPDATERULESRESPONSE_RULEIDENTIFIERS']._serialized_end=6170 + _globals['_DELETERULEREQUEST']._serialized_start=6172 + _globals['_DELETERULEREQUEST']._serialized_end=6257 + _globals['_DELETERULERESPONSE']._serialized_start=6259 + _globals['_DELETERULERESPONSE']._serialized_end=6279 + _globals['_BATCHDELETERULESREQUEST']._serialized_start=6281 + _globals['_BATCHDELETERULESREQUEST']._serialized_end=6376 + _globals['_BATCHDELETERULESRESPONSE']._serialized_start=6378 + _globals['_BATCHDELETERULESRESPONSE']._serialized_end=6404 + _globals['_ARCHIVERULEREQUEST']._serialized_start=6406 + _globals['_ARCHIVERULEREQUEST']._serialized_end=6492 + _globals['_ARCHIVERULERESPONSE']._serialized_start=6494 + _globals['_ARCHIVERULERESPONSE']._serialized_end=6515 + _globals['_BATCHARCHIVERULESREQUEST']._serialized_start=6517 + _globals['_BATCHARCHIVERULESREQUEST']._serialized_end=6613 + _globals['_BATCHARCHIVERULESRESPONSE']._serialized_start=6615 + _globals['_BATCHARCHIVERULESRESPONSE']._serialized_end=6642 + _globals['_UNDELETERULEREQUEST']._serialized_start=6644 + _globals['_UNDELETERULEREQUEST']._serialized_end=6731 + _globals['_UNDELETERULERESPONSE']._serialized_start=6733 + _globals['_UNDELETERULERESPONSE']._serialized_end=6755 + _globals['_BATCHUNDELETERULESREQUEST']._serialized_start=6757 + _globals['_BATCHUNDELETERULESREQUEST']._serialized_end=6854 + _globals['_BATCHUNDELETERULESRESPONSE']._serialized_start=6856 + _globals['_BATCHUNDELETERULESRESPONSE']._serialized_end=6884 + _globals['_UNARCHIVERULEREQUEST']._serialized_start=6886 + _globals['_UNARCHIVERULEREQUEST']._serialized_end=6974 + _globals['_UNARCHIVERULERESPONSE']._serialized_start=6976 + _globals['_UNARCHIVERULERESPONSE']._serialized_end=6999 + _globals['_BATCHUNARCHIVERULESREQUEST']._serialized_start=7001 + _globals['_BATCHUNARCHIVERULESREQUEST']._serialized_end=7099 + _globals['_BATCHUNARCHIVERULESRESPONSE']._serialized_start=7101 + _globals['_BATCHUNARCHIVERULESRESPONSE']._serialized_end=7130 + _globals['_VIEWHUMANFRIENDLYRULESREQUEST']._serialized_start=7132 + _globals['_VIEWHUMANFRIENDLYRULESREQUEST']._serialized_end=7199 + _globals['_VIEWHUMANFRIENDLYRULESRESPONSE']._serialized_start=7201 + _globals['_VIEWHUMANFRIENDLYRULESRESPONSE']._serialized_end=7273 + _globals['_UPDATEHUMANFRIENDLYRULESREQUEST']._serialized_start=7276 + _globals['_UPDATEHUMANFRIENDLYRULESREQUEST']._serialized_end=7427 + _globals['_UPDATEHUMANFRIENDLYRULESRESPONSE']._serialized_start=7430 + _globals['_UPDATEHUMANFRIENDLYRULESRESPONSE']._serialized_end=7570 + _globals['_VIEWJSONRULESREQUEST']._serialized_start=7572 + _globals['_VIEWJSONRULESREQUEST']._serialized_end=7626 + _globals['_VIEWJSONRULESRESPONSE']._serialized_start=7628 + _globals['_VIEWJSONRULESRESPONSE']._serialized_end=7687 + _globals['_JSONRULESREQUEST']._serialized_start=7690 + _globals['_JSONRULESREQUEST']._serialized_end=7822 + _globals['_JSONRULESRESPONSE']._serialized_start=7825 + _globals['_JSONRULESRESPONSE']._serialized_end=8146 + _globals['_VALIDATEJSONRULESREQUEST']._serialized_start=8148 + _globals['_VALIDATEJSONRULESREQUEST']._serialized_end=8238 + _globals['_VALIDATEJSONRULESRESPONSE']._serialized_start=8240 + _globals['_VALIDATEJSONRULESRESPONSE']._serialized_end=8334 + _globals['_UPDATEJSONRULESREQUEST']._serialized_start=8336 + _globals['_UPDATEJSONRULESREQUEST']._serialized_end=8424 + _globals['_UPDATEJSONRULESRESPONSE']._serialized_start=8426 + _globals['_UPDATEJSONRULESRESPONSE']._serialized_end=8518 + _globals['_LISTRULESREQUEST']._serialized_start=8521 + _globals['_LISTRULESREQUEST']._serialized_end=8670 + _globals['_LISTRULESRESPONSE']._serialized_start=8672 + _globals['_LISTRULESRESPONSE']._serialized_end=8779 + _globals['_LISTRULEVERSIONSREQUEST']._serialized_start=8782 + _globals['_LISTRULEVERSIONSREQUEST']._serialized_end=8931 + _globals['_RULEVERSION']._serialized_start=8934 + _globals['_RULEVERSION']._serialized_end=9454 + _globals['_LISTRULEVERSIONSRESPONSE']._serialized_start=9457 + _globals['_LISTRULEVERSIONSRESPONSE']._serialized_end=9593 + _globals['_GETRULEVERSIONREQUEST']._serialized_start=9595 + _globals['_GETRULEVERSIONREQUEST']._serialized_end=9663 + _globals['_GETRULEVERSIONRESPONSE']._serialized_start=9665 + _globals['_GETRULEVERSIONRESPONSE']._serialized_end=9735 + _globals['_BATCHGETRULEVERSIONSREQUEST']._serialized_start=9737 + _globals['_BATCHGETRULEVERSIONSREQUEST']._serialized_end=9813 + _globals['_BATCHGETRULEVERSIONSRESPONSE']._serialized_start=9815 + _globals['_BATCHGETRULEVERSIONSRESPONSE']._serialized_end=9893 + _globals['_RULECONDITIONEXPRESSION']._serialized_start=9896 + _globals['_RULECONDITIONEXPRESSION']._serialized_end=10140 + _globals['_SINGLECHANNELCOMPARISONEXPRESSION']._serialized_start=10143 + _globals['_SINGLECHANNELCOMPARISONEXPRESSION']._serialized_end=10474 + _globals['_LASTVALUETHRESHOLD']._serialized_start=10476 + _globals['_LASTVALUETHRESHOLD']._serialized_end=10496 + _globals['_CALCULATEDCHANNELCONFIG']._serialized_start=10499 + _globals['_CALCULATEDCHANNELCONFIG']._serialized_end=10878 + _globals['_CALCULATEDCHANNELCONFIG_CHANNELREFERENCESENTRY']._serialized_start=10777 + _globals['_CALCULATEDCHANNELCONFIG_CHANNELREFERENCESENTRY']._serialized_end=10878 + _globals['_CHANNELREFERENCE']._serialized_start=10880 + _globals['_CHANNELREFERENCE']._serialized_end=10958 + _globals['_RULEACTIONCONFIGURATION']._serialized_start=10961 + _globals['_RULEACTIONCONFIGURATION']._serialized_end=11169 + _globals['_NOTIFICATIONACTIONCONFIGURATION']._serialized_start=11171 + _globals['_NOTIFICATIONACTIONCONFIGURATION']._serialized_end=11255 + _globals['_ANNOTATIONACTIONCONFIGURATION']._serialized_start=11258 + _globals['_ANNOTATIONACTIONCONFIGURATION']._serialized_end=11534 + _globals['_EVALUATERULESREQUEST']._serialized_start=11537 + _globals['_EVALUATERULESREQUEST']._serialized_end=11807 + _globals['_EVALUATEDANNOTATIONOPTIONS']._serialized_start=11809 + _globals['_EVALUATEDANNOTATIONOPTIONS']._serialized_end=11862 + _globals['_TIMERANGEQUERY']._serialized_start=11865 + _globals['_TIMERANGEQUERY']._serialized_end=11995 + _globals['_EVALUATERULESRESPONSE']._serialized_start=11998 + _globals['_EVALUATERULESRESPONSE']._serialized_end=12251 + _globals['_DRYRUNANNOTATION']._serialized_start=12254 + _globals['_DRYRUNANNOTATION']._serialized_end=12501 + _globals['_RULESERVICE']._serialized_start=12868 + _globals['_RULESERVICE']._serialized_end=17891 # @@protoc_insertion_point(module_scope) diff --git a/python/lib/sift/rules/v1/rules_pb2.pyi b/python/lib/sift/rules/v1/rules_pb2.pyi index 0a577483c..1210d7544 100644 --- a/python/lib/sift/rules/v1/rules_pb2.pyi +++ b/python/lib/sift/rules/v1/rules_pb2.pyi @@ -114,6 +114,8 @@ class Rule(google.protobuf.message.Message): DELETED_DATE_FIELD_NUMBER: builtins.int IS_EXTERNAL_FIELD_NUMBER: builtins.int METADATA_FIELD_NUMBER: builtins.int + ARCHIVED_DATE_FIELD_NUMBER: builtins.int + IS_ARCHIVED_FIELD_NUMBER: builtins.int rule_id: builtins.str asset_id: builtins.str name: builtins.str @@ -125,6 +127,8 @@ class Rule(google.protobuf.message.Message): client_key: builtins.str """client_key is a client provided identifier for the rule. It is immutable after rule creation.""" is_external: builtins.bool + is_archived: builtins.bool + """is_archived is inferred from when archived_date is not null""" @property def created_date(self) -> google.protobuf.timestamp_pb2.Timestamp: ... @property @@ -141,6 +145,10 @@ class Rule(google.protobuf.message.Message): def deleted_date(self) -> google.protobuf.timestamp_pb2.Timestamp: ... @property def metadata(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[sift.metadata.v1.metadata_pb2.MetadataValue]: ... + @property + def archived_date(self) -> google.protobuf.timestamp_pb2.Timestamp: + """archived_date indicates when the rule was archived""" + def __init__( self, *, @@ -162,9 +170,11 @@ class Rule(google.protobuf.message.Message): deleted_date: google.protobuf.timestamp_pb2.Timestamp | None = ..., is_external: builtins.bool = ..., metadata: collections.abc.Iterable[sift.metadata.v1.metadata_pb2.MetadataValue] | None = ..., + archived_date: google.protobuf.timestamp_pb2.Timestamp | None = ..., + is_archived: builtins.bool = ..., ) -> None: ... - def HasField(self, field_name: typing.Literal["asset_configuration", b"asset_configuration", "contextual_channels", b"contextual_channels", "created_date", b"created_date", "deleted_date", b"deleted_date", "modified_date", b"modified_date", "rule_version", b"rule_version"]) -> builtins.bool: ... - def ClearField(self, field_name: typing.Literal["asset_configuration", b"asset_configuration", "asset_id", b"asset_id", "client_key", b"client_key", "conditions", b"conditions", "contextual_channels", b"contextual_channels", "created_by_user_id", b"created_by_user_id", "created_date", b"created_date", "deleted_date", b"deleted_date", "description", b"description", "is_enabled", b"is_enabled", "is_external", b"is_external", "metadata", b"metadata", "modified_by_user_id", b"modified_by_user_id", "modified_date", b"modified_date", "name", b"name", "organization_id", b"organization_id", "rule_id", b"rule_id", "rule_version", b"rule_version"]) -> None: ... + def HasField(self, field_name: typing.Literal["archived_date", b"archived_date", "asset_configuration", b"asset_configuration", "contextual_channels", b"contextual_channels", "created_date", b"created_date", "deleted_date", b"deleted_date", "modified_date", b"modified_date", "rule_version", b"rule_version"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["archived_date", b"archived_date", "asset_configuration", b"asset_configuration", "asset_id", b"asset_id", "client_key", b"client_key", "conditions", b"conditions", "contextual_channels", b"contextual_channels", "created_by_user_id", b"created_by_user_id", "created_date", b"created_date", "deleted_date", b"deleted_date", "description", b"description", "is_archived", b"is_archived", "is_enabled", b"is_enabled", "is_external", b"is_external", "metadata", b"metadata", "modified_by_user_id", b"modified_by_user_id", "modified_date", b"modified_date", "name", b"name", "organization_id", b"organization_id", "rule_id", b"rule_id", "rule_version", b"rule_version"]) -> None: ... global___Rule = Rule @@ -529,6 +539,7 @@ class UpdateRuleRequest(google.protobuf.message.Message): CONTEXTUAL_CHANNELS_FIELD_NUMBER: builtins.int IS_EXTERNAL_FIELD_NUMBER: builtins.int METADATA_FIELD_NUMBER: builtins.int + IS_ARCHIVED_FIELD_NUMBER: builtins.int rule_id: builtins.str name: builtins.str description: builtins.str @@ -541,6 +552,7 @@ class UpdateRuleRequest(google.protobuf.message.Message): client_key: builtins.str """client_key is a client provided identifier for the rule. It is immutable after being set""" is_external: builtins.bool + is_archived: builtins.bool @property def conditions(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___UpdateConditionRequest]: ... @property @@ -565,9 +577,10 @@ class UpdateRuleRequest(google.protobuf.message.Message): contextual_channels: global___ContextualChannels | None = ..., is_external: builtins.bool = ..., metadata: collections.abc.Iterable[sift.metadata.v1.metadata_pb2.MetadataValue] | None = ..., + is_archived: builtins.bool = ..., ) -> None: ... def HasField(self, field_name: typing.Literal["_client_key", b"_client_key", "_rule_id", b"_rule_id", "asset_configuration", b"asset_configuration", "client_key", b"client_key", "contextual_channels", b"contextual_channels", "rule_id", b"rule_id"]) -> builtins.bool: ... - def ClearField(self, field_name: typing.Literal["_client_key", b"_client_key", "_rule_id", b"_rule_id", "asset_configuration", b"asset_configuration", "asset_id", b"asset_id", "client_key", b"client_key", "conditions", b"conditions", "contextual_channels", b"contextual_channels", "description", b"description", "is_enabled", b"is_enabled", "is_external", b"is_external", "metadata", b"metadata", "name", b"name", "organization_id", b"organization_id", "rule_id", b"rule_id", "version_notes", b"version_notes"]) -> None: ... + def ClearField(self, field_name: typing.Literal["_client_key", b"_client_key", "_rule_id", b"_rule_id", "asset_configuration", b"asset_configuration", "asset_id", b"asset_id", "client_key", b"client_key", "conditions", b"conditions", "contextual_channels", b"contextual_channels", "description", b"description", "is_archived", b"is_archived", "is_enabled", b"is_enabled", "is_external", b"is_external", "metadata", b"metadata", "name", b"name", "organization_id", b"organization_id", "rule_id", b"rule_id", "version_notes", b"version_notes"]) -> None: ... @typing.overload def WhichOneof(self, oneof_group: typing.Literal["_client_key", b"_client_key"]) -> typing.Literal["client_key"] | None: ... @typing.overload @@ -823,6 +836,68 @@ class BatchDeleteRulesResponse(google.protobuf.message.Message): global___BatchDeleteRulesResponse = BatchDeleteRulesResponse +@typing.final +class ArchiveRuleRequest(google.protobuf.message.Message): + """ArchiveRuleRequest is used to archive a rule by rule_id or client_key. If both are provided, only rule_id will be used.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RULE_ID_FIELD_NUMBER: builtins.int + CLIENT_KEY_FIELD_NUMBER: builtins.int + rule_id: builtins.str + client_key: builtins.str + def __init__( + self, + *, + rule_id: builtins.str = ..., + client_key: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["client_key", b"client_key", "rule_id", b"rule_id"]) -> None: ... + +global___ArchiveRuleRequest = ArchiveRuleRequest + +@typing.final +class ArchiveRuleResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___ArchiveRuleResponse = ArchiveRuleResponse + +@typing.final +class BatchArchiveRulesRequest(google.protobuf.message.Message): + """BatchArchiveRulesRequest is used to archive a rule by rule_id or client_key. For each rule if both are provided, only rule_id will be used.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RULE_IDS_FIELD_NUMBER: builtins.int + CLIENT_KEYS_FIELD_NUMBER: builtins.int + @property + def rule_ids(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + @property + def client_keys(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + def __init__( + self, + *, + rule_ids: collections.abc.Iterable[builtins.str] | None = ..., + client_keys: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["client_keys", b"client_keys", "rule_ids", b"rule_ids"]) -> None: ... + +global___BatchArchiveRulesRequest = BatchArchiveRulesRequest + +@typing.final +class BatchArchiveRulesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___BatchArchiveRulesResponse = BatchArchiveRulesResponse + @typing.final class UndeleteRuleRequest(google.protobuf.message.Message): """UndeleteRuleRequest is used to undelete a rule by rule_id or client_key. If both are provided, only rule_id will be used.""" @@ -885,6 +960,68 @@ class BatchUndeleteRulesResponse(google.protobuf.message.Message): global___BatchUndeleteRulesResponse = BatchUndeleteRulesResponse +@typing.final +class UnarchiveRuleRequest(google.protobuf.message.Message): + """UnarchiveRuleRequest is used to unarchive a rule by rule_id or client_key. If both are provided, only rule_id will be used.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RULE_ID_FIELD_NUMBER: builtins.int + CLIENT_KEY_FIELD_NUMBER: builtins.int + rule_id: builtins.str + client_key: builtins.str + def __init__( + self, + *, + rule_id: builtins.str = ..., + client_key: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["client_key", b"client_key", "rule_id", b"rule_id"]) -> None: ... + +global___UnarchiveRuleRequest = UnarchiveRuleRequest + +@typing.final +class UnarchiveRuleResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___UnarchiveRuleResponse = UnarchiveRuleResponse + +@typing.final +class BatchUnarchiveRulesRequest(google.protobuf.message.Message): + """BatchUnarchiveRulesRequest is used to unarchive a rule by rule_id or client_key. For each rule if both are provided, only rule_id will be used.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RULE_IDS_FIELD_NUMBER: builtins.int + CLIENT_KEYS_FIELD_NUMBER: builtins.int + @property + def rule_ids(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + @property + def client_keys(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + def __init__( + self, + *, + rule_ids: collections.abc.Iterable[builtins.str] | None = ..., + client_keys: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["client_keys", b"client_keys", "rule_ids", b"rule_ids"]) -> None: ... + +global___BatchUnarchiveRulesRequest = BatchUnarchiveRulesRequest + +@typing.final +class BatchUnarchiveRulesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___BatchUnarchiveRulesResponse = BatchUnarchiveRulesResponse + @typing.final class ViewHumanFriendlyRulesRequest(google.protobuf.message.Message): """Deprecated - use ViewJsonRulesRequest.""" @@ -1138,7 +1275,8 @@ class ListRulesRequest(google.protobuf.message.Message): """ filter: builtins.str """A [Common Expression Language (CEL)](https://github.com/google/cel-spec) filter string. - Available fields to filter by are `rule_id`, `client_key`, `name`, `description`, `asset_id`, `tag_id`, and `deleted_date`. + Available fields to filter by are `rule_id`, `client_key`, `name`, `description`, `is_external`, `asset_id`, `tag_id`, + `created_date`, `created_by_user_id`, `modified_date`, `modified_by_user_id`, `deleted_date`, `is_archived`, and `archived_date`. For further information about how to use CELs, please refer to [this guide](https://github.com/google/cel-spec/blob/master/doc/langdef.md#standard-definitions). Optional. """ @@ -1228,16 +1366,24 @@ class RuleVersion(google.protobuf.message.Message): VERSION_NOTES_FIELD_NUMBER: builtins.int GENERATED_CHANGE_MESSAGE_FIELD_NUMBER: builtins.int DELETED_DATE_FIELD_NUMBER: builtins.int + ARCHIVED_DATE_FIELD_NUMBER: builtins.int + IS_ARCHIVED_FIELD_NUMBER: builtins.int rule_id: builtins.str rule_version_id: builtins.str version: builtins.str created_by_user_id: builtins.str version_notes: builtins.str generated_change_message: builtins.str + is_archived: builtins.bool + """is_archived is inferred from when archived_date is not null""" @property def created_date(self) -> google.protobuf.timestamp_pb2.Timestamp: ... @property def deleted_date(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + @property + def archived_date(self) -> google.protobuf.timestamp_pb2.Timestamp: + """archived_date indicates when the rule version was archived""" + def __init__( self, *, @@ -1249,9 +1395,11 @@ class RuleVersion(google.protobuf.message.Message): version_notes: builtins.str = ..., generated_change_message: builtins.str = ..., deleted_date: google.protobuf.timestamp_pb2.Timestamp | None = ..., + archived_date: google.protobuf.timestamp_pb2.Timestamp | None = ..., + is_archived: builtins.bool = ..., ) -> None: ... - def HasField(self, field_name: typing.Literal["created_date", b"created_date", "deleted_date", b"deleted_date"]) -> builtins.bool: ... - def ClearField(self, field_name: typing.Literal["created_by_user_id", b"created_by_user_id", "created_date", b"created_date", "deleted_date", b"deleted_date", "generated_change_message", b"generated_change_message", "rule_id", b"rule_id", "rule_version_id", b"rule_version_id", "version", b"version", "version_notes", b"version_notes"]) -> None: ... + def HasField(self, field_name: typing.Literal["archived_date", b"archived_date", "created_date", b"created_date", "deleted_date", b"deleted_date"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["archived_date", b"archived_date", "created_by_user_id", b"created_by_user_id", "created_date", b"created_date", "deleted_date", b"deleted_date", "generated_change_message", b"generated_change_message", "is_archived", b"is_archived", "rule_id", b"rule_id", "rule_version_id", b"rule_version_id", "version", b"version", "version_notes", b"version_notes"]) -> None: ... global___RuleVersion = RuleVersion diff --git a/python/lib/sift/rules/v1/rules_pb2_grpc.py b/python/lib/sift/rules/v1/rules_pb2_grpc.py index b54c7e70a..f1a610ca3 100644 --- a/python/lib/sift/rules/v1/rules_pb2_grpc.py +++ b/python/lib/sift/rules/v1/rules_pb2_grpc.py @@ -49,11 +49,31 @@ def __init__(self, channel): request_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.DeleteRuleRequest.SerializeToString, response_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.DeleteRuleResponse.FromString, ) + self.ArchiveRule = channel.unary_unary( + '/sift.rules.v1.RuleService/ArchiveRule', + request_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.ArchiveRuleRequest.SerializeToString, + response_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.ArchiveRuleResponse.FromString, + ) self.BatchDeleteRules = channel.unary_unary( '/sift.rules.v1.RuleService/BatchDeleteRules', request_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.BatchDeleteRulesRequest.SerializeToString, response_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.BatchDeleteRulesResponse.FromString, ) + self.BatchArchiveRules = channel.unary_unary( + '/sift.rules.v1.RuleService/BatchArchiveRules', + request_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.BatchArchiveRulesRequest.SerializeToString, + response_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.BatchArchiveRulesResponse.FromString, + ) + self.UnarchiveRule = channel.unary_unary( + '/sift.rules.v1.RuleService/UnarchiveRule', + request_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.UnarchiveRuleRequest.SerializeToString, + response_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.UnarchiveRuleResponse.FromString, + ) + self.BatchUnarchiveRules = channel.unary_unary( + '/sift.rules.v1.RuleService/BatchUnarchiveRules', + request_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.BatchUnarchiveRulesRequest.SerializeToString, + response_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.BatchUnarchiveRulesResponse.FromString, + ) self.UndeleteRule = channel.unary_unary( '/sift.rules.v1.RuleService/UndeleteRule', request_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.UndeleteRuleRequest.SerializeToString, @@ -162,28 +182,60 @@ def BatchUpdateRules(self, request, context): raise NotImplementedError('Method not implemented!') def DeleteRule(self, request, context): - """Deletes a rule + """Deprecated - use ArchiveRule instead. + Archives a rule. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ArchiveRule(self, request, context): + """Archives a rule. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') def BatchDeleteRules(self, request, context): - """Deletes multiple rules + """Deprecated - use BatchArchiveRules instead. + Archives multiple rules. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def BatchArchiveRules(self, request, context): + """Batch archives rules. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UnarchiveRule(self, request, context): + """Unarchives a rule. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def BatchUnarchiveRules(self, request, context): + """Batch unarchives rules. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') def UndeleteRule(self, request, context): - """Undeletes a rule + """Deprecated - use UnarchiveRule instead. + Unarchives a rule """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') def BatchUndeleteRules(self, request, context): - """Undeletes multiple rules + """Deprecated - use BatchUnarchiveRules instead. + Unarchives multiple rules """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') @@ -296,11 +348,31 @@ def add_RuleServiceServicer_to_server(servicer, server): request_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.DeleteRuleRequest.FromString, response_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.DeleteRuleResponse.SerializeToString, ), + 'ArchiveRule': grpc.unary_unary_rpc_method_handler( + servicer.ArchiveRule, + request_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.ArchiveRuleRequest.FromString, + response_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.ArchiveRuleResponse.SerializeToString, + ), 'BatchDeleteRules': grpc.unary_unary_rpc_method_handler( servicer.BatchDeleteRules, request_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.BatchDeleteRulesRequest.FromString, response_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.BatchDeleteRulesResponse.SerializeToString, ), + 'BatchArchiveRules': grpc.unary_unary_rpc_method_handler( + servicer.BatchArchiveRules, + request_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.BatchArchiveRulesRequest.FromString, + response_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.BatchArchiveRulesResponse.SerializeToString, + ), + 'UnarchiveRule': grpc.unary_unary_rpc_method_handler( + servicer.UnarchiveRule, + request_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.UnarchiveRuleRequest.FromString, + response_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.UnarchiveRuleResponse.SerializeToString, + ), + 'BatchUnarchiveRules': grpc.unary_unary_rpc_method_handler( + servicer.BatchUnarchiveRules, + request_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.BatchUnarchiveRulesRequest.FromString, + response_serializer=sift_dot_rules_dot_v1_dot_rules__pb2.BatchUnarchiveRulesResponse.SerializeToString, + ), 'UndeleteRule': grpc.unary_unary_rpc_method_handler( servicer.UndeleteRule, request_deserializer=sift_dot_rules_dot_v1_dot_rules__pb2.UndeleteRuleRequest.FromString, @@ -490,6 +562,23 @@ def DeleteRule(request, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod + def ArchiveRule(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/sift.rules.v1.RuleService/ArchiveRule', + sift_dot_rules_dot_v1_dot_rules__pb2.ArchiveRuleRequest.SerializeToString, + sift_dot_rules_dot_v1_dot_rules__pb2.ArchiveRuleResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod def BatchDeleteRules(request, target, @@ -507,6 +596,57 @@ def BatchDeleteRules(request, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod + def BatchArchiveRules(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/sift.rules.v1.RuleService/BatchArchiveRules', + sift_dot_rules_dot_v1_dot_rules__pb2.BatchArchiveRulesRequest.SerializeToString, + sift_dot_rules_dot_v1_dot_rules__pb2.BatchArchiveRulesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def UnarchiveRule(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/sift.rules.v1.RuleService/UnarchiveRule', + sift_dot_rules_dot_v1_dot_rules__pb2.UnarchiveRuleRequest.SerializeToString, + sift_dot_rules_dot_v1_dot_rules__pb2.UnarchiveRuleResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def BatchUnarchiveRules(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/sift.rules.v1.RuleService/BatchUnarchiveRules', + sift_dot_rules_dot_v1_dot_rules__pb2.BatchUnarchiveRulesRequest.SerializeToString, + sift_dot_rules_dot_v1_dot_rules__pb2.BatchUnarchiveRulesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod def UndeleteRule(request, target, diff --git a/python/lib/sift/rules/v1/rules_pb2_grpc.pyi b/python/lib/sift/rules/v1/rules_pb2_grpc.pyi index edca3da5f..f0e302b9e 100644 --- a/python/lib/sift/rules/v1/rules_pb2_grpc.pyi +++ b/python/lib/sift/rules/v1/rules_pb2_grpc.pyi @@ -66,25 +66,57 @@ class RuleServiceStub: sift.rules.v1.rules_pb2.DeleteRuleRequest, sift.rules.v1.rules_pb2.DeleteRuleResponse, ] - """Deletes a rule""" + """Deprecated - use ArchiveRule instead. + Archives a rule. + """ + + ArchiveRule: grpc.UnaryUnaryMultiCallable[ + sift.rules.v1.rules_pb2.ArchiveRuleRequest, + sift.rules.v1.rules_pb2.ArchiveRuleResponse, + ] + """Archives a rule.""" BatchDeleteRules: grpc.UnaryUnaryMultiCallable[ sift.rules.v1.rules_pb2.BatchDeleteRulesRequest, sift.rules.v1.rules_pb2.BatchDeleteRulesResponse, ] - """Deletes multiple rules""" + """Deprecated - use BatchArchiveRules instead. + Archives multiple rules. + """ + + BatchArchiveRules: grpc.UnaryUnaryMultiCallable[ + sift.rules.v1.rules_pb2.BatchArchiveRulesRequest, + sift.rules.v1.rules_pb2.BatchArchiveRulesResponse, + ] + """Batch archives rules.""" + + UnarchiveRule: grpc.UnaryUnaryMultiCallable[ + sift.rules.v1.rules_pb2.UnarchiveRuleRequest, + sift.rules.v1.rules_pb2.UnarchiveRuleResponse, + ] + """Unarchives a rule.""" + + BatchUnarchiveRules: grpc.UnaryUnaryMultiCallable[ + sift.rules.v1.rules_pb2.BatchUnarchiveRulesRequest, + sift.rules.v1.rules_pb2.BatchUnarchiveRulesResponse, + ] + """Batch unarchives rules.""" UndeleteRule: grpc.UnaryUnaryMultiCallable[ sift.rules.v1.rules_pb2.UndeleteRuleRequest, sift.rules.v1.rules_pb2.UndeleteRuleResponse, ] - """Undeletes a rule""" + """Deprecated - use UnarchiveRule instead. + Unarchives a rule + """ BatchUndeleteRules: grpc.UnaryUnaryMultiCallable[ sift.rules.v1.rules_pb2.BatchUndeleteRulesRequest, sift.rules.v1.rules_pb2.BatchUndeleteRulesResponse, ] - """Undeletes multiple rules""" + """Deprecated - use BatchUnarchiveRules instead. + Unarchives multiple rules + """ EvaluateRules: grpc.UnaryUnaryMultiCallable[ sift.rules.v1.rules_pb2.EvaluateRulesRequest, @@ -186,25 +218,57 @@ class RuleServiceAsyncStub: sift.rules.v1.rules_pb2.DeleteRuleRequest, sift.rules.v1.rules_pb2.DeleteRuleResponse, ] - """Deletes a rule""" + """Deprecated - use ArchiveRule instead. + Archives a rule. + """ + + ArchiveRule: grpc.aio.UnaryUnaryMultiCallable[ + sift.rules.v1.rules_pb2.ArchiveRuleRequest, + sift.rules.v1.rules_pb2.ArchiveRuleResponse, + ] + """Archives a rule.""" BatchDeleteRules: grpc.aio.UnaryUnaryMultiCallable[ sift.rules.v1.rules_pb2.BatchDeleteRulesRequest, sift.rules.v1.rules_pb2.BatchDeleteRulesResponse, ] - """Deletes multiple rules""" + """Deprecated - use BatchArchiveRules instead. + Archives multiple rules. + """ + + BatchArchiveRules: grpc.aio.UnaryUnaryMultiCallable[ + sift.rules.v1.rules_pb2.BatchArchiveRulesRequest, + sift.rules.v1.rules_pb2.BatchArchiveRulesResponse, + ] + """Batch archives rules.""" + + UnarchiveRule: grpc.aio.UnaryUnaryMultiCallable[ + sift.rules.v1.rules_pb2.UnarchiveRuleRequest, + sift.rules.v1.rules_pb2.UnarchiveRuleResponse, + ] + """Unarchives a rule.""" + + BatchUnarchiveRules: grpc.aio.UnaryUnaryMultiCallable[ + sift.rules.v1.rules_pb2.BatchUnarchiveRulesRequest, + sift.rules.v1.rules_pb2.BatchUnarchiveRulesResponse, + ] + """Batch unarchives rules.""" UndeleteRule: grpc.aio.UnaryUnaryMultiCallable[ sift.rules.v1.rules_pb2.UndeleteRuleRequest, sift.rules.v1.rules_pb2.UndeleteRuleResponse, ] - """Undeletes a rule""" + """Deprecated - use UnarchiveRule instead. + Unarchives a rule + """ BatchUndeleteRules: grpc.aio.UnaryUnaryMultiCallable[ sift.rules.v1.rules_pb2.BatchUndeleteRulesRequest, sift.rules.v1.rules_pb2.BatchUndeleteRulesResponse, ] - """Undeletes multiple rules""" + """Deprecated - use BatchUnarchiveRules instead. + Unarchives multiple rules + """ EvaluateRules: grpc.aio.UnaryUnaryMultiCallable[ sift.rules.v1.rules_pb2.EvaluateRulesRequest, @@ -320,7 +384,17 @@ class RuleServiceServicer(metaclass=abc.ABCMeta): request: sift.rules.v1.rules_pb2.DeleteRuleRequest, context: _ServicerContext, ) -> typing.Union[sift.rules.v1.rules_pb2.DeleteRuleResponse, collections.abc.Awaitable[sift.rules.v1.rules_pb2.DeleteRuleResponse]]: - """Deletes a rule""" + """Deprecated - use ArchiveRule instead. + Archives a rule. + """ + + @abc.abstractmethod + def ArchiveRule( + self, + request: sift.rules.v1.rules_pb2.ArchiveRuleRequest, + context: _ServicerContext, + ) -> typing.Union[sift.rules.v1.rules_pb2.ArchiveRuleResponse, collections.abc.Awaitable[sift.rules.v1.rules_pb2.ArchiveRuleResponse]]: + """Archives a rule.""" @abc.abstractmethod def BatchDeleteRules( @@ -328,7 +402,33 @@ class RuleServiceServicer(metaclass=abc.ABCMeta): request: sift.rules.v1.rules_pb2.BatchDeleteRulesRequest, context: _ServicerContext, ) -> typing.Union[sift.rules.v1.rules_pb2.BatchDeleteRulesResponse, collections.abc.Awaitable[sift.rules.v1.rules_pb2.BatchDeleteRulesResponse]]: - """Deletes multiple rules""" + """Deprecated - use BatchArchiveRules instead. + Archives multiple rules. + """ + + @abc.abstractmethod + def BatchArchiveRules( + self, + request: sift.rules.v1.rules_pb2.BatchArchiveRulesRequest, + context: _ServicerContext, + ) -> typing.Union[sift.rules.v1.rules_pb2.BatchArchiveRulesResponse, collections.abc.Awaitable[sift.rules.v1.rules_pb2.BatchArchiveRulesResponse]]: + """Batch archives rules.""" + + @abc.abstractmethod + def UnarchiveRule( + self, + request: sift.rules.v1.rules_pb2.UnarchiveRuleRequest, + context: _ServicerContext, + ) -> typing.Union[sift.rules.v1.rules_pb2.UnarchiveRuleResponse, collections.abc.Awaitable[sift.rules.v1.rules_pb2.UnarchiveRuleResponse]]: + """Unarchives a rule.""" + + @abc.abstractmethod + def BatchUnarchiveRules( + self, + request: sift.rules.v1.rules_pb2.BatchUnarchiveRulesRequest, + context: _ServicerContext, + ) -> typing.Union[sift.rules.v1.rules_pb2.BatchUnarchiveRulesResponse, collections.abc.Awaitable[sift.rules.v1.rules_pb2.BatchUnarchiveRulesResponse]]: + """Batch unarchives rules.""" @abc.abstractmethod def UndeleteRule( @@ -336,7 +436,9 @@ class RuleServiceServicer(metaclass=abc.ABCMeta): request: sift.rules.v1.rules_pb2.UndeleteRuleRequest, context: _ServicerContext, ) -> typing.Union[sift.rules.v1.rules_pb2.UndeleteRuleResponse, collections.abc.Awaitable[sift.rules.v1.rules_pb2.UndeleteRuleResponse]]: - """Undeletes a rule""" + """Deprecated - use UnarchiveRule instead. + Unarchives a rule + """ @abc.abstractmethod def BatchUndeleteRules( @@ -344,7 +446,9 @@ class RuleServiceServicer(metaclass=abc.ABCMeta): request: sift.rules.v1.rules_pb2.BatchUndeleteRulesRequest, context: _ServicerContext, ) -> typing.Union[sift.rules.v1.rules_pb2.BatchUndeleteRulesResponse, collections.abc.Awaitable[sift.rules.v1.rules_pb2.BatchUndeleteRulesResponse]]: - """Undeletes multiple rules""" + """Deprecated - use BatchUnarchiveRules instead. + Unarchives multiple rules + """ @abc.abstractmethod def EvaluateRules( diff --git a/python/lib/sift_client/_internal/low_level_wrappers/__init__.py b/python/lib/sift_client/_internal/low_level_wrappers/__init__.py index 6bdef7f17..2105116b0 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/__init__.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/__init__.py @@ -5,6 +5,7 @@ from sift_client._internal.low_level_wrappers.channels import ChannelsLowLevelClient from sift_client._internal.low_level_wrappers.ingestion import IngestionLowLevelClient from sift_client._internal.low_level_wrappers.ping import PingLowLevelClient +from sift_client._internal.low_level_wrappers.reports import ReportsLowLevelClient from sift_client._internal.low_level_wrappers.rules import RulesLowLevelClient from sift_client._internal.low_level_wrappers.runs import RunsLowLevelClient @@ -14,6 +15,7 @@ "ChannelsLowLevelClient", "IngestionLowLevelClient", "PingLowLevelClient", + "ReportsLowLevelClient", "RulesLowLevelClient", "RunsLowLevelClient", ] diff --git a/python/lib/sift_client/_internal/low_level_wrappers/reports.py b/python/lib/sift_client/_internal/low_level_wrappers/reports.py new file mode 100644 index 000000000..ab7178166 --- /dev/null +++ b/python/lib/sift_client/_internal/low_level_wrappers/reports.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any, cast + +from sift.reports.v1.reports_pb2 import ( + CancelReportRequest, + GetReportRequest, + GetReportResponse, + ListReportsRequest, + ListReportsResponse, + RerunReportRequest, + RerunReportResponse, + UpdateReportRequest, +) +from sift.reports.v1.reports_pb2_grpc import ReportServiceStub + +from sift_client._internal.low_level_wrappers.base import LowLevelClientBase +from sift_client.sift_types.report import Report, ReportUpdate +from sift_client.transport import WithGrpcClient + +if TYPE_CHECKING: + from sift_client.transport.grpc_transport import GrpcClient + +# Configure logging +logger = logging.getLogger(__name__) + + +class ReportsLowLevelClient(LowLevelClientBase, WithGrpcClient): + """Low-level client for the ReportsAPI. + + This class provides a thin wrapper around the autogenerated bindings for the ReportsAPI. + """ + + def __init__(self, grpc_client: GrpcClient): + """Initialize the ReportsLowLevelClient. + + Args: + grpc_client: The gRPC client to use for making API calls. + """ + super().__init__(grpc_client) + + async def get_report(self, report_id: str) -> Report: + """Get a report by report_id. + + Args: + report_id: The report ID to get. + + Returns: + The Report. + + Raises: + ValueError: If report_id is not provided. + """ + if not report_id: + raise ValueError("report_id must be provided") + + request = GetReportRequest(report_id=report_id) + response = await self._grpc_client.get_stub(ReportServiceStub).GetReport(request) + grpc_report = cast("GetReportResponse", response).report + return Report._from_proto(grpc_report) + + async def list_reports( + self, + *, + page_size: int | None = None, + page_token: str | None = None, + query_filter: str | None = None, + organization_id: str | None = None, + order_by: str | None = None, + ) -> tuple[list[Report], str]: + """List reports with optional filtering and pagination. + + Args: + page_size: The maximum number of reports to return. + page_token: A page token for pagination. + query_filter: A CEL filter string. + organization_id: The organization ID to filter by. + order_by: How to order the retrieved reports. + + Returns: + A tuple of (reports, next_page_token). + """ + request_kwargs: dict[str, Any] = {} + if page_size is not None: + request_kwargs["page_size"] = page_size + if page_token is not None: + request_kwargs["page_token"] = page_token + if query_filter is not None: + request_kwargs["filter"] = query_filter + if organization_id is not None: + request_kwargs["organization_id"] = organization_id + if order_by is not None: + request_kwargs["order_by"] = order_by + + request = ListReportsRequest(**request_kwargs) + response = await self._grpc_client.get_stub(ReportServiceStub).ListReports(request) + response = cast("ListReportsResponse", response) + reports = [Report._from_proto(report) for report in response.reports] + return reports, response.next_page_token + + async def list_all_reports( + self, + *, + query_filter: str | None = None, + organization_id: str | None = None, + order_by: str | None = None, + max_results: int | None = None, + ) -> list[Report]: + """List all reports with optional filtering. + + Args: + query_filter: A CEL filter string. + organization_id: The organization ID to filter by. + order_by: How to order the retrieved reports. + max_results: Maximum number of results to return. + + Returns: + A list of all matching reports. + """ + return await self._handle_pagination( + self.list_reports, + kwargs={ + "query_filter": query_filter, + "organization_id": organization_id, + }, + order_by=order_by, + max_results=max_results, + ) + + async def rerun_report(self, report_id: str) -> tuple[str, str]: + """Rerun a report. + + Args: + report_id: The ID of the report to rerun. + + Returns: + A tuple of (job_id, new_report_id). + + Raises: + ValueError: If report_id is not provided. + """ + if not report_id: + raise ValueError("report_id must be provided") + + request = RerunReportRequest(report_id=report_id) + response = await self._grpc_client.get_stub(ReportServiceStub).RerunReport(request) + response = cast("RerunReportResponse", response) + return response.job_id, response.report_id + + async def cancel_report(self, report_id: str) -> None: + """Cancel a report. + + Args: + report_id: The ID of the report to cancel. + + Raises: + ValueError: If report_id is not provided. + """ + if not report_id: + raise ValueError("report_id must be provided") + + request = CancelReportRequest(report_id=report_id) + await self._grpc_client.get_stub(ReportServiceStub).CancelReport(request) + + async def update_report(self, update: ReportUpdate) -> Report: + """Update a report. + + Args: + update: The updates to apply. + + Returns: + The updated report. + """ + report_proto, field_mask = update.to_proto_with_mask() + request = UpdateReportRequest(report=report_proto, update_mask=field_mask) + await self._grpc_client.get_stub(ReportServiceStub).UpdateReport(request) + # Unfortunately, updating a report doesn't return the updated report. + return await self.get_report(update.resource_id) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/rules.py b/python/lib/sift_client/_internal/low_level_wrappers/rules.py index 676b5fa71..f490427d5 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/rules.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/rules.py @@ -3,6 +3,14 @@ import logging from typing import TYPE_CHECKING, Any, cast +from sift.common.type.v1.resource_identifier_pb2 import ResourceIdentifier, ResourceIdentifiers +from sift.rule_evaluation.v1.rule_evaluation_pb2 import ( + AssetsTimeRange, + EvaluateRulesRequest, + EvaluateRulesResponse, + RunTimeRange, +) +from sift.rule_evaluation.v1.rule_evaluation_pb2_grpc import RuleEvaluationServiceStub from sift.rules.v1.rules_pb2 import ( BatchDeleteRulesRequest, BatchGetRulesRequest, @@ -31,15 +39,20 @@ from sift.rules.v1.rules_pb2_grpc import RuleServiceStub from sift_client._internal.low_level_wrappers.base import LowLevelClientBase +from sift_client._internal.low_level_wrappers.reports import ReportsLowLevelClient from sift_client.sift_types.rule import ( Rule, RuleAction, RuleUpdate, ) from sift_client.transport import GrpcClient, WithGrpcClient +from sift_client.util.util import count_non_none if TYPE_CHECKING: + from datetime import datetime + from sift_client.sift_types.channel import ChannelReference + from sift_client.sift_types.report import Report # Configure logging logger = logging.getLogger(__name__) @@ -329,7 +342,7 @@ async def archive_rule(self, rule_id: str | None = None, client_key: str | None request_kwargs["client_key"] = client_key request = DeleteRuleRequest(**request_kwargs) - await self._grpc_client.get_stub(RuleServiceStub).ArchiveRule(request) + await self._grpc_client.get_stub(RuleServiceStub).DeleteRule(request) async def batch_archive_rules( self, rule_ids: list[str] | None = None, client_keys: list[str] | None = None @@ -445,3 +458,95 @@ async def list_all_rules( order_by=order_by, max_results=max_results, ) + + async def evaluate_rules( + self, + *, + run_id: str | None = None, + asset_ids: list[str] | None = None, + all_applicable_rules: bool | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, + rule_ids: list[str] | None = None, + rule_version_ids: list[str] | None = None, + report_template_id: str | None = None, + report_name: str | None = None, + tags: list[str] | None = None, + organization_id: str | None = None, + ) -> tuple[int, Report | None, str | None]: + """Evaluate a rule. + + Args: + run_id: The run ID to evaluate. + asset_ids: The asset IDs to evaluate. + start_time: The start time of the run. + end_time: The end time of the run. + all_applicable_rules: Whether to evaluate all rules applicable to the selected run, assets, or time range. + rule_ids: The rule IDs to evaluate. + rule_version_ids: The rule version IDs to evaluate. + report_template_id: The report template ID to evaluate. + report_name: The name of the report to create. + tags: Optional tags to add to generated annotations. + organization_id: The organization ID to evaluate. + + Returns: + The result of the rule execution. + """ + if count_non_none(run_id, asset_ids) > 1: + raise ValueError( + "Pick only one run_id or asset_ids to select what to evaluate against." + ) + + all_applicable_rules = ( + None if not all_applicable_rules else True + ) # Cast to None if False so we don't count it against other filters if they aren't opting in. + if count_non_none(rule_ids, rule_version_ids, report_template_id, all_applicable_rules) > 1: + raise ValueError( + "Pick only one rule_ids, rule_version_ids, report_template_id, or all_applicable_rules to further filter which rules to evaluate." + ) + + kwargs: dict[str, Any] = {} + # Time frame filters are run(ID), run_time_range(ID + start/end time), or assets(asset_ids + start/end time) + if start_time and end_time: + if run_id: + kwargs["run_time_range"] = RunTimeRange( + run=run_id, start_time=start_time, end_time=end_time + ) + else: + kwargs["assets"] = AssetsTimeRange( + assets={"ids": {"ids": asset_ids}}, + start_time=start_time, + end_time=end_time, + ) + elif run_id: + kwargs["run"] = ResourceIdentifier(id=run_id) + if all_applicable_rules: + kwargs["all_applicable_rules"] = all_applicable_rules + if rule_ids: + kwargs["rules"] = {"rules": ResourceIdentifiers(ids={"ids": rule_ids})} + if rule_version_ids: + kwargs["rule_versions"] = rule_version_ids + if report_template_id: + kwargs["report_template"] = report_template_id + if tags: + kwargs["tags"] = tags + if report_name: + kwargs["report_name"] = report_name + if organization_id: + kwargs["organization_id"] = organization_id + + print("kwargs: ", kwargs) + + request = EvaluateRulesRequest(**kwargs) + response = await self._grpc_client.get_stub(RuleEvaluationServiceStub).EvaluateRules( + request + ) + response = cast("EvaluateRulesResponse", response) + print("response: ", response) + created_annotation_count = response.created_annotation_count + report_id = response.report_id + job_id = response.job_id + if report_id: + report = await ReportsLowLevelClient(self._grpc_client).get_report(report_id=report_id) + return created_annotation_count, report, job_id + return created_annotation_count, None, job_id diff --git a/python/lib/sift_client/_internal/low_level_wrappers/tags.py b/python/lib/sift_client/_internal/low_level_wrappers/tags.py new file mode 100644 index 000000000..4ae12a4e4 --- /dev/null +++ b/python/lib/sift_client/_internal/low_level_wrappers/tags.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any, cast + +from sift.tags.v2.tags_pb2 import ( + CreateTagRequest, + CreateTagResponse, + ListTagsRequest, + ListTagsResponse, +) +from sift.tags.v2.tags_pb2_grpc import TagServiceStub + +from sift_client._internal.low_level_wrappers.base import LowLevelClientBase +from sift_client.sift_types.tag import Tag +from sift_client.transport import WithGrpcClient + +if TYPE_CHECKING: + from sift_client.transport.grpc_transport import GrpcClient + +# Configure logging +logger = logging.getLogger(__name__) + + +class TagsLowLevelClient(LowLevelClientBase, WithGrpcClient): + """Low-level client for the TagsAPI. + + This class provides a thin wrapper around the autogenerated bindings for the TagsAPI. + """ + + def __init__(self, grpc_client: GrpcClient): + """Initialize the TagsLowLevelClient. + + Args: + grpc_client: The gRPC client to use for making API calls. + """ + super().__init__(grpc_client) + + async def create_tag(self, name: str) -> Tag: + """Create a new tag. + + Args: + name: The name of the tag. + + Returns: + The created Tag. + + Raises: + ValueError: If name is not provided. + """ + if not name: + raise ValueError("name must be provided") + + request = CreateTagRequest(name=name) + response = await self._grpc_client.get_stub(TagServiceStub).CreateTag(request) + grpc_tag = cast("CreateTagResponse", response).tag + return Tag._from_proto(grpc_tag) + + async def list_tags( + self, + *, + page_size: int | None = None, + page_token: str | None = None, + query_filter: str | None = None, + order_by: str | None = None, + ) -> tuple[list[Tag], str]: + """List tags with optional filtering and pagination. + + Args: + page_size: The maximum number of tags to return. + page_token: A page token for pagination. + query_filter: A CEL filter string. + order_by: How to order the retrieved tags. + + Returns: + A tuple of (tags, next_page_token). + """ + request_kwargs: dict[str, Any] = {} + if page_size is not None: + request_kwargs["page_size"] = page_size + if page_token is not None: + request_kwargs["page_token"] = page_token + if query_filter is not None: + request_kwargs["filter"] = query_filter + if order_by is not None: + request_kwargs["order_by"] = order_by + + request = ListTagsRequest(**request_kwargs) + response = await self._grpc_client.get_stub(TagServiceStub).ListTags(request) + response = cast("ListTagsResponse", response) + + tags = [Tag._from_proto(tag) for tag in response.tags] + return tags, response.next_page_token + + async def list_all_tags( + self, + *, + query_filter: str | None = None, + order_by: str | None = None, + max_results: int | None = None, + ) -> list[Tag]: + """List all tags with optional filtering. + + Args: + query_filter: A CEL filter string. + order_by: How to order the retrieved tags. + max_results: Maximum number of results to return. + + Returns: + A list of all matching tags. + """ + return await self._handle_pagination( + self.list_tags, + kwargs={"query_filter": query_filter}, + order_by=order_by, + max_results=max_results, + ) diff --git a/python/lib/sift_client/_tests/integrated/reports.py b/python/lib/sift_client/_tests/integrated/reports.py new file mode 100644 index 000000000..f3891bfb9 --- /dev/null +++ b/python/lib/sift_client/_tests/integrated/reports.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""This test demonstrates the usage of the Runs API. + +It creates a new run, updates it, and associates assets with it. +It also lists runs, filters them, and deletes the run. + +It uses the SiftClient to interact with the API. +""" + +import asyncio +import os + +from sift_client import SiftClient, SiftConnectionConfig +from sift_client.sift_types import ( + ChannelReference, + ReportRuleStatus, + RuleAction, + RuleAnnotationType, +) + + +async def main(): + """Main function demonstrating the Runs API usage.""" + # Initialize the client + # You can set these environment variables or pass them directly + grpc_url = os.getenv("SIFT_GRPC_URI", "localhost:50051") + rest_url = os.getenv("SIFT_REST_URI", "localhost:8080") + api_key = os.getenv("SIFT_LOCAL_API_KEY", "") + + client = SiftClient( + connection_config=SiftConnectionConfig( + api_key=api_key, + grpc_url=grpc_url, + rest_url=rest_url, + use_ssl=False, + ), + ) + + asset = client.assets.find(name="NostromoLV426") + asset_id = asset.id_ + print(f"Using asset: {asset.name} (ID: {asset_id})") + + # List runs for this asset + runs = asset.runs + print( + f"Found {len(runs)} run(s): {[run.name for run in runs]} for asset {asset.name} (ID: {asset_id})" + ) + + # Pick one. + run = runs[0] + run_id = run.id_ + print(f"Using run: {run.name} (ID: {run_id})") + + tags = client.tags.find_or_create(names=["test", "api-created"]) + tag_ids = [tag.id_ for tag in tags] + + rule = client.rules.create( + name="test_rule", + description="Test rule", + expression="$1 > 0.1", + channel_references=[ + ChannelReference(channel_reference="$1", channel_identifier="mainmotor.velocity"), + ], + action=RuleAction.annotation( + annotation_type=RuleAnnotationType.DATA_REVIEW, + tags_ids=tag_ids, + default_assignee_user_id=None, + ), + asset_ids=[asset_id], + ) + print(f"Created rule: {rule.name} (ID: {rule.id_})") + + report_from_rules = client.reports.create_from_rules( + name="report_from_rules", + run_id=run_id, + rule_ids=[rule.id_], + ) + print(f"Created report: {report_from_rules.name} (ID: {report_from_rules.id_})") + print(f"Report summaries: {report_from_rules.summaries}") + print(f"Report tags: {report_from_rules.tags}") + + report_from_applicable_rules_run = client.reports.create_from_applicable_rules( + organization_id=asset.organization_id, + name="report_from_applicable_rules_run", + run_id=run_id, + ) + print( + f"Created report: {report_from_applicable_rules_run.name} (ID: {report_from_applicable_rules_run.id_})" + ) + print(f"Report summaries: {report_from_applicable_rules_run.summaries}") + print(f"Report tags: {report_from_applicable_rules_run.tags}") + + job_id, rerun_report_id = client.reports.rerun(report=report_from_rules) + rerun_report = client.reports.get(report_id=rerun_report_id) + print(f"Rerun report: {rerun_report.name} (ID: {rerun_report.id_})") + print(f"Report summaries: {rerun_report.summaries}") + print(f"Report tags: {rerun_report.tags}") + + assert rerun_report.metadata == {} + updated_report = client.reports.update( + report=rerun_report, + update={ + "metadata": { + "test_type": "ci", + }, + }, + ) + print(f"Updated report: {updated_report.name} (ID: {updated_report.id_})") + + reports = client.reports.list_( + run_id=run_id, + organization_id=asset.organization_id, + ) + print(f"Found {len(reports)} report(s): {[report.name for report in reports]}") + + try: + client.reports.find(name=rerun_report.name) + except ValueError as e: + assert "Multiple reports found for query" in str(e) + + find_report = client.reports.find(name=rerun_report.name, metadata={"test_type": "ci"}) + + job_id, second_rerun_report_id = client.reports.rerun(report=report_from_rules) + print(f"Second rerun report: {second_rerun_report_id} (ID: {second_rerun_report_id})") + + client.reports.cancel(report=second_rerun_report_id) + canceled_report = client.reports.get(report_id=second_rerun_report_id) + print(f"Canceled report: {canceled_report.name} (ID: {canceled_report.id_})") + + client.rules.archive(rule_ids=[rule.id_]) + + report_ids = { + report_from_rules.id_, + report_from_applicable_rules_run.id_, + rerun_report.id_, + second_rerun_report_id, + } + for report_id in report_ids: + client.reports.archive(report=report_id) + + assert rerun_report.rerun_from_report_id == report_from_rules.id_ + assert find_report.id_ == rerun_report.id_ + for summary in canceled_report.summaries: + assert summary.status == ReportRuleStatus.CANCELED + print("All tests passed") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/lib/sift_client/_tests/integrated/rules.py b/python/lib/sift_client/_tests/integrated/rules.py index 85c1e1ddc..8307ee4c2 100644 --- a/python/lib/sift_client/_tests/integrated/rules.py +++ b/python/lib/sift_client/_tests/integrated/rules.py @@ -1,7 +1,7 @@ import os from datetime import datetime, timezone -from sift_client.client import SiftClient +from sift_client.client import SiftClient, SiftConnectionConfig # Import sift_client types for calculated channels and rules from sift_client.sift_types import ( @@ -36,10 +36,15 @@ def main(): - grpc_url = os.getenv("SIFT_GRPC_URI", "localhost:50051") + grpc_url = os.getenv("SIFT_GRPC_URI", "http://localhost:50051") api_key = os.getenv("SIFT_API_KEY", "") rest_url = os.getenv("SIFT_REST_URI", "localhost:8080") - client = SiftClient(grpc_url=grpc_url, api_key=api_key, rest_url=rest_url) + api_key = os.getenv("SIFT_LOCAL_API_KEY", "") + client = SiftClient( + connection_config=SiftConnectionConfig( + grpc_url=grpc_url, api_key=api_key, rest_url=rest_url, use_ssl=False + ) + ) asset = client.assets.find(name="NostromoLV426") asset_id = asset.id_ @@ -48,6 +53,8 @@ def main(): unique_name_suffix = datetime.now(tz=timezone.utc).strftime("%Y%m%d%H%M%S") num_rules = 8 print(f"\n=== Creating {num_rules} rules with unique suffix: {unique_name_suffix} ===") + tags = client.tags.find_or_create(names=["test", "initial"]) + tag_ids = [tag.id_ for tag in tags] created_rules = [] for i in range(num_rules): rule = client.rules.create( @@ -59,7 +66,7 @@ def main(): ], action=RuleAction.annotation( annotation_type=RuleAnnotationType.DATA_REVIEW, - tags=["test", "initial"], + tags_ids=tag_ids, default_assignee_user_id=None, ), asset_ids=[asset_id], @@ -68,7 +75,7 @@ def main(): print(f"Created rule: {rule.name} (ID: {rule.id_})") # Find the rules we just created - search_results = client.rules.list( + search_results = client.rules.list_( name_regex=f"test_rule_{unique_name_suffix}.*", ) assert len(search_results) == num_rules, ( @@ -105,18 +112,20 @@ def main(): # Test 3: Update action (change annotation type and tags) print("\n--- Test 3: Update action ---") rule_3 = created_rules[2] + updated_tags = client.tags.find_or_create(names=["updated", "phase", "alert"]) + updated_tag_ids = [tag.id_ for tag in updated_tags] updated_rule_3 = rule_3.update( RuleUpdate( action=RuleAction.annotation( annotation_type=RuleAnnotationType.PHASE, - tags=["updated", "phase", "alert"], + tags_ids=updated_tag_ids, default_assignee_user_id=rule_3.created_by_user_id, ), ) ) print(f"Updated {updated_rule_3.name}: action type = {updated_rule_3.action.action_type}") print(f" - annotation type: {updated_rule_3.action.annotation_type}") - print(f" - tags: {updated_rule_3.action.tags}") + print(f" - tags: {updated_rule_3.action.tags_ids}") print(f" - assignee: {updated_rule_3.action.default_assignee_user_id}") # Test 4: Update name diff --git a/python/lib/sift_client/_tests/integrated/runs.py b/python/lib/sift_client/_tests/integrated/runs.py index 7a8cdf19b..669cad507 100644 --- a/python/lib/sift_client/_tests/integrated/runs.py +++ b/python/lib/sift_client/_tests/integrated/runs.py @@ -11,7 +11,7 @@ import os from datetime import datetime, timedelta, timezone -from sift_client import SiftClient +from sift_client import SiftClient, SiftConnectionConfig async def main(): @@ -21,10 +21,14 @@ async def main(): grpc_url = os.getenv("SIFT_GRPC_URI", "localhost:50051") rest_url = os.getenv("SIFT_REST_URI", "localhost:8080") api_key = os.getenv("SIFT_API_KEY", "") + api_key = os.getenv("SIFT_LOCAL_API_KEY", "") client = SiftClient( - api_key=api_key, - grpc_url=grpc_url, - rest_url=rest_url, + connection_config=SiftConnectionConfig( + api_key=api_key, + grpc_url=grpc_url, + rest_url=rest_url, + use_ssl=False, + ), ) # Use a known asset to fetch a run. @@ -52,7 +56,7 @@ async def main(): # Example 1: List all runs print("\n1. Listing all runs...") - runs = client.runs.list(limit=5) + runs = client.runs.list_(limit=5) print(f" Found {len(runs)} runs:") for run in runs: print(f" - {run.name} (ID: {run.id_}), Organization ID: {run.organization_id}") @@ -61,7 +65,7 @@ async def main(): print("\n2. Testing different filter options...") # Get a sample run for testing filters - sample_runs = client.runs.list(limit=3) + sample_runs = client.runs.list_(limit=3) if not sample_runs: print(" No runs available for filter testing") return @@ -71,21 +75,21 @@ async def main(): # 2a: Filter by exact name print("\n 2a. Filter by exact name...") run_name = sample_run.name - runs = client.runs.list(name=run_name, limit=5) + runs = client.runs.list_(name=run_name, limit=5) print(f" Found {len(runs)} runs with exact name '{run_name}':") for run in runs: print(f" - {run.name} (ID: {run.id_})") # 2b: Filter by name containing text print("\n 2b. Filter by name containing text...") - runs = client.runs.list(name_contains="test", limit=5) + runs = client.runs.list_(name_contains="test", limit=5) print(f" Found {len(runs)} runs with 'test' in name:") for run in runs: print(f" - {run.name}") # 2c: Filter by name using regex print("\n 2c. Filter by name using regex...") - runs = client.runs.list(name_regex=".*test.*", limit=5) + runs = client.runs.list_(name_regex=".*test.*", limit=5) print(f" Found {len(runs)} runs with 'test' in name (regex):") for run in runs: print(f" - {run.name}") @@ -93,7 +97,7 @@ async def main(): # 2d: Filter by exact description print("\n 2d. Filter by exact description...") if sample_run.description: - runs = client.runs.list(description=sample_run.description, limit=5) + runs = client.runs.list_(description=sample_run.description, limit=5) print(f" Found {len(runs)} runs with exact description '{sample_run.description}':") for run in runs: print(f" - {run.name}: {run.description}") @@ -102,7 +106,7 @@ async def main(): # 2e: Filter by description containing text print("\n 2e. Filter by description containing text...") - runs = client.runs.list(description_contains="test", limit=5) + runs = client.runs.list_(description_contains="test", limit=5) print(f" Found {len(runs)} runs with 'test' in description:") for run in runs: print(f" - {run.name}: {run.description}") @@ -112,7 +116,7 @@ async def main(): # Calculate duration for sample run if it has start and stop times if sample_run.start_time and sample_run.stop_time: duration_seconds = int((sample_run.stop_time - sample_run.start_time).total_seconds()) - runs = client.runs.list(duration_seconds=duration_seconds, limit=5) + runs = client.runs.list_(duration_seconds=duration_seconds, limit=5) print(f" Found {len(runs)} runs with duration {duration_seconds} seconds:") for run in runs: if run.start_time and run.stop_time: @@ -124,7 +128,7 @@ async def main(): # 2g: Filter by client key print("\n 2g. Filter by client key...") if sample_run.client_key: - runs = client.runs.list(client_key=sample_run.client_key, limit=5) + runs = client.runs.list_(client_key=sample_run.client_key, limit=5) print(f" Found {len(runs)} runs with client key '{sample_run.client_key}':") for run in runs: print(f" - {run.name} (client_key: {run.client_key})") @@ -135,7 +139,7 @@ async def main(): print("\n 2h. Filter by asset ID...") if sample_run.asset_ids: asset_id = sample_run.asset_ids[0] - runs = client.runs.list(asset_id=asset_id, limit=5) + runs = client.runs.list_(asset_id=asset_id, limit=5) print(f" Found {len(runs)} runs associated with asset {asset_id}:") for run in runs: print(f" - {run.name} (asset_ids: {list(run.asset_ids)})") @@ -144,7 +148,7 @@ async def main(): # 2i: Filter by asset name print("\n 2i. Filter by asset name...") - runs = client.runs.list(asset_name="NostromoLV426", limit=5) + runs = client.runs.list_(asset_name="NostromoLV426", limit=5) print(f" Found {len(runs)} runs associated with asset 'NostromoLV426':") for run in runs: print(f" - {run.name}") @@ -152,7 +156,7 @@ async def main(): # 2j: Filter by created by user ID print("\n 2j. Filter by created by user ID...") created_by_user_id = sample_run.created_by_user_id - runs = client.runs.list(created_by_user_id=created_by_user_id, limit=5) + runs = client.runs.list_(created_by_user_id=created_by_user_id, limit=5) print(f" Found {len(runs)} runs created by user {created_by_user_id}:") for run in runs: print(f" - {run.name} (created by: {run.created_by_user_id})") @@ -161,25 +165,25 @@ async def main(): print("\n 2l. Testing ordering options...") # Order by name ascending - runs = client.runs.list(order_by="name", limit=3) + runs = client.runs.list_(order_by="name", limit=3) print(" First 3 runs ordered by name (ascending):") for run in runs: print(f" - {run.name}") # Order by name descending - runs = client.runs.list(order_by="name desc", limit=3) + runs = client.runs.list_(order_by="name desc", limit=3) print(" First 3 runs ordered by name (descending):") for run in runs: print(f" - {run.name}") # Order by creation date (newest first - default) - runs = client.runs.list(order_by="created_date desc", limit=3) + runs = client.runs.list_(order_by="created_date desc", limit=3) print(" First 3 runs ordered by creation date (newest first):") for run in runs: print(f" - {run.name} (created: {run.created_date})") # Order by creation date (oldest first) - runs = client.runs.list(order_by="created_date", limit=3) + runs = client.runs.list_(order_by="created_date", limit=3) print(" First 3 runs ordered by creation date (oldest first):") for run in runs: print(f" - {run.name} (created: {run.created_date})") @@ -206,7 +210,7 @@ async def main(): start_time = datetime.now(timezone.utc) stop_time = start_time + timedelta(minutes=2) - previously_created_runs = client.runs.list(name_regex="Example Test Run.*") + previously_created_runs = client.runs.list_(name_regex="Example Test Run.*") if previously_created_runs: print(f" Deleting previously created runs: {previously_created_runs}") for run in previously_created_runs: @@ -257,7 +261,7 @@ async def main(): # Example 6: Associate assets with a run print("\n6. Associating assets with a run...") - ongoing_runs = client.runs.list( + ongoing_runs = client.runs.list_( name_regex="Example Test Run.*", include_archived=True, is_stopped=False ) if ongoing_runs: diff --git a/python/lib/sift_client/client.py b/python/lib/sift_client/client.py index 427e4def5..d122a896a 100644 --- a/python/lib/sift_client/client.py +++ b/python/lib/sift_client/client.py @@ -11,10 +11,14 @@ IngestionAPIAsync, PingAPI, PingAPIAsync, + ReportsAPI, + ReportsAPIAsync, RulesAPI, RulesAPIAsync, RunsAPI, RunsAPIAsync, + TagsAPI, + TagsAPIAsync, ) from sift_client.transport import ( GrpcClient, @@ -80,12 +84,18 @@ class SiftClient( ingestion: IngestionAPIAsync """Instance of the Ingestion API for making synchronous requests.""" + reports: ReportsAPI + """Instance of the Reports API for making synchronous requests.""" + rules: RulesAPI """Instance of the Rules API for making synchronous requests.""" runs: RunsAPI """Instance of the Runs API for making synchronous requests.""" + tags: TagsAPI + """Instance of the Tags API for making synchronous requests.""" + async_: AsyncAPIs """Accessor for the asynchronous APIs. All asynchronous APIs are available as attributes on this accessor.""" @@ -130,7 +140,8 @@ def __init__( self.ingestion = IngestionAPIAsync(self) self.rules = RulesAPI(self) self.runs = RunsAPI(self) - + self.reports = ReportsAPI(self) + self.tags = TagsAPI(self) # Accessor for the asynchronous APIs self.async_ = AsyncAPIs( ping=PingAPIAsync(self), @@ -140,6 +151,8 @@ def __init__( ingestion=IngestionAPIAsync(self), rules=RulesAPIAsync(self), runs=RunsAPIAsync(self), + reports=ReportsAPIAsync(self), + tags=TagsAPIAsync(self), ) @property diff --git a/python/lib/sift_client/resources/__init__.py b/python/lib/sift_client/resources/__init__.py index 5997acb02..5810fe7cc 100644 --- a/python/lib/sift_client/resources/__init__.py +++ b/python/lib/sift_client/resources/__init__.py @@ -3,15 +3,21 @@ from sift_client.resources.channels import ChannelsAPIAsync from sift_client.resources.ingestion import IngestionAPIAsync from sift_client.resources.ping import PingAPIAsync +from sift_client.resources.reports import ReportsAPIAsync from sift_client.resources.rules import RulesAPIAsync from sift_client.resources.runs import RunsAPIAsync +from sift_client.resources.tags import TagsAPIAsync + +# ruff: noqa TagsAPIAsync needs to be imported before sync_stubs to avoid circular import from sift_client.resources.sync_stubs import ( AssetsAPI, CalculatedChannelsAPI, ChannelsAPI, PingAPI, + ReportsAPI, RulesAPI, RunsAPI, + TagsAPI, ) __all__ = [ @@ -24,8 +30,12 @@ "IngestionAPIAsync", "PingAPI", "PingAPIAsync", + "ReportsAPI", + "ReportsAPIAsync", "RulesAPI", "RulesAPIAsync", "RunsAPI", "RunsAPIAsync", + "TagsAPI", + "TagsAPIAsync", ] diff --git a/python/lib/sift_client/resources/reports.py b/python/lib/sift_client/resources/reports.py new file mode 100644 index 000000000..a8089e9b4 --- /dev/null +++ b/python/lib/sift_client/resources/reports.py @@ -0,0 +1,310 @@ +from __future__ import annotations + +import re +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sift_client._internal.low_level_wrappers.reports import ReportsLowLevelClient +from sift_client._internal.low_level_wrappers.rules import RulesLowLevelClient +from sift_client.resources._base import ResourceBase +from sift_client.sift_types.report import Report, ReportUpdate +from sift_client.util.cel_utils import contains, equals, match + +if TYPE_CHECKING: + from sift_client.client import SiftClient + + +class ReportsAPIAsync(ResourceBase): + """High-level API for interacting with reports.""" + + def __init__(self, sift_client: SiftClient): + """Initialize the ReportsAPI. + + Args: + sift_client: The Sift client to use. + """ + super().__init__(sift_client) + self._low_level_client = ReportsLowLevelClient(grpc_client=self.client.grpc_client) + self._rules_low_level_client = RulesLowLevelClient(grpc_client=self.client.grpc_client) + + async def get( + self, + *, + report_id: str, + ) -> Report: + """Get a Report. + + Args: + report_id: The ID of the report. + + Returns: + The Report. + """ + report = await self._low_level_client.get_report(report_id=report_id) + return self._apply_client_to_instance(report) + + async def list_( + self, + *, + name: str | None = None, + name_contains: str | None = None, + name_regex: str | re.Pattern | None = None, + description: str | None = None, + description_contains: str | None = None, + run_id: str | None = None, + organization_id: str | None = None, + report_template_id: str | None = None, + metadata: dict[str, str | float | bool] | None = None, + tag_name: str | None = None, + created_by_user_id: str | None = None, + order_by: str | None = None, + limit: int | None = None, + include_archived: bool = False, + ) -> list[Report]: + """List reports with optional filtering. + + Args: + name: Exact name of the report. + name_contains: Partial name of the report. + name_regex: Regular expression string to filter reports by name. + description: Exact description of the report. + description_contains: Partial description of the report. + run_id: Run ID to filter by. + organization_id: Organization ID to filter by. + report_template_id: Report template ID to filter by. + metadata: Metadata to filter by. + tag_name: Tag name to filter by. + created_by_user_id: The user ID of the creator of the reports. + order_by: How to order the retrieved reports. + limit: How many reports to retrieve. If None, retrieves all matches. + include_archived: Whether to include archived reports. + + Returns: + A list of Reports that matches the filter. + """ + # Build CEL filter + filter_parts = [] + + if name: + filter_parts.append(equals("name", name)) + elif name_contains: + filter_parts.append(contains("name", name_contains)) + elif name_regex: + if isinstance(name_regex, re.Pattern): + name_regex = name_regex.pattern + filter_parts.append(match("name", name_regex)) # type: ignore + + if description: + filter_parts.append(equals("description", description)) + elif description_contains: + filter_parts.append(contains("description", description_contains)) + + if run_id: + filter_parts.append(equals("run_id", run_id)) + + if report_template_id: + filter_parts.append(equals("report_template_id", report_template_id)) + + if metadata: + for key, value in metadata.items(): + filter_parts.append(equals(f'metadata["{key}"]', value)) + + if tag_name: + filter_parts.append(contains("tags", tag_name)) + + if created_by_user_id: + filter_parts.append(equals("created_by_user_id", created_by_user_id)) + + if not include_archived: + filter_parts.append(equals("archived_date", None)) + + query_filter = " && ".join(filter_parts) if filter_parts else None + + reports = await self._low_level_client.list_all_reports( + query_filter=query_filter, + organization_id=organization_id, + order_by=order_by, + max_results=limit, + ) + return self._apply_client_to_instances(reports) + + async def find(self, **kwargs) -> Report | None: + """Find a single report matching the given query. Takes the same arguments as `list`. If more than one report is found, + raises an error. + + Args: + **kwargs: Keyword arguments to pass to `list`. + + Returns: + The Report found or None. + """ + reports = await self.list_(**kwargs) + if len(reports) > 1: + raise ValueError("Multiple reports found for query") + elif len(reports) == 1: + return reports[0] + return None + + async def create_from_template( + self, + *, + report_template_id: str, + run_id: str, + organization_id: str | None = None, + name: str | None = None, + ) -> Report | None: + """Create a new report from a report template. + + Args: + report_template_id: The ID of the report template to use. + run_id: The run ID to associate with the report. + organization_id: The organization ID. + name: Optional name for the report. + + Returns: + The created Report or None if no report was created. + """ + ( + created_annotation_count, + created_report, + job_id, + ) = await self._rules_low_level_client.evaluate_rules( + report_template_id=report_template_id, + run_id=run_id, + organization_id=organization_id, + report_name=name, + ) + if created_report: + return self._apply_client_to_instance(created_report) + return None + + async def create_from_rules( + self, + *, + name: str, + run_id: str | None = None, + organization_id: str | None = None, + rule_ids: list[str] | None = None, + ) -> Report | None: + """Create a new report from rules. + + Args: + name: The name of the report. + run_id: The run ID to associate with the report. + organization_id: The organization ID. + rule_ids: List of rule IDs to include in the report. + + Returns: + The created Report or None if no report was created. + """ + ( + created_annotation_count, + created_report, + job_id, + ) = await self._rules_low_level_client.evaluate_rules( + run_id=run_id, + organization_id=organization_id, + rule_ids=rule_ids, + report_name=name, + ) + if created_report: + return self._apply_client_to_instance(created_report) + return None + + async def create_from_applicable_rules( + self, + *, + run_id: str | None = None, + organization_id: str | None = None, + name: str | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, + ) -> Report | None: + """Create a new report from applicable rules based on a run. + If you want to evaluate against assets, use the rules client instead since no report is created in that case. + + Args: + run_id: The run ID to associate with the report. + organization_id: The organization ID. + name: Optional name for the report. + start_time: Optional start time to evaluate rules against. + end_time: Optional end time to evaluate rules against. + + Returns: + The created Report or None if no report was created. + """ + ( + created_annotation_count, + created_report, + job_id, + ) = await self._rules_low_level_client.evaluate_rules( + run_id=run_id, + organization_id=organization_id, + start_time=start_time, + end_time=end_time, + report_name=name, + all_applicable_rules=True, + ) + if created_report: + return self._apply_client_to_instance(created_report) + return None + + async def rerun( + self, + *, + report: str | Report, + ) -> tuple[str, str]: + """Rerun a report. + + Args: + report: The Report or report ID to rerun. + + Returns: + A tuple of (job_id, new_report_id). + """ + report_id = report.id_ if isinstance(report, Report) else report + if not isinstance(report_id, str): + raise TypeError(f"report_id must be a string not {type(report_id)}") + return await self._low_level_client.rerun_report(report_id=report_id) + + async def cancel( + self, + *, + report: str | Report, + ) -> None: + """Cancel a report. + + Args: + report: The Report or report ID to cancel. + """ + report_id = report.id_ if isinstance(report, Report) else report + if not isinstance(report_id, str): + raise TypeError(f"report_id must be a string not {type(report_id)}") + await self._low_level_client.cancel_report(report_id=report_id) + + async def update(self, report: str | Report, update: ReportUpdate | dict) -> Report: + """Update a report. + + Args: + report: The Report or report ID to update. + update: The updates to apply. + """ + report_id = report.id_ if isinstance(report, Report) else report + + if isinstance(update, dict): + update = ReportUpdate.model_validate(update) + update.resource_id = report_id + updated_report = await self._low_level_client.update_report(update=update) + return self._apply_client_to_instance(updated_report) + + async def archive( + self, + *, + report: str | Report, + ) -> Report: + """Archive a report.""" + report_id = report.id_ if isinstance(report, Report) else report + update = ReportUpdate(archived_date=datetime.now(timezone.utc)) + update.resource_id = report_id + updated_report = await self._low_level_client.update_report(update=update) + return self._apply_client_to_instance(updated_report) diff --git a/python/lib/sift_client/resources/rules.py b/python/lib/sift_client/resources/rules.py index a101a3ae5..554dee395 100644 --- a/python/lib/sift_client/resources/rules.py +++ b/python/lib/sift_client/resources/rules.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: import re + from datetime import datetime from sift_client.client import SiftClient from sift_client.sift_types.channel import ChannelReference @@ -57,6 +58,10 @@ async def list_( name: str | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, + asset_ids: list[str] | None = None, + asset_tags_ids: list[str] | None = None, + client_key: str | None = None, + created_by_user_id: str | None = None, order_by: str | None = None, limit: int | None = None, include_deleted: bool = False, @@ -67,6 +72,10 @@ async def list_( name: Exact name of the rule. name_contains: Partial name of the rule. name_regex: Regular expression string to filter rules by name. + asset_ids: List of asset IDs to filter rules by. + asset_tags_ids: List of asset tags IDs to filter rules by. + client_key: The client key of the rules. + created_by_user_id: The user ID of the creator of the rules. order_by: How to order the retrieved rules. limit: How many rules to retrieve. If None, retrieves all matches. include_deleted: Include deleted rules. @@ -84,9 +93,18 @@ async def list_( filters.append(cel.contains("name", name_contains)) if name_regex: filters.append(cel.match("name", name_regex)) + if asset_ids: + filters.append(cel.in_("asset_id", asset_ids)) + if asset_tags_ids: + filters.append(cel.in_("tag_id", asset_tags_ids)) + if client_key: + filters.append(cel.equals("client_key", client_key)) + if created_by_user_id: + filters.append(cel.equals("created_by_user_id", created_by_user_id)) + # We mostly want to OR these filters except for the deleted_date filter + filter_str = " || ".join(filters) if filters else "" if not include_deleted: - filters.append(cel.equals_null("deleted_date")) - filter_str = " && ".join(filters) if filters else "" + filter_str = f"({filter_str}) && {cel.equals_null('deleted_date')}" rules = await self._low_level_client.list_all_rules( filter_query=filter_str, order_by=order_by, @@ -260,3 +278,54 @@ async def batch_get( rule_ids=rule_ids, client_keys=client_keys ) return self._apply_client_to_instances(rules) + + async def evaluate( + self, + *, + run_id: str | None = None, + asset_ids: list[str] | None = None, + all_applicable_rules: bool | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, + rule_ids: list[str] | None = None, + rule_version_ids: list[str] | None = None, + report_template_id: str | None = None, + tags: list[str] | None = None, + ) -> tuple[int, str | None, str | None]: + """Evaluate a rule. + + Pick one of the following grouping of rules to evaluate against: + - run_id (may optionally include start_time and end_time for more specificity) + - asset_ids (requires start_time and end_time) + And one of the following filters to select which rules to evaluate: + - rule_ids + - rule_version_ids + - report_template_id + - all_applicable_rules + + Args: + run_id: The run ID to evaluate. + asset_ids: The asset IDs to evaluate. + all_applicable_rules: Whether to evaluate all rules applicable to the selected run, assets, or time range. + start_time: The start time of when to evaluate rules against. + end_time: The end time of when to evaluate rules against. + rule_ids: The rule IDs to evaluate. + rule_version_ids: The rule version IDs to evaluate. + report_template_id: The report template ID to evaluate. + tags: Optional tags to add to generated annotations. + + Returns: + A tuple of (created_annotation_count, report_id, job_id). + """ + created_annotation_count, report_id, job_id = await self._low_level_client.evaluate_rules( + run_id=run_id, + asset_ids=asset_ids, + all_applicable_rules=all_applicable_rules, + start_time=start_time, + end_time=end_time, + rule_ids=rule_ids, + rule_version_ids=rule_version_ids, + report_template_id=report_template_id, + tags=tags, + ) + return created_annotation_count, report_id, job_id diff --git a/python/lib/sift_client/resources/runs.py b/python/lib/sift_client/resources/runs.py index abb324f89..9c7543e43 100644 --- a/python/lib/sift_client/resources/runs.py +++ b/python/lib/sift_client/resources/runs.py @@ -6,7 +6,16 @@ from sift_client._internal.low_level_wrappers.runs import RunsLowLevelClient from sift_client.resources._base import ResourceBase from sift_client.sift_types.run import Run, RunUpdate -from sift_client.util.cel_utils import contains, equals, equals_null, match, not_ +from sift_client.util.cel_utils import ( + contains, + equals, + equals_null, + greater_than, + in_, + less_than, + match, + not_, +) if TYPE_CHECKING: from datetime import datetime @@ -55,6 +64,7 @@ async def list_( name: str | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, + run_ids: list[str] | None = None, description: str | None = None, description_contains: str | None = None, duration_seconds: int | None = None, @@ -63,7 +73,16 @@ async def list_( asset_name: str | None = None, created_by_user_id: str | None = None, is_stopped: bool | None = None, + created_date_start: datetime | None = None, + created_date_end: datetime | None = None, + modified_date_start: datetime | None = None, + modified_date_end: datetime | None = None, + start_time_start: datetime | None = None, + start_time_end: datetime | None = None, + stop_time_start: datetime | None = None, + stop_time_end: datetime | None = None, include_archived: bool = False, + organization_id: str | None = None, order_by: str | None = None, limit: int | None = None, ) -> list[Run]: @@ -73,6 +92,7 @@ async def list_( name: Exact name of the run. name_contains: Partial name of the run. name_regex: Regular expression string to filter runs by name. + run_ids: List of run IDs to filter by. description: Exact description of the run. description_contains: Partial description of the run. duration_seconds: Duration of the run in seconds. @@ -81,7 +101,16 @@ async def list_( asset_name: Asset name to filter by. created_by_user_id: User ID who created the run. is_stopped: Whether the run is stopped. + created_date_start: Start date for created_date filter. + created_date_end: End date for created_date filter. + modified_date_start: Start date for modified_date filter. + modified_date_end: End date for modified_date filter. + start_time_start: Start date for start_time filter. + start_time_end: End date for start_time filter. + stop_time_start: Start date for stop_time filter. + stop_time_end: End date for stop_time filter. include_archived: Whether to include archived runs. + organization_id: Organization ID to filter by. order_by: How to order the retrieved runs. limit: How many runs to retrieve. If None, retrieves all matches. @@ -100,6 +129,9 @@ async def list_( name_regex = name_regex.pattern filter_parts.append(match("name", name_regex)) # type: ignore + if run_ids: + filter_parts.append(in_("run_id", run_ids)) + if description: filter_parts.append(equals("description", description)) elif description_contains: @@ -126,6 +158,33 @@ async def list_( if not include_archived: filter_parts.append(equals("archived_date", None)) + if created_date_start: + filter_parts.append(greater_than("created_date", created_date_start)) + + if created_date_end: + filter_parts.append(less_than("created_date", created_date_end)) + + if modified_date_start: + filter_parts.append(greater_than("modified_date", modified_date_start)) + + if modified_date_end: + filter_parts.append(less_than("modified_date", modified_date_end)) + + if start_time_start: + filter_parts.append(greater_than("start_time", start_time_start)) + + if start_time_end: + filter_parts.append(less_than("start_time", start_time_end)) + + if stop_time_start: + filter_parts.append(greater_than("stop_time", stop_time_start)) + + if stop_time_end: + filter_parts.append(less_than("stop_time", stop_time_end)) + + if organization_id: + filter_parts.append(equals("organization_id", organization_id)) + query_filter = " && ".join(filter_parts) if filter_parts else None runs = await self._low_level_client.list_all_runs( diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.py b/python/lib/sift_client/resources/sync_stubs/__init__.py index 7246389a4..dca41435b 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.py +++ b/python/lib/sift_client/resources/sync_stubs/__init__.py @@ -8,8 +8,10 @@ CalculatedChannelsAPIAsync, ChannelsAPIAsync, PingAPIAsync, + ReportsAPIAsync, RulesAPIAsync, RunsAPIAsync, + TagsAPIAsync, ) PingAPI = generate_sync_api(PingAPIAsync, "PingAPI") @@ -18,5 +20,7 @@ ChannelsAPI = generate_sync_api(ChannelsAPIAsync, "ChannelsAPI") RulesAPI = generate_sync_api(RulesAPIAsync, "RulesAPI") RunsAPI = generate_sync_api(RunsAPIAsync, "RunsAPI") +ReportsAPI = generate_sync_api(ReportsAPIAsync, "ReportsAPI") +TagsAPI = generate_sync_api(TagsAPIAsync, "TagsAPI") -__all__ = ["AssetsAPI", "CalculatedChannelsAPI", "PingAPI", "RunsAPI"] +__all__ = ["AssetsAPI", "CalculatedChannelsAPI", "PingAPI", "ReportsAPI", "RunsAPI", "TagsAPI"] diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 0c52d3b15..e78635a92 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -13,20 +13,21 @@ from sift_client.client import SiftClient from sift_client.sift_types.asset import Asset, AssetUpdate from sift_client.sift_types.calculated_channel import CalculatedChannel, CalculatedChannelUpdate from sift_client.sift_types.channel import Channel, ChannelReference +from sift_client.sift_types.report import Report from sift_client.sift_types.rule import Rule, RuleAction, RuleUpdate from sift_client.sift_types.run import Run, RunUpdate +from sift_client.sift_types.tag import Tag, TagUpdate class AssetsAPI: """Sync counterpart to `AssetsAPIAsync`. High-level API for interacting with assets. - This class provides a Pythonic, notebook-friendly interface for interacting with the AssetsAPI. - It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. - - All methods in this class use the Asset class from the low-level wrapper, which is a user-friendly - representation of an asset using standard Python data structures and types. + This class provides a Pythonic, notebook-friendly interface for interacting with the AssetsAPI. + It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + All methods in this class use the Asset class from the low-level wrapper, which is a user-friendly + representation of an asset using standard Python data structures and types. """ def __init__(self, sift_client: SiftClient): @@ -139,12 +140,11 @@ class CalculatedChannelsAPI: High-level API for interacting with calculated channels. - This class provides a Pythonic, notebook-friendly interface for interacting with the CalculatedChannelsAPI. - It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. - - All methods in this class use the CalculatedChannel class from the low-level wrapper, which is a user-friendly - representation of a calculated channel using standard Python data structures and types. + This class provides a Pythonic, notebook-friendly interface for interacting with the CalculatedChannelsAPI. + It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + All methods in this class use the CalculatedChannel class from the low-level wrapper, which is a user-friendly + representation of a calculated channel using standard Python data structures and types. """ def __init__(self, sift_client: SiftClient): @@ -351,12 +351,11 @@ class ChannelsAPI: High-level API for interacting with channels. - This class provides a Pythonic, notebook-friendly interface for interacting with the ChannelsAPI. - It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. - - All methods in this class use the Channel class from the low-level wrapper, which is a user-friendly - representation of a channel using standard Python data structures and types. + This class provides a Pythonic, notebook-friendly interface for interacting with the ChannelsAPI. + It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + All methods in this class use the Channel class from the low-level wrapper, which is a user-friendly + representation of a channel using standard Python data structures and types. """ def __init__(self, sift_client: SiftClient): @@ -491,17 +490,171 @@ class PingAPI: """ ... +class ReportsAPI: + """Sync counterpart to `ReportsAPIAsync`. + + High-level API for interacting with reports. + """ + + def __init__(self, sift_client: SiftClient): + """Initialize the ReportsAPI. + + Args: + sift_client: The Sift client to use. + """ + ... + + def _run(self, coro): ... + def cancel(self, *, report: str | Report) -> None: + """Cancel a report. + + Args: + report: The Report or report ID to cancel. + """ + ... + + def create_from_applicable_rules( + self, + *, + run_id: str | None = None, + organization_id: str, + name: str | None = None, + asset_ids: list[str] | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, + ) -> Report: + """Create a new report from applicable rules based on assets or run. + Using assets requires a start and end time. + Using run_id allows but does not require a start and end time. + + Args: + run_id: The run ID to associate with the report. + organization_id: The organization ID. + name: Optional name for the report. + asset_ids: Asset IDs to add to generated annotations. + start_time: Start time of the report. + end_time: End time of the report. + + Returns: + The created Report. + """ + ... + + def create_from_rules( + self, name: str, run_id: str, organization_id: str, rule_ids: list[str] | None = None + ) -> Report: + """Create a new report from rules. + + Args: + name: The name of the report. + run_id: The run ID to associate with the report. + organization_id: The organization ID. + rule_ids: List of rule IDs to include in the report. + + Returns: + The created Report. + """ + ... + + def create_from_template( + self, report_template_id: str, run_id: str, organization_id: str, name: str | None = None + ) -> Report: + """Create a new report from a report template. + + Args: + report_template_id: The ID of the report template to use. + run_id: The run ID to associate with the report. + organization_id: The organization ID. + name: Optional name for the report. + + Returns: + The created Report. + """ + ... + + def find(self, **kwargs) -> Report | None: + """Find a single report matching the given query. Takes the same arguments as `list`. If more than one report is found, + raises an error. + + Args: + **kwargs: Keyword arguments to pass to `list`. + + Returns: + The Report found or None. + """ + ... + + def get(self, *, report_id: str) -> Report: + """Get a Report. + + Args: + report_id: The ID of the report. + + Returns: + The Report. + """ + ... + + def list_( + self, + *, + name: str | None = None, + name_contains: str | None = None, + name_regex: str | re.Pattern | None = None, + description: str | None = None, + description_contains: str | None = None, + run_id: str | None = None, + organization_id: str | None = None, + created_by_user_id: str | None = None, + modified_by_user_id: str | None = None, + report_template_id: str | None = None, + tag_name: str | None = None, + order_by: str | None = None, + limit: int | None = None, + ) -> list[Report]: + """List reports with optional filtering. + + Args: + name: Exact name of the report. + name_contains: Partial name of the report. + name_regex: Regular expression string to filter reports by name. + description: Exact description of the report. + description_contains: Partial description of the report. + run_id: Run ID to filter by. + organization_id: Organization ID to filter by. + created_by_user_id: User ID who created the report. + modified_by_user_id: User ID who modified the report. + report_template_id: Report template ID to filter by. + tag_name: Tag name to filter by. + order_by: How to order the retrieved reports. + limit: How many reports to retrieve. If None, retrieves all matches. + + Returns: + A list of Reports that matches the filter. + """ + ... + + def rerun(self, *, report: str | Report) -> tuple[str, str]: + """Rerun a report. + + Args: + report: The Report or report ID to rerun. + + Returns: + A tuple of (job_id, new_report_id). + """ + ... + class RulesAPI: """Sync counterpart to `RulesAPIAsync`. High-level API for interacting with rules. - This class provides a Pythonic, notebook-friendly interface for interacting with the RulesAPI. - It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. - - All methods in this class use the Rule class from the low-level wrapper, which is a user-friendly - representation of a rule using standard Python data structures and types. + This class provides a Pythonic, notebook-friendly interface for interacting with the RulesAPI. + It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + All methods in this class use the Rule class from the low-level wrapper, which is a user-friendly + representation of a rule using standard Python data structures and types. """ def __init__(self, sift_client: SiftClient): @@ -572,6 +725,47 @@ class RulesAPI: """Create a new rule.""" ... + def evaluate( + self, + *, + run_id: str | None = None, + assets: list[str] | None = None, + all_applicable_rules: bool | None = None, + run_start_time: datetime | None = None, + run_end_time: datetime | None = None, + rule_ids: list[str] | None = None, + rule_version_ids: list[str] | None = None, + report_template_id: str | None = None, + tags: list[str] | None = None, + ) -> Report | None: + """Evaluate a rule. + + Pick one of the following grouping of rules to evaluate against: + - run_id + - assets + - run_start_time and run_end_time + And one of the following filters to select which rules to evaluate: + - rule_ids + - rule_version_ids + - report_template_id + - all_applicable_rules + + Args: + run_id: The run ID to evaluate. + assets: The assets to evaluate. + all_applicable_rules: Whether to evaluate all rules applicable to the selected run, assets, or time range. + run_start_time: The start time of the run. + run_end_time: The end time of the run. + rule_ids: The rule IDs to evaluate. + rule_version_ids: The rule version IDs to evaluate. + report_template_id: The report template ID to evaluate. + tags: Optional tags to add to generated annotations. + + Returns: + The result of the rule evaluation. + """ + ... + def find(self, **kwargs) -> Rule | None: """Find a single rule matching the given query. Takes the same arguments as `list`. If more than one rule is found, raises an error. @@ -602,6 +796,9 @@ class RulesAPI: name: str | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, + asset_ids: list[str] | None = None, + asset_tags_ids: list[str] | None = None, + client_key: str | None = None, order_by: str | None = None, limit: int | None = None, include_deleted: bool = False, @@ -612,6 +809,9 @@ class RulesAPI: name: Exact name of the rule. name_contains: Partial name of the rule. name_regex: Regular expression string to filter rules by name. + asset_ids: List of asset IDs to filter rules by. + asset_tags_ids: List of asset tags IDs to filter rules by. + client_key: The client key of the rules. order_by: How to order the retrieved rules. limit: How many rules to retrieve. If None, retrieves all matches. include_deleted: Include deleted rules. @@ -656,12 +856,11 @@ class RunsAPI: High-level API for interacting with runs. - This class provides a Pythonic, notebook-friendly interface for interacting with the RunsAPI. - It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. - - All methods in this class use the Run class from the low-level wrapper, which is a user-friendly - representation of a run using standard Python data structures and types. + This class provides a Pythonic, notebook-friendly interface for interacting with the RunsAPI. + It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + All methods in this class use the Run class from the low-level wrapper, which is a user-friendly + representation of a run using standard Python data structures and types. """ def __init__(self, sift_client: SiftClient): @@ -749,6 +948,7 @@ class RunsAPI: name: str | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, + run_ids: list[str] | None = None, description: str | None = None, description_contains: str | None = None, duration_seconds: int | None = None, @@ -757,7 +957,16 @@ class RunsAPI: asset_name: str | None = None, created_by_user_id: str | None = None, is_stopped: bool | None = None, + created_date_start: datetime | None = None, + created_date_end: datetime | None = None, + modified_date_start: datetime | None = None, + modified_date_end: datetime | None = None, + start_time_start: datetime | None = None, + start_time_end: datetime | None = None, + stop_time_start: datetime | None = None, + stop_time_end: datetime | None = None, include_archived: bool = False, + organization_id: str | None = None, order_by: str | None = None, limit: int | None = None, ) -> list[Run]: @@ -767,6 +976,7 @@ class RunsAPI: name: Exact name of the run. name_contains: Partial name of the run. name_regex: Regular expression string to filter runs by name. + run_ids: List of run IDs to filter by. description: Exact description of the run. description_contains: Partial description of the run. duration_seconds: Duration of the run in seconds. @@ -775,7 +985,16 @@ class RunsAPI: asset_name: Asset name to filter by. created_by_user_id: User ID who created the run. is_stopped: Whether the run is stopped. + created_date_start: Start date for created_date filter. + created_date_end: End date for created_date filter. + modified_date_start: Start date for modified_date filter. + modified_date_end: End date for modified_date filter. + start_time_start: Start date for start_time filter. + start_time_end: End date for start_time filter. + stop_time_start: Start date for stop_time filter. + stop_time_end: End date for stop_time filter. include_archived: Whether to include archived runs. + organization_id: Organization ID to filter by. order_by: How to order the retrieved runs. limit: How many runs to retrieve. If None, retrieves all matches. @@ -811,3 +1030,86 @@ class RunsAPI: The updated Run. """ ... + +class TagsAPI: + """Sync counterpart to `TagsAPIAsync`. + + High-level API for interacting with tags. + """ + + def __init__(self, sift_client: SiftClient): + """Initialize the TagsAPI. + + Args: + sift_client: The Sift client to use. + """ + ... + + def _run(self, coro): ... + def create(self, name: str) -> Tag: + """Create a new tag. + + Args: + name: The name of the tag. + + Returns: + The created Tag. + """ + ... + + def find(self, **kwargs) -> Tag | None: + """Find a single tag matching the given query. Takes the same arguments as `list`. If more than one tag is found, + raises an error. + + Args: + **kwargs: Keyword arguments to pass to `list`. + + Returns: + The Tag found or None. + """ + ... + + def list_( + self, + *, + name: str | None = None, + name_contains: str | None = None, + name_regex: str | re.Pattern | None = None, + names: list[str] | None = None, + tag_ids: list[str] | None = None, + created_by_user_id: str | None = None, + order_by: str | None = None, + limit: int | None = None, + ) -> list[Tag]: + """List tags with optional filtering. + + Args: + name: Exact name of the tag. + name_contains: Partial name of the tag. + name_regex: Regular expression string to filter tags by name. + names: List of tag names to filter by. + tag_ids: List of tag IDs to filter by. + created_by_user_id: User ID who created the tag. + order_by: How to order the retrieved tags. + limit: How many tags to retrieve. If None, retrieves all matches. + + Returns: + A list of Tags that matches the filter. + """ + ... + + def update(self, tag: str | Tag, update: TagUpdate | dict) -> Tag: + """Update a Tag. + + Args: + tag: The Tag or tag ID to update. + update: Updates to apply to the Tag. + + Returns: + The updated Tag. + + Note: + The tags API doesn't have an update method in the proto, + so this would need to be implemented if the API supports it. + """ + ... diff --git a/python/lib/sift_client/resources/tags.py b/python/lib/sift_client/resources/tags.py new file mode 100644 index 000000000..3caee19d6 --- /dev/null +++ b/python/lib/sift_client/resources/tags.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from sift_client._internal.low_level_wrappers.tags import TagsLowLevelClient +from sift_client.resources._base import ResourceBase +from sift_client.util.cel_utils import contains, equals, in_, match + +if TYPE_CHECKING: + from sift_client.client import SiftClient + from sift_client.sift_types.tag import Tag, TagUpdate + + +class TagsAPIAsync(ResourceBase): + """High-level API for interacting with tags.""" + + def __init__(self, sift_client: SiftClient): + """Initialize the TagsAPI. + + Args: + sift_client: The Sift client to use. + """ + super().__init__(sift_client) + self._low_level_client = TagsLowLevelClient(grpc_client=self.client.grpc_client) + + async def list_( + self, + *, + name: str | None = None, + name_contains: str | None = None, + name_regex: str | re.Pattern | None = None, + names: list[str] | None = None, + tag_ids: list[str] | None = None, + created_by_user_id: str | None = None, + order_by: str | None = None, + limit: int | None = None, + ) -> list[Tag]: + """List tags with optional filtering. + + Args: + name: Exact name of the tag. + name_contains: Partial name of the tag. + name_regex: Regular expression string to filter tags by name. + names: List of tag names to filter by. + tag_ids: List of tag IDs to filter by. + created_by_user_id: User ID who created the tag. + order_by: How to order the retrieved tags. + limit: How many tags to retrieve. If None, retrieves all matches. + + Returns: + A list of Tags that matches the filter. + """ + # Build CEL filter + filter_parts = [] + + if name: + filter_parts.append(equals("name", name)) + elif name_contains: + filter_parts.append(contains("name", name_contains)) + elif name_regex: + if isinstance(name_regex, re.Pattern): + name_regex = name_regex.pattern + filter_parts.append(match("name", name_regex)) # type: ignore + + if names: + filter_parts.append(in_("name", names)) + if tag_ids: + filter_parts.append(in_("tag_id", tag_ids)) + + if created_by_user_id: + filter_parts.append(equals("created_by_user_id", created_by_user_id)) + + query_filter = " && ".join(filter_parts) if filter_parts else None + + tags = await self._low_level_client.list_all_tags( + query_filter=query_filter, + order_by=order_by, + max_results=limit, + ) + return self._apply_client_to_instances(tags) + + async def find(self, **kwargs) -> Tag | None: + """Find a single tag matching the given query. Takes the same arguments as `list`. If more than one tag is found, + raises an error. + + Args: + **kwargs: Keyword arguments to pass to `list`. + + Returns: + The Tag found or None. + """ + tags = await self.list_(**kwargs) + if len(tags) > 1: + raise ValueError("Multiple tags found for query") + elif len(tags) == 1: + return tags[0] + return None + + async def create(self, name: str) -> Tag: + """Create a new tag. + + Args: + name: The name of the tag. + + Returns: + The created Tag. + """ + created_tag = await self._low_level_client.create_tag(name=name) + return self._apply_client_to_instance(created_tag) + + async def find_or_create(self, names: list[str]) -> list[Tag]: + """Find tags by name or create them if they don't exist. + + Args: + names: List of tag names to find or create. + + Returns: + List of Tags that were found or created. + """ + tags = await self.list_(names=names) + existing_tag_names = {tag.name for tag in tags} + for name in names: + if name not in existing_tag_names: + tags.append(await self.create(name)) + return tags + + async def update(self, tag: str | Tag, update: TagUpdate | dict) -> Tag: + """Update a Tag. + + Args: + tag: The Tag or tag ID to update. + update: Updates to apply to the Tag. + + Returns: + The updated Tag. + + Note: + The tags API doesn't have an update method in the proto, + so this would need to be implemented if the API supports it. + """ + # Note: The tags API doesn't have an update method in the proto, + # so this would need to be implemented if the API supports it + raise NotImplementedError("Tag updates are not supported by the current API") diff --git a/python/lib/sift_client/sift_types/__init__.py b/python/lib/sift_client/sift_types/__init__.py index 6a389fa51..3bafe2eab 100644 --- a/python/lib/sift_client/sift_types/__init__.py +++ b/python/lib/sift_client/sift_types/__init__.py @@ -10,6 +10,7 @@ ChannelReference, ) from sift_client.sift_types.ingestion import IngestionConfig +from sift_client.sift_types.report import Report, ReportRuleStatus, ReportRuleSummary, ReportUpdate from sift_client.sift_types.rule import ( Rule, RuleAction, @@ -19,6 +20,7 @@ RuleVersion, ) from sift_client.sift_types.run import Run, RunUpdate +from sift_client.sift_types.tag import Tag, TagUpdate __all__ = [ "Asset", @@ -30,6 +32,10 @@ "ChannelDataType", "ChannelReference", "IngestionConfig", + "Report", + "ReportRuleStatus", + "ReportRuleSummary", + "ReportUpdate", "Rule", "RuleAction", "RuleActionType", @@ -38,4 +44,6 @@ "RuleVersion", "Run", "RunUpdate", + "Tag", + "TagUpdate", ] diff --git a/python/lib/sift_client/sift_types/report.py b/python/lib/sift_client/sift_types/report.py new file mode 100644 index 000000000..c8120e123 --- /dev/null +++ b/python/lib/sift_client/sift_types/report.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from enum import Enum +from typing import TYPE_CHECKING, ClassVar + +from pydantic import ConfigDict +from sift.reports.v1.reports_pb2 import Report as ReportProto +from sift.reports.v1.reports_pb2 import ReportRuleSummary as ReportRuleSummaryProto +from sift.reports.v1.reports_pb2 import ReportTag as ReportTagProto + +from sift_client.sift_types._base import BaseType, MappingHelper, ModelUpdate +from sift_client.util.metadata import metadata_dict_to_proto, metadata_proto_to_dict + +if TYPE_CHECKING: + from sift_client.client import SiftClient + + +class ReportRuleStatus(Enum): + """Report rule status.""" + + UNSPECIFIED = 0 + CREATED = 1 + LIVE = 2 + FINISHED = 3 + FAILED = 4 + CANCELED = 5 + ERROR = 6 + + +class ReportRuleSummary(BaseType[ReportRuleSummaryProto, "ReportRuleSummary"]): + """ReportRuleSummary model representing a rule summary within a report.""" + + rule_id: str + rule_client_key: str | None = None + rule_version_id: str + rule_version_number: int + report_rule_version_id: str + num_open: int + num_failed: int + num_passed: int + status: ReportRuleStatus + created_date: datetime + modified_date: datetime + asset_id: str + deleted_date: datetime | None = None + + @classmethod + def _from_proto( + cls, proto: ReportRuleSummaryProto, sift_client: SiftClient | None = None + ) -> ReportRuleSummary: + return cls( + id_=proto.report_rule_version_id, + rule_id=proto.rule_id, + rule_client_key=proto.rule_client_key, + rule_version_id=proto.rule_version_id, + rule_version_number=proto.rule_version_number, + report_rule_version_id=proto.report_rule_version_id, + num_open=proto.num_open, + num_failed=proto.num_failed, + num_passed=proto.num_passed, + status=ReportRuleStatus(proto.status), + created_date=proto.created_date.ToDatetime(tzinfo=timezone.utc), + modified_date=proto.modified_date.ToDatetime(tzinfo=timezone.utc), + asset_id=proto.asset_id, + deleted_date=proto.deleted_date.ToDatetime(tzinfo=timezone.utc) + if proto.HasField("deleted_date") + else None, + _client=sift_client, + ) + + def _to_proto(self) -> ReportRuleSummaryProto: + """Convert to protobuf message.""" + return ReportRuleSummaryProto( + rule_id=self.rule_id, + rule_client_key=self.rule_client_key, + rule_version_id=self.rule_version_id, + rule_version_number=self.rule_version_number, + report_rule_version_id=self.report_rule_version_id, + num_open=self.num_open, + num_failed=self.num_failed, + num_passed=self.num_passed, + status=self.status.value, + created_date=self.created_date, + modified_date=self.modified_date, + asset_id=self.asset_id, + deleted_date=self.deleted_date, + ) + + +class Report(BaseType[ReportProto, "Report"]): + """Report model representing a data analysis report.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + report_template_id: str + run_id: str + organization_id: str + name: str + description: str + created_by_user_id: str + modified_by_user_id: str + created_date: datetime + modified_date: datetime + summaries: list[ReportRuleSummary] + tags: list[str] + rerun_from_report_id: str + metadata: dict[str, str | float | bool] + archived_date: datetime | None = None + + @classmethod + def _from_proto(cls, proto: ReportProto, sift_client: SiftClient | None = None) -> Report: + return cls( + id_=proto.report_id, + report_template_id=proto.report_template_id, + run_id=proto.run_id, + organization_id=proto.organization_id, + name=proto.name, + description=proto.description, + created_by_user_id=proto.created_by_user_id, + modified_by_user_id=proto.modified_by_user_id, + created_date=proto.created_date.ToDatetime(tzinfo=timezone.utc), + modified_date=proto.modified_date.ToDatetime(tzinfo=timezone.utc), + summaries=[ + ReportRuleSummary._from_proto(summary, sift_client) for summary in proto.summaries + ], + tags=[tag.tag_name for tag in proto.tags], + rerun_from_report_id=proto.rerun_from_report_id, + metadata=metadata_proto_to_dict(proto.metadata), + archived_date=proto.archived_date.ToDatetime(tzinfo=timezone.utc) + if proto.HasField("archived_date") + else None, + _client=sift_client, + ) + + def _to_proto(self) -> ReportProto: + """Convert to protobuf message.""" + proto = ReportProto( + report_id=self.id_ or "", + run_id=self.run_id, + organization_id=self.organization_id, + name=self.name, + description=self.description, + report_template_id=self.report_template_id, + tags=[ReportTagProto(tag_name=tag) for tag in self.tags], + summaries=[summary._to_proto() for summary in self.summaries], + ) + + return proto + + +class ReportUpdate(ModelUpdate[ReportProto]): + """Model of the Report fields that can be updated.""" + + archived_date: datetime | None = None + metadata: dict[str, str | float | bool] | None = None + + _to_proto_helpers: ClassVar = { + "metadata": MappingHelper( + proto_attr_path="metadata", update_field="metadata", converter=metadata_dict_to_proto + ), + } + + def _get_proto_class(self) -> type[ReportProto]: + return ReportProto + + def _add_resource_id_to_proto(self, proto_msg: ReportProto): + if self._resource_id is None: + raise ValueError("Resource ID must be set before adding to proto") + proto_msg.report_id = self._resource_id diff --git a/python/lib/sift_client/sift_types/rule.py b/python/lib/sift_client/sift_types/rule.py index 90c40e6ab..a3aa83432 100644 --- a/python/lib/sift_client/sift_types/rule.py +++ b/python/lib/sift_client/sift_types/rule.py @@ -3,6 +3,7 @@ from datetime import datetime, timezone from enum import Enum from typing import TYPE_CHECKING +from uuid import UUID from sift.rules.v1.rules_pb2 import ( ActionKind, @@ -232,14 +233,14 @@ class RuleAction(BaseType[RuleActionProto, "RuleAction"]): modified_by_user_id: str | None = None version_id: str | None = None annotation_type: RuleAnnotationType | None = None - tags: list[str] | None = None + tags_ids: list[str] | None = None default_assignee_user_id: str | None = None @classmethod def annotation( cls, annotation_type: RuleAnnotationType, - tags: list[str], + tags_ids: list[str], default_assignee_user_id: str | None = None, ) -> RuleAction: """Create an annotation action. @@ -247,12 +248,12 @@ def annotation( Args: annotation_type: Type of annotation to create. default_assignee_user_id: User ID to assign the annotation to. - tags: List of tag IDs to add to the annotation. + tags_ids: List of tag IDs to add to the annotation. """ return cls( action_type=RuleActionType.ANNOTATION, annotation_type=annotation_type, - tags=tags, + tags_ids=[str(UUID(tag_id)) for tag_id in tags_ids], default_assignee_user_id=default_assignee_user_id, ) @@ -268,7 +269,7 @@ def _from_proto( created_by_user_id=proto.created_by_user_id, modified_by_user_id=proto.modified_by_user_id, version_id=proto.rule_action_version_id, - tags=( + tags_ids=( list(proto.configuration.annotation.tag_ids) if proto.configuration.annotation.tag_ids else None @@ -294,7 +295,7 @@ def _to_update_request(self) -> UpdateActionRequest: annotation=( AnnotationActionConfiguration( assigned_to_user_id=self.default_assignee_user_id, - tag_ids=self.tags, + tag_ids=self.tags_ids, annotation_type=self.annotation_type.value, # type: ignore ) if self.action_type == RuleActionType.ANNOTATION diff --git a/python/lib/sift_client/sift_types/tag.py b/python/lib/sift_client/sift_types/tag.py new file mode 100644 index 000000000..1c46378d7 --- /dev/null +++ b/python/lib/sift_client/sift_types/tag.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from pydantic import ConfigDict +from sift.tags.v2.tags_pb2 import Tag as TagProto + +from sift_client.sift_types._base import BaseType, ModelUpdate + +if TYPE_CHECKING: + from sift_client.client import SiftClient + + +class TagUpdate(ModelUpdate[TagProto]): + """Update model for Tag.""" + + name: str | None = None + + def _get_proto_class(self) -> type[TagProto]: + return TagProto + + def _add_resource_id_to_proto(self, proto_msg: TagProto): + if self._resource_id is None: + raise ValueError("Resource ID must be set before adding to proto") + proto_msg.tag_id = self._resource_id + + +class Tag(BaseType[TagProto, "Tag"]): + """Model of the Sift Tag.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + name: str + created_date: datetime + created_by_user_id: str + + @classmethod + def _from_proto(cls, proto: TagProto, sift_client: SiftClient | None = None) -> Tag: + return cls( + id_=proto.tag_id, + name=proto.name, + created_date=proto.created_date.ToDatetime(tzinfo=timezone.utc), + created_by_user_id=proto.created_by_user_id, + _client=sift_client, + ) + + def _to_proto(self) -> TagProto: + """Convert to protobuf message.""" + proto = TagProto( + tag_id=self.id_ or "", + name=self.name, + created_by_user_id=self.created_by_user_id, + created_date=self.created_date, # type: ignore + ) + return proto diff --git a/python/lib/sift_client/util/cel_utils.py b/python/lib/sift_client/util/cel_utils.py index 219f6fe19..f2abba800 100644 --- a/python/lib/sift_client/util/cel_utils.py +++ b/python/lib/sift_client/util/cel_utils.py @@ -198,7 +198,7 @@ def greater_than(field: str, value: int | float | datetime) -> str: as_string = value.isoformat() else: as_string = str(value) - return f"{field} > {as_string}" + return f"{field} > timestamp('{as_string}')" def less_than(field: str, value: int | float | datetime) -> str: @@ -215,4 +215,4 @@ def less_than(field: str, value: int | float | datetime) -> str: as_string = value.isoformat() else: as_string = str(value) - return f"{field} < {as_string}" + return f"{field} < timestamp('{as_string}')" diff --git a/python/lib/sift_client/util/util.py b/python/lib/sift_client/util/util.py index 4202ee715..e631351e1 100644 --- a/python/lib/sift_client/util/util.py +++ b/python/lib/sift_client/util/util.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple if TYPE_CHECKING: from sift_client.resources import ( @@ -9,8 +9,10 @@ ChannelsAPIAsync, IngestionAPIAsync, PingAPIAsync, + ReportsAPIAsync, RulesAPIAsync, RunsAPIAsync, + TagsAPIAsync, ) @@ -32,8 +34,19 @@ class AsyncAPIs(NamedTuple): ingestion: IngestionAPIAsync """Instance of the Ingestion API for making asynchronous requests.""" + reports: ReportsAPIAsync + """Instance of the Reports API for making asynchronous requests.""" + runs: RunsAPIAsync """Instance of the Runs API for making asynchronous requests.""" + tags: TagsAPIAsync + """Instance of the Tags API for making asynchronous requests.""" + rules: RulesAPIAsync """Instance of the Rules API for making asynchronous requests.""" + + +def count_non_none(*args: Any) -> int: + """Count the number of non-none arguments.""" + return sum(1 for arg in args if arg is not None) From b44667d8a8b91bb6226fd66f37ec87e3b22bee75 Mon Sep 17 00:00:00 2001 From: Ian Later Date: Mon, 13 Oct 2025 11:53:58 -0700 Subject: [PATCH 02/12] wip --- .../_internal/low_level_wrappers/rules.py | 5 +- python/lib/sift_client/resources/_base.py | 27 ++- python/lib/sift_client/resources/assets.py | 9 +- .../resources/calculated_channels.py | 9 +- python/lib/sift_client/resources/rules.py | 9 +- python/lib/sift_client/resources/runs.py | 11 +- .../resources/sync_stubs/__init__.pyi | 201 ++++++------------ python/lib/sift_client/sift_types/__init__.py | 3 +- python/lib/sift_client/sift_types/asset.py | 10 +- python/lib/sift_client/sift_types/report.py | 24 ++- python/lib/sift_client/sift_types/run.py | 32 +-- python/lib/sift_client/sift_types/tag.py | 32 ++- 12 files changed, 168 insertions(+), 204 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/rules.py b/python/lib/sift_client/_internal/low_level_wrappers/rules.py index 6e3c63234..2843eef49 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/rules.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/rules.py @@ -44,6 +44,7 @@ RuleCreate, RuleUpdate, ) +from sift_client.sift_types.tag import Tag from sift_client.transport import GrpcClient, WithGrpcClient from sift_client.util.util import count_non_none @@ -463,7 +464,7 @@ async def evaluate_rules( rule_version_ids: list[str] | None = None, report_template_id: str | None = None, report_name: str | None = None, - tags: list[str] | None = None, + tags: list[str | Tag] | None = None, organization_id: str | None = None, ) -> tuple[int, Report | None, str | None]: """Evaluate a rule. @@ -521,7 +522,7 @@ async def evaluate_rules( if report_template_id: kwargs["report_template"] = report_template_id if tags: - kwargs["tags"] = tags + kwargs["tags"] = [tag.name if isinstance(tag, Tag) else tag for tag in tags] if report_name: kwargs["report_name"] = report_name if organization_id: diff --git a/python/lib/sift_client/resources/_base.py b/python/lib/sift_client/resources/_base.py index a46ea0fa7..377ac0ebb 100644 --- a/python/lib/sift_client/resources/_base.py +++ b/python/lib/sift_client/resources/_base.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, TypeVar from sift_client.errors import _sift_client_experimental_warning +from sift_client.sift_types.tag import Tag from sift_client.util import cel_utils as cel _sift_client_experimental_warning() @@ -15,7 +16,6 @@ from sift_client.client import SiftClient from sift_client.sift_types._base import BaseType from sift_client.transport.base_connection import GrpcClient, RestClient - T = TypeVar("T", bound="BaseType") @@ -92,15 +92,28 @@ def _build_time_cel_filters( def _build_tags_metadata_cel_filters( self, - tags: list[Any] | list[str] | None = None, + tag_names: list[Any] | list[str] | None = None, + tag_ids: list[Any] | list[str] | None = None, metadata: list[Any] | None = None, ) -> list[str]: + """Build CEL filters for tags and metadata. + Note: Some resources only support filtering on tag_id but conceptually users are most likely to want to filter on tag names. Check the request proto when using this helper and consider using tag_names by default if supported as a filterable field by the request proto. + + Args: + tag_names: Creates filters for tag names + tag_ids: Creates filters for tag IDs + metadata: Creates filters for metadata. + + Returns: + A list of CEL filters. + """ filter_parts = [] - if tags: - if all(isinstance(tag, str) for tag in tags): - filter_parts.append(cel.in_("tag_name", tags)) - else: - raise NotImplementedError + if tag_names: + tag_names = [tag.name if isinstance(tag, Tag) else tag for tag in tag_names] + filter_parts.append(cel.in_("tag_name", tag_names)) + if tag_ids: + tag_ids = [tag.id_ if isinstance(tag, Tag) else tag for tag in tag_ids] + filter_parts.append(cel.in_("tag_id", tag_ids)) if metadata: raise NotImplementedError return filter_parts diff --git a/python/lib/sift_client/resources/assets.py b/python/lib/sift_client/resources/assets.py index 6d1727766..21fa7e8ec 100644 --- a/python/lib/sift_client/resources/assets.py +++ b/python/lib/sift_client/resources/assets.py @@ -12,6 +12,7 @@ from datetime import datetime from sift_client.client import SiftClient + from sift_client.sift_types.tag import Tag class AssetsAPIAsync(ResourceBase): @@ -78,9 +79,7 @@ async def list_( created_by: Any | str | None = None, modified_by: Any | str | None = None, # tags - tags: list[Any] | list[str] | None = None, - _tag_ids: list[str] - | None = None, # For compatibility until first class Tag support is added + tags: list[Any] | list[str] | list[Tag] | None = None, # metadata metadata: list[Any] | None = None, # common filters @@ -126,7 +125,7 @@ async def list_( created_by=created_by, modified_by=modified_by, ), - *self._build_tags_metadata_cel_filters(tags=tags, metadata=metadata), + *self._build_tags_metadata_cel_filters(tag_names=tags, metadata=metadata), *self._build_common_cel_filters( description_contains=description_contains, include_archived=include_archived, @@ -135,8 +134,6 @@ async def list_( ] if asset_ids: filter_parts.append(cel.in_("asset_id", asset_ids)) - if _tag_ids: - filter_parts.append(cel.in_("tag_id", _tag_ids)) filter_query = cel.and_(*filter_parts) assets = await self._low_level_client.list_all_assets( diff --git a/python/lib/sift_client/resources/calculated_channels.py b/python/lib/sift_client/resources/calculated_channels.py index 2c1f30750..05c1505d5 100644 --- a/python/lib/sift_client/resources/calculated_channels.py +++ b/python/lib/sift_client/resources/calculated_channels.py @@ -20,6 +20,7 @@ from datetime import datetime from sift_client.client import SiftClient + from sift_client.sift_types.tag import Tag class CalculatedChannelsAPIAsync(ResourceBase): @@ -86,7 +87,7 @@ async def list_( created_by: Any | str | None = None, modified_by: Any | str | None = None, # tags - tags: list[Any] | list[str] | None = None, + tags: list[Any] | list[str] | list[Tag] | None = None, # metadata metadata: list[Any] | None = None, # calculated channel specific @@ -140,7 +141,7 @@ async def list_( created_by=created_by, modified_by=modified_by, ), - *self._build_tags_metadata_cel_filters(tags=tags, metadata=metadata), + *self._build_tags_metadata_cel_filters(tag_names=tags, metadata=metadata), *self._build_common_cel_filters( description_contains=description_contains, include_archived=include_archived, @@ -291,7 +292,7 @@ async def list_versions( created_by: Any | str | None = None, modified_by: Any | str | None = None, # tags - tags: list[Any] | list[str] | None = None, + tags: list[Any] | list[str] | list[Tag] | None = None, # metadata metadata: list[Any] | None = None, # common filters @@ -338,7 +339,7 @@ async def list_versions( created_by=created_by, modified_by=modified_by, ), - *self._build_tags_metadata_cel_filters(tags=tags, metadata=metadata), + *self._build_tags_metadata_cel_filters(tag_names=tags, metadata=metadata), *self._build_common_cel_filters( description_contains=description_contains, include_archived=include_archived, diff --git a/python/lib/sift_client/resources/rules.py b/python/lib/sift_client/resources/rules.py index 09d9f255c..1983d0727 100644 --- a/python/lib/sift_client/resources/rules.py +++ b/python/lib/sift_client/resources/rules.py @@ -12,6 +12,7 @@ from datetime import datetime from sift_client.client import SiftClient + from sift_client.sift_types.tag import Tag class RulesAPIAsync(ResourceBase): @@ -72,7 +73,7 @@ async def list_( metadata: list[Any] | None = None, # rule specific asset_ids: list[str] | None = None, - asset_tag_ids: list[str] | None = None, + asset_tags: list[str | Tag] | None = None, # common filters description_contains: str | None = None, include_archived: bool = False, @@ -96,7 +97,7 @@ async def list_( modified_by: Filter rules last modified by this User or user ID. metadata: Filter rules by metadata criteria. asset_ids: Filter rules associated with any of these Asset IDs. - asset_tag_ids: Filter rules associated with any of these Asset Tag IDs. + asset_tags: Filter rules associated with any Assets that have these Tag IDs. description_contains: Partial description of the rule. include_archived: If True, include archived rules in results. filter_query: Explicit CEL query to filter rules. @@ -118,7 +119,7 @@ async def list_( created_by=created_by, modified_by=modified_by, ), - *self._build_tags_metadata_cel_filters(metadata=metadata), + *self._build_tags_metadata_cel_filters(tag_ids=asset_tags, metadata=metadata), *self._build_common_cel_filters( description_contains=description_contains, include_archived=include_archived, @@ -131,8 +132,6 @@ async def list_( filter_parts.append(cel.in_("client_key", client_keys)) if asset_ids: filter_parts.append(cel.in_("asset_id", asset_ids)) - if asset_tag_ids: - filter_parts.append(cel.in_("tag_id", asset_tag_ids)) query_filter = cel.and_(*filter_parts) rules = await self._low_level_client.list_all_rules( filter_query=query_filter, diff --git a/python/lib/sift_client/resources/runs.py b/python/lib/sift_client/resources/runs.py index 1e097612a..bba84f77e 100644 --- a/python/lib/sift_client/resources/runs.py +++ b/python/lib/sift_client/resources/runs.py @@ -5,6 +5,7 @@ from sift_client._internal.low_level_wrappers.runs import RunsLowLevelClient from sift_client.resources._base import ResourceBase from sift_client.sift_types.run import Run, RunCreate, RunUpdate +from sift_client.sift_types.tag import Tag from sift_client.util import cel_utils as cel if TYPE_CHECKING: @@ -72,10 +73,13 @@ async def list_( # created/modified users created_by: Any | str | None = None, modified_by: Any | str | None = None, + # tags + tags: list[str | Tag] | None = None, # metadata metadata: list[Any] | None = None, # run specific assets: list[Asset] | list[str] | None = None, + asset_tags: list[str | Tag] | None = None, duration_less_than: timedelta | None = None, duration_greater_than: timedelta | None = None, start_time_after: datetime | None = None, @@ -104,8 +108,10 @@ async def list_( modified_before: Filter runs modified before this datetime. created_by: Filter runs created by this User or user ID. modified_by: Filter runs last modified by this User or user ID. + tags: Filter runs with any of these Tags IDs. metadata: Filter runs by metadata criteria. assets: Filter runs associated with any of these Assets or asset IDs. + asset_tags: Filter runs associated with any Assets that have these Tag IDs. duration_less_than: Filter runs with duration less than this time. duration_greater_than: Filter runs with duration greater than this time. start_time_after: Filter runs that started after this datetime. @@ -134,7 +140,7 @@ async def list_( created_by=created_by, modified_by=modified_by, ), - *self._build_tags_metadata_cel_filters(metadata=metadata), + *self._build_tags_metadata_cel_filters(tag_ids=tags, metadata=metadata), *self._build_common_cel_filters( description_contains=description_contains, include_archived=include_archived, @@ -152,6 +158,9 @@ async def list_( else: asset = cast("list[Asset]", assets) # linting filter_parts.append(cel.in_("asset_id", [a._id_or_error for a in asset])) + if asset_tags: + asset_tag_ids = [tag.id_ if isinstance(tag, Tag) else tag for tag in asset_tags] + filter_parts.append(cel.in_("asset_tag_id", asset_tag_ids)) if duration_less_than: filter_parts.append(cel.less_than("duration_string", duration_less_than)) if duration_greater_than: diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index ddea2cced..1c4ec76c7 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -11,23 +11,16 @@ import pyarrow as pa from sift_client.client import SiftClient from sift_client.sift_types.asset import Asset, AssetUpdate -<<<<<<< HEAD -from sift_client.sift_types.calculated_channel import CalculatedChannel, CalculatedChannelUpdate -from sift_client.sift_types.channel import Channel, ChannelReference -from sift_client.sift_types.report import Report -from sift_client.sift_types.rule import Rule, RuleAction, RuleUpdate -from sift_client.sift_types.run import Run, RunUpdate -from sift_client.sift_types.tag import Tag, TagUpdate -======= from sift_client.sift_types.calculated_channel import ( CalculatedChannel, CalculatedChannelCreate, CalculatedChannelUpdate, ) from sift_client.sift_types.channel import Channel +from sift_client.sift_types.report import Report, ReportUpdate from sift_client.sift_types.rule import Rule, RuleCreate, RuleUpdate from sift_client.sift_types.run import Run, RunCreate, RunUpdate ->>>>>>> origin/main +from sift_client.sift_types.tag import Tag, TagUpdate class AssetsAPI: """Sync counterpart to `AssetsAPIAsync`. @@ -99,8 +92,7 @@ class AssetsAPI: modified_before: datetime | None = None, created_by: Any | str | None = None, modified_by: Any | str | None = None, - tags: list[Any] | list[str] | None = None, - _tag_ids: list[str] | None = None, + tags: list[Any] | list[str] | list[Tag] | None = None, metadata: list[Any] | None = None, description_contains: str | None = None, include_archived: bool = False, @@ -244,7 +236,7 @@ class CalculatedChannelsAPI: modified_before: datetime | None = None, created_by: Any | str | None = None, modified_by: Any | str | None = None, - tags: list[Any] | list[str] | None = None, + tags: list[Any] | list[str] | list[Tag] | None = None, metadata: list[Any] | None = None, asset: Asset | str | None = None, run: Run | str | None = None, @@ -299,7 +291,7 @@ class CalculatedChannelsAPI: modified_before: datetime | None = None, created_by: Any | str | None = None, modified_by: Any | str | None = None, - tags: list[Any] | list[str] | None = None, + tags: list[Any] | list[str] | list[Tag] | None = None, metadata: list[Any] | None = None, description_contains: str | None = None, include_archived: bool = False, @@ -524,6 +516,10 @@ class ReportsAPI: ... def _run(self, coro): ... + def archive(self, *, report: str | Report) -> Report: + """Archive a report.""" + ... + def cancel(self, *, report: str | Report) -> None: """Cancel a report. @@ -536,32 +532,34 @@ class ReportsAPI: self, *, run_id: str | None = None, - organization_id: str, + organization_id: str | None = None, name: str | None = None, - asset_ids: list[str] | None = None, start_time: datetime | None = None, end_time: datetime | None = None, - ) -> Report: - """Create a new report from applicable rules based on assets or run. - Using assets requires a start and end time. - Using run_id allows but does not require a start and end time. + ) -> Report | None: + """Create a new report from applicable rules based on a run. + If you want to evaluate against assets, use the rules client instead since no report is created in that case. Args: run_id: The run ID to associate with the report. organization_id: The organization ID. name: Optional name for the report. - asset_ids: Asset IDs to add to generated annotations. - start_time: Start time of the report. - end_time: End time of the report. + start_time: Optional start time to evaluate rules against. + end_time: Optional end time to evaluate rules against. Returns: - The created Report. + The created Report or None if no report was created. """ ... def create_from_rules( - self, name: str, run_id: str, organization_id: str, rule_ids: list[str] | None = None - ) -> Report: + self, + *, + name: str, + run_id: str | None = None, + organization_id: str | None = None, + rule_ids: list[str] | None = None, + ) -> Report | None: """Create a new report from rules. Args: @@ -571,13 +569,18 @@ class ReportsAPI: rule_ids: List of rule IDs to include in the report. Returns: - The created Report. + The created Report or None if no report was created. """ ... def create_from_template( - self, report_template_id: str, run_id: str, organization_id: str, name: str | None = None - ) -> Report: + self, + *, + report_template_id: str, + run_id: str, + organization_id: str | None = None, + name: str | None = None, + ) -> Report | None: """Create a new report from a report template. Args: @@ -587,7 +590,7 @@ class ReportsAPI: name: Optional name for the report. Returns: - The created Report. + The created Report or None if no report was created. """ ... @@ -624,12 +627,13 @@ class ReportsAPI: description_contains: str | None = None, run_id: str | None = None, organization_id: str | None = None, - created_by_user_id: str | None = None, - modified_by_user_id: str | None = None, report_template_id: str | None = None, + metadata: dict[str, str | float | bool] | None = None, tag_name: str | None = None, + created_by_user_id: str | None = None, order_by: str | None = None, limit: int | None = None, + include_archived: bool = False, ) -> list[Report]: """List reports with optional filtering. @@ -641,12 +645,13 @@ class ReportsAPI: description_contains: Partial description of the report. run_id: Run ID to filter by. organization_id: Organization ID to filter by. - created_by_user_id: User ID who created the report. - modified_by_user_id: User ID who modified the report. report_template_id: Report template ID to filter by. + metadata: Metadata to filter by. tag_name: Tag name to filter by. + created_by_user_id: The user ID of the creator of the reports. order_by: How to order the retrieved reports. limit: How many reports to retrieve. If None, retrieves all matches. + include_archived: Whether to include archived reports. Returns: A list of Reports that matches the filter. @@ -664,6 +669,15 @@ class ReportsAPI: """ ... + def update(self, report: str | Report, update: ReportUpdate | dict) -> Report: + """Update a report. + + Args: + report: The Report or report ID to update. + update: The updates to apply. + """ + ... + class RulesAPI: """Sync counterpart to `RulesAPIAsync`. @@ -707,47 +721,6 @@ class RulesAPI: """ ... - def evaluate( - self, - *, - run_id: str | None = None, - assets: list[str] | None = None, - all_applicable_rules: bool | None = None, - run_start_time: datetime | None = None, - run_end_time: datetime | None = None, - rule_ids: list[str] | None = None, - rule_version_ids: list[str] | None = None, - report_template_id: str | None = None, - tags: list[str] | None = None, - ) -> Report | None: - """Evaluate a rule. - - Pick one of the following grouping of rules to evaluate against: - - run_id - - assets - - run_start_time and run_end_time - And one of the following filters to select which rules to evaluate: - - rule_ids - - rule_version_ids - - report_template_id - - all_applicable_rules - - Args: - run_id: The run ID to evaluate. - assets: The assets to evaluate. - all_applicable_rules: Whether to evaluate all rules applicable to the selected run, assets, or time range. - run_start_time: The start time of the run. - run_end_time: The end time of the run. - rule_ids: The rule IDs to evaluate. - rule_version_ids: The rule version IDs to evaluate. - report_template_id: The report template ID to evaluate. - tags: Optional tags to add to generated annotations. - - Returns: - The result of the rule evaluation. - """ - ... - def find(self, **kwargs) -> Rule | None: """Find a single rule matching the given query. Takes the same arguments as `list`. If more than one rule is found, raises an error. @@ -778,11 +751,6 @@ class RulesAPI: name: str | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, -<<<<<<< HEAD - asset_ids: list[str] | None = None, - asset_tags_ids: list[str] | None = None, - client_key: str | None = None, -======= rule_ids: list[str] | None = None, client_keys: list[str] | None = None, created_after: datetime | None = None, @@ -793,11 +761,10 @@ class RulesAPI: modified_by: Any | str | None = None, metadata: list[Any] | None = None, asset_ids: list[str] | None = None, - asset_tag_ids: list[str] | None = None, + asset_tags: list[str | Tag] | None = None, description_contains: str | None = None, include_archived: bool = False, filter_query: str | None = None, ->>>>>>> origin/main order_by: str | None = None, limit: int | None = None, ) -> list[Rule]: @@ -807,14 +774,6 @@ class RulesAPI: name: Exact name of the rule. name_contains: Partial name of the rule. name_regex: Regular expression string to filter rules by name. -<<<<<<< HEAD - asset_ids: List of asset IDs to filter rules by. - asset_tags_ids: List of asset tags IDs to filter rules by. - client_key: The client key of the rules. - order_by: How to order the retrieved rules. - limit: How many rules to retrieve. If None, retrieves all matches. - include_deleted: Include deleted rules. -======= rule_ids: IDs of rules to filter to. client_keys: Client keys of rules to filter to. created_after: Rules created after this datetime. @@ -825,13 +784,12 @@ class RulesAPI: modified_by: Filter rules last modified by this User or user ID. metadata: Filter rules by metadata criteria. asset_ids: Filter rules associated with any of these Asset IDs. - asset_tag_ids: Filter rules associated with any of these Asset Tag IDs. + asset_tags: Filter rules associated with any Assets that have these Tag IDs. description_contains: Partial description of the rule. include_archived: If True, include archived rules in results. filter_query: Explicit CEL query to filter rules. order_by: Field and direction to order results by. limit: Maximum number of rules to return. If None, returns all matches. ->>>>>>> origin/main Returns: A list of Rules that matches the filter. @@ -946,26 +904,6 @@ class RunsAPI: name_contains: str | None = None, name_regex: str | re.Pattern | None = None, run_ids: list[str] | None = None, -<<<<<<< HEAD - description: str | None = None, - description_contains: str | None = None, - duration_seconds: int | None = None, - client_key: str | None = None, - asset_id: str | None = None, - asset_name: str | None = None, - created_by_user_id: str | None = None, - is_stopped: bool | None = None, - created_date_start: datetime | None = None, - created_date_end: datetime | None = None, - modified_date_start: datetime | None = None, - modified_date_end: datetime | None = None, - start_time_start: datetime | None = None, - start_time_end: datetime | None = None, - stop_time_start: datetime | None = None, - stop_time_end: datetime | None = None, - include_archived: bool = False, - organization_id: str | None = None, -======= client_keys: list[str] | None = None, created_after: datetime | None = None, created_before: datetime | None = None, @@ -973,8 +911,10 @@ class RunsAPI: modified_before: datetime | None = None, created_by: Any | str | None = None, modified_by: Any | str | None = None, + tags: list[str | Tag] | None = None, metadata: list[Any] | None = None, assets: list[Asset] | list[str] | None = None, + asset_tags: list[str | Tag] | None = None, duration_less_than: timedelta | None = None, duration_greater_than: timedelta | None = None, start_time_after: datetime | None = None, @@ -985,7 +925,6 @@ class RunsAPI: description_contains: str | None = None, include_archived: bool = False, filter_query: str | None = None, ->>>>>>> origin/main order_by: str | None = None, limit: int | None = None, ) -> list[Run]: @@ -994,30 +933,6 @@ class RunsAPI: Args: name: Exact name of the run. name_contains: Partial name of the run. -<<<<<<< HEAD - name_regex: Regular expression string to filter runs by name. - run_ids: List of run IDs to filter by. - description: Exact description of the run. - description_contains: Partial description of the run. - duration_seconds: Duration of the run in seconds. - client_key: Client key to filter by. - asset_id: Asset ID to filter by. - asset_name: Asset name to filter by. - created_by_user_id: User ID who created the run. - is_stopped: Whether the run is stopped. - created_date_start: Start date for created_date filter. - created_date_end: End date for created_date filter. - modified_date_start: Start date for modified_date filter. - modified_date_end: End date for modified_date filter. - start_time_start: Start date for start_time filter. - start_time_end: End date for start_time filter. - stop_time_start: Start date for stop_time filter. - stop_time_end: End date for stop_time filter. - include_archived: Whether to include archived runs. - organization_id: Organization ID to filter by. - order_by: How to order the retrieved runs. - limit: How many runs to retrieve. If None, retrieves all matches. -======= name_regex: Regular expression to filter runs by name. run_ids: Filter to runs with any of these IDs. client_keys: Filter to runs with any of these client keys. @@ -1027,8 +942,10 @@ class RunsAPI: modified_before: Filter runs modified before this datetime. created_by: Filter runs created by this User or user ID. modified_by: Filter runs last modified by this User or user ID. + tags: Filter runs with any of these Tags IDs. metadata: Filter runs by metadata criteria. assets: Filter runs associated with any of these Assets or asset IDs. + asset_tags: Filter runs associated with any Assets that have these Tag IDs. duration_less_than: Filter runs with duration less than this time. duration_greater_than: Filter runs with duration greater than this time. start_time_after: Filter runs that started after this datetime. @@ -1041,7 +958,6 @@ class RunsAPI: filter_query: Explicit CEL query to filter runs. order_by: Field and direction to order results by. limit: Maximum number of runs to return. If None, returns all matches. ->>>>>>> origin/main Returns: A list of Run objects that match the filter criteria. @@ -1114,6 +1030,17 @@ class TagsAPI: """ ... + def find_or_create(self, names: list[str]) -> list[Tag]: + """Find tags by name or create them if they don't exist. + + Args: + names: List of tag names to find or create. + + Returns: + List of Tags that were found or created. + """ + ... + def list_( self, *, diff --git a/python/lib/sift_client/sift_types/__init__.py b/python/lib/sift_client/sift_types/__init__.py index c7602d2d9..6097be5aa 100644 --- a/python/lib/sift_client/sift_types/__init__.py +++ b/python/lib/sift_client/sift_types/__init__.py @@ -22,7 +22,7 @@ RuleVersion, ) from sift_client.sift_types.run import Run, RunCreate, RunUpdate -from sift_client.sift_types.tag import Tag, TagUpdate +from sift_client.sift_types.tag import Tag, TagCreate, TagUpdate __all__ = [ "Asset", @@ -52,5 +52,6 @@ "RunCreate", "RunUpdate", "Tag", + "TagCreate", "TagUpdate", ] diff --git a/python/lib/sift_client/sift_types/asset.py b/python/lib/sift_client/sift_types/asset.py index dfab26c9f..a0f088c7a 100644 --- a/python/lib/sift_client/sift_types/asset.py +++ b/python/lib/sift_client/sift_types/asset.py @@ -6,6 +6,7 @@ from sift.assets.v1.assets_pb2 import Asset as AssetProto from sift_client.sift_types._base import BaseType, MappingHelper, ModelUpdate +from sift_client.sift_types.tag import Tag from sift_client.util.metadata import metadata_dict_to_proto, metadata_proto_to_dict if TYPE_CHECKING: @@ -24,7 +25,7 @@ class Asset(BaseType[AssetProto, "Asset"]): created_by_user_id: str modified_date: datetime modified_by_user_id: str - tags: list[str] + tags: list[str | Tag] metadata: dict[str, str | float | bool] is_archived: bool @@ -109,7 +110,7 @@ def _from_proto(cls, proto: AssetProto, sift_client: SiftClient | None = None) - class AssetUpdate(ModelUpdate[AssetProto]): """Model of the Asset Fields that can be updated.""" - tags: list[str] | None = None + tags: list[str | Tag] | None = None metadata: dict[str, str | float | bool] | None = None is_archived: bool | None = None @@ -119,6 +120,11 @@ class AssetUpdate(ModelUpdate[AssetProto]): update_field="metadata", converter=metadata_dict_to_proto, ), + "tags": MappingHelper( + proto_attr_path="tags", + update_field="tags", + converter=lambda tags: [tag.name if isinstance(tag, Tag) else tag for tag in tags], + ), } def _get_proto_class(self) -> type[AssetProto]: diff --git a/python/lib/sift_client/sift_types/report.py b/python/lib/sift_client/sift_types/report.py index c8120e123..c28719588 100644 --- a/python/lib/sift_client/sift_types/report.py +++ b/python/lib/sift_client/sift_types/report.py @@ -9,7 +9,8 @@ from sift.reports.v1.reports_pb2 import ReportRuleSummary as ReportRuleSummaryProto from sift.reports.v1.reports_pb2 import ReportTag as ReportTagProto -from sift_client.sift_types._base import BaseType, MappingHelper, ModelUpdate +from sift_client.sift_types._base import BaseType, MappingHelper, ModelCreateUpdateBase, ModelUpdate +from sift_client.sift_types.tag import Tag from sift_client.util.metadata import metadata_dict_to_proto, metadata_proto_to_dict if TYPE_CHECKING: @@ -149,21 +150,34 @@ def _to_proto(self) -> ReportProto: return proto -class ReportUpdate(ModelUpdate[ReportProto]): - """Model of the Report fields that can be updated.""" +class ReportCreateUpdateBase(ModelCreateUpdateBase): + """Base model for Report create and update.""" - archived_date: datetime | None = None + name: str + description: str | None = None + tags: list[str] | list[Tag] | None = None metadata: dict[str, str | float | bool] | None = None - _to_proto_helpers: ClassVar = { + _to_proto_helpers: ClassVar[dict[str, MappingHelper]] = { "metadata": MappingHelper( proto_attr_path="metadata", update_field="metadata", converter=metadata_dict_to_proto ), + "tags": MappingHelper( + proto_attr_path="tags", + update_field="tags", + converter=lambda tags: [tag.name if isinstance(tag, Tag) else tag for tag in tags], + ), } def _get_proto_class(self) -> type[ReportProto]: return ReportProto + +class ReportUpdate(ReportCreateUpdateBase, ModelUpdate[ReportProto]): + """Model of the Report fields that can be updated.""" + + is_archived: bool | None = None + def _add_resource_id_to_proto(self, proto_msg: ReportProto): if self._resource_id is None: raise ValueError("Resource ID must be set before adding to proto") diff --git a/python/lib/sift_client/sift_types/run.py b/python/lib/sift_client/sift_types/run.py index eb3e081b1..2e7816a5b 100644 --- a/python/lib/sift_client/sift_types/run.py +++ b/python/lib/sift_client/sift_types/run.py @@ -14,6 +14,7 @@ ModelCreateUpdateBase, ModelUpdate, ) +from sift_client.sift_types.tag import Tag from sift_client.util.metadata import metadata_dict_to_proto, metadata_proto_to_dict if TYPE_CHECKING: @@ -123,7 +124,7 @@ class RunBase(ModelCreateUpdateBase): description: str | None = None start_time: datetime | None = None stop_time: datetime | None = None - tags: list[str] | None = None + tags: list[str] | list[Tag] | None = None metadata: dict[str, str | float | bool] | None = None _to_proto_helpers: ClassVar[dict[str, MappingHelper]] = { @@ -132,6 +133,11 @@ class RunBase(ModelCreateUpdateBase): update_field="metadata", converter=metadata_dict_to_proto, ), + "tags": MappingHelper( + proto_attr_path="tags", + update_field="tags", + converter=lambda tags: [tag.name if isinstance(tag, Tag) else tag for tag in tags], + ), } @model_validator(mode="after") @@ -152,20 +158,8 @@ class RunCreate(RunBase, ModelCreate[CreateRunRequestProto]): name: str client_key: str | None = None - tags: list[str] | None = None - metadata: dict[str, str | float | bool] | None = None - start_time: datetime | None = None - stop_time: datetime | None = None organization_id: str | None = None - _to_proto_helpers: ClassVar[dict[str, MappingHelper]] = { - "metadata": MappingHelper( - proto_attr_path="metadata", - update_field="metadata", - converter=metadata_dict_to_proto, - ), - } - def _get_proto_class(self) -> type[CreateRunRequestProto]: return CreateRunRequestProto @@ -174,20 +168,8 @@ class RunUpdate(RunBase, ModelUpdate[RunProto]): """Update model for Run.""" name: str | None = None - tags: list[str] | None = None - metadata: dict[str, str | float | bool] | None = None - start_time: datetime | None = None - stop_time: datetime | None = None is_archived: bool | None = None - _to_proto_helpers: ClassVar[dict[str, MappingHelper]] = { - "metadata": MappingHelper( - proto_attr_path="metadata", - update_field="metadata", - converter=metadata_dict_to_proto, - ), - } - def _get_proto_class(self) -> type[RunProto]: return RunProto diff --git a/python/lib/sift_client/sift_types/tag.py b/python/lib/sift_client/sift_types/tag.py index 1c46378d7..690e46576 100644 --- a/python/lib/sift_client/sift_types/tag.py +++ b/python/lib/sift_client/sift_types/tag.py @@ -3,34 +3,48 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from pydantic import ConfigDict +from sift.tags.v2.tags_pb2 import CreateTagRequest as CreateTagRequestProto from sift.tags.v2.tags_pb2 import Tag as TagProto -from sift_client.sift_types._base import BaseType, ModelUpdate +from sift_client.sift_types._base import ( + BaseType, + ModelCreate, + ModelCreateUpdateBase, + ModelUpdate, +) if TYPE_CHECKING: from sift_client.client import SiftClient -class TagUpdate(ModelUpdate[TagProto]): - """Update model for Tag.""" +class TagCreateUpdateBase(ModelCreateUpdateBase): + """Base model for Tag create and update.""" - name: str | None = None + name: str + + +class TagCreate(TagCreateUpdateBase, ModelCreate[TagProto]): + """Create model for Tag.""" + + def _get_proto_class(self) -> type[CreateTagRequestProto]: + return CreateTagRequestProto - def _get_proto_class(self) -> type[TagProto]: - return TagProto + +class TagUpdate(TagCreateUpdateBase, ModelUpdate[TagProto]): + """Update model for Tag.""" def _add_resource_id_to_proto(self, proto_msg: TagProto): if self._resource_id is None: raise ValueError("Resource ID must be set before adding to proto") proto_msg.tag_id = self._resource_id + def _get_proto_class(self) -> type[TagProto]: + return TagProto + class Tag(BaseType[TagProto, "Tag"]): """Model of the Sift Tag.""" - model_config = ConfigDict(arbitrary_types_allowed=True) - name: str created_date: datetime created_by_user_id: str From 51cd94d56366245a98df5b37d260a2dbcc620697 Mon Sep 17 00:00:00 2001 From: Ian Later Date: Mon, 13 Oct 2025 13:00:17 -0700 Subject: [PATCH 03/12] wip --- python/lib/sift_client/_tests/conftest.py | 11 +-- python/lib/sift_client/resources/reports.py | 76 +++++++++++---------- python/lib/sift_client/resources/tags.py | 59 ++++++++++------ 3 files changed, 84 insertions(+), 62 deletions(-) diff --git a/python/lib/sift_client/_tests/conftest.py b/python/lib/sift_client/_tests/conftest.py index 1f218dedc..4838c0d03 100644 --- a/python/lib/sift_client/_tests/conftest.py +++ b/python/lib/sift_client/_tests/conftest.py @@ -5,7 +5,7 @@ import pytest -from sift_client import SiftClient +from sift_client import SiftClient,SiftConnectionConfig from sift_client.util.util import AsyncAPIs @@ -21,9 +21,12 @@ def sift_client() -> SiftClient: api_key = os.getenv("SIFT_API_KEY", "") return SiftClient( - api_key=api_key, - grpc_url=grpc_url, - rest_url=rest_url, + connection_config=SiftConnectionConfig( + api_key=api_key, + grpc_url=grpc_url, + rest_url=rest_url, + use_ssl=False, + ) ) diff --git a/python/lib/sift_client/resources/reports.py b/python/lib/sift_client/resources/reports.py index a8089e9b4..fa63e2525 100644 --- a/python/lib/sift_client/resources/reports.py +++ b/python/lib/sift_client/resources/reports.py @@ -8,7 +8,7 @@ from sift_client._internal.low_level_wrappers.rules import RulesLowLevelClient from sift_client.resources._base import ResourceBase from sift_client.sift_types.report import Report, ReportUpdate -from sift_client.util.cel_utils import contains, equals, match +from sift_client.util import cel_utils as cel if TYPE_CHECKING: from sift_client.client import SiftClient @@ -49,17 +49,23 @@ async def list_( name: str | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, - description: str | None = None, + names: list[str] | None = None, description_contains: str | None = None, run_id: str | None = None, organization_id: str | None = None, report_template_id: str | None = None, metadata: dict[str, str | float | bool] | None = None, tag_name: str | None = None, - created_by_user_id: str | None = None, + created_by: str | None = None, + modified_by: str | None = None, order_by: str | None = None, limit: int | None = None, include_archived: bool = False, + filter_query: str | None = None, + created_after: datetime | None = None, + created_before: datetime | None = None, + modified_after: datetime | None = None, + modified_before: datetime | None = None, ) -> list[Report]: """List reports with optional filtering. @@ -67,58 +73,54 @@ async def list_( name: Exact name of the report. name_contains: Partial name of the report. name_regex: Regular expression string to filter reports by name. - description: Exact description of the report. + names: List of report names to filter by. description_contains: Partial description of the report. run_id: Run ID to filter by. organization_id: Organization ID to filter by. report_template_id: Report template ID to filter by. metadata: Metadata to filter by. tag_name: Tag name to filter by. - created_by_user_id: The user ID of the creator of the reports. + created_by: The user ID of the creator of the reports. + modified_by: The user ID of the last modifier of the reports. order_by: How to order the retrieved reports. limit: How many reports to retrieve. If None, retrieves all matches. include_archived: Whether to include archived reports. - + filter_query: Explicit CEL query to filter reports. + created_after: Filter reports created after this datetime. + created_before: Filter reports created before this datetime. + modified_after: Filter reports modified after this datetime. + modified_before: Filter reports modified before this datetime. Returns: A list of Reports that matches the filter. """ # Build CEL filter - filter_parts = [] - - if name: - filter_parts.append(equals("name", name)) - elif name_contains: - filter_parts.append(contains("name", name_contains)) - elif name_regex: - if isinstance(name_regex, re.Pattern): - name_regex = name_regex.pattern - filter_parts.append(match("name", name_regex)) # type: ignore - - if description: - filter_parts.append(equals("description", description)) - elif description_contains: - filter_parts.append(contains("description", description_contains)) + filter_parts = [ + *self._build_name_cel_filters( + name=name, name_contains=name_contains, name_regex=name_regex, names=names + ), + *self._build_time_cel_filters( + created_after=created_after, + created_before=created_before, + modified_after=modified_after, + modified_before=modified_before, + created_by=created_by, + modified_by=modified_by, + ), + *self._build_tags_metadata_cel_filters(tag_names=[tag_name], metadata=metadata), + *self._build_common_cel_filters( + description_contains=description_contains, + include_archived=include_archived, + filter_query=filter_query, + ), + ] if run_id: - filter_parts.append(equals("run_id", run_id)) + filter_parts.append(cel.equals("run_id", run_id)) if report_template_id: - filter_parts.append(equals("report_template_id", report_template_id)) - - if metadata: - for key, value in metadata.items(): - filter_parts.append(equals(f'metadata["{key}"]', value)) - - if tag_name: - filter_parts.append(contains("tags", tag_name)) - - if created_by_user_id: - filter_parts.append(equals("created_by_user_id", created_by_user_id)) - - if not include_archived: - filter_parts.append(equals("archived_date", None)) + filter_parts.append(cel.equals("report_template_id", report_template_id)) - query_filter = " && ".join(filter_parts) if filter_parts else None + query_filter = cel.and_(*filter_parts) if filter_parts else None reports = await self._low_level_client.list_all_reports( query_filter=query_filter, diff --git a/python/lib/sift_client/resources/tags.py b/python/lib/sift_client/resources/tags.py index 3caee19d6..e7ccbdcf5 100644 --- a/python/lib/sift_client/resources/tags.py +++ b/python/lib/sift_client/resources/tags.py @@ -1,11 +1,12 @@ from __future__ import annotations import re +from datetime import datetime from typing import TYPE_CHECKING from sift_client._internal.low_level_wrappers.tags import TagsLowLevelClient from sift_client.resources._base import ResourceBase -from sift_client.util.cel_utils import contains, equals, in_, match +from sift_client.util import cel_utils as cel if TYPE_CHECKING: from sift_client.client import SiftClient @@ -32,7 +33,14 @@ async def list_( name_regex: str | re.Pattern | None = None, names: list[str] | None = None, tag_ids: list[str] | None = None, - created_by_user_id: str | None = None, + created_by: str | None = None, + modified_by: str | None = None, + include_archived: bool = False, + filter_query: str | None = None, + created_after: datetime | None = None, + created_before: datetime | None = None, + modified_after: datetime | None = None, + modified_before: datetime | None = None, order_by: str | None = None, limit: int | None = None, ) -> list[Tag]: @@ -44,7 +52,14 @@ async def list_( name_regex: Regular expression string to filter tags by name. names: List of tag names to filter by. tag_ids: List of tag IDs to filter by. - created_by_user_id: User ID who created the tag. + created_by: User ID who created the tag. + modified_by: User ID who last modified the tag. + include_archived: Whether to include archived tags. + filter_query: Explicit CEL query to filter tags. + created_after: Filter tags created after this datetime. + created_before: Filter tags created before this datetime. + modified_after: Filter tags modified after this datetime. + modified_before: Filter tags modified before this datetime. order_by: How to order the retrieved tags. limit: How many tags to retrieve. If None, retrieves all matches. @@ -52,26 +67,28 @@ async def list_( A list of Tags that matches the filter. """ # Build CEL filter - filter_parts = [] - - if name: - filter_parts.append(equals("name", name)) - elif name_contains: - filter_parts.append(contains("name", name_contains)) - elif name_regex: - if isinstance(name_regex, re.Pattern): - name_regex = name_regex.pattern - filter_parts.append(match("name", name_regex)) # type: ignore - - if names: - filter_parts.append(in_("name", names)) + filter_parts = [ + *self._build_name_cel_filters( + name=name, name_contains=name_contains, name_regex=name_regex, names=names + ), + *self._build_time_cel_filters( + created_after=created_after, + created_before=created_before, + modified_after=modified_after, + modified_before=modified_before, + created_by=created_by, + modified_by=modified_by, + ), + *self._build_common_cel_filters( + include_archived=include_archived, + filter_query=filter_query, + ), + ] + if tag_ids: - filter_parts.append(in_("tag_id", tag_ids)) + filter_parts.append(cel.in_("tag_id", tag_ids)) - if created_by_user_id: - filter_parts.append(equals("created_by_user_id", created_by_user_id)) - - query_filter = " && ".join(filter_parts) if filter_parts else None + query_filter = cel.and_(*filter_parts) if filter_parts else None tags = await self._low_level_client.list_all_tags( query_filter=query_filter, From 8a5c03463fa31891a7c41d5381cb274ba60a766c Mon Sep 17 00:00:00 2001 From: Ian Later Date: Mon, 13 Oct 2025 13:09:56 -0700 Subject: [PATCH 04/12] adf --- python/lib/sift_client/_tests/conftest.py | 2 +- .../_tests/resources/test_reports.py | 0 .../sift_client/_tests/resources/test_tags.py | 427 ++++++++++++++++++ python/lib/sift_client/resources/reports.py | 1 + python/lib/sift_client/resources/tags.py | 6 +- 5 files changed, 433 insertions(+), 3 deletions(-) create mode 100644 python/lib/sift_client/_tests/resources/test_reports.py create mode 100644 python/lib/sift_client/_tests/resources/test_tags.py diff --git a/python/lib/sift_client/_tests/conftest.py b/python/lib/sift_client/_tests/conftest.py index 4838c0d03..3378ec03f 100644 --- a/python/lib/sift_client/_tests/conftest.py +++ b/python/lib/sift_client/_tests/conftest.py @@ -5,7 +5,7 @@ import pytest -from sift_client import SiftClient,SiftConnectionConfig +from sift_client import SiftClient, SiftConnectionConfig from sift_client.util.util import AsyncAPIs diff --git a/python/lib/sift_client/_tests/resources/test_reports.py b/python/lib/sift_client/_tests/resources/test_reports.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/lib/sift_client/_tests/resources/test_tags.py b/python/lib/sift_client/_tests/resources/test_tags.py new file mode 100644 index 000000000..6f374e79f --- /dev/null +++ b/python/lib/sift_client/_tests/resources/test_tags.py @@ -0,0 +1,427 @@ +"""Pytest tests for the Tags API. + +These tests demonstrate and validate the usage of the Tags API including: +- Basic tag operations (list, find) +- Tag filtering and searching +- Tag creation and find_or_create +- Error handling and edge cases +""" + +from datetime import datetime, timezone + +import pytest + +from sift_client import SiftClient +from sift_client.resources import TagsAPI, TagsAPIAsync +from sift_client.sift_types import Tag + +pytestmark = pytest.mark.integration + + +def test_client_binding(sift_client): + assert sift_client.tags + assert isinstance(sift_client.tags, TagsAPI) + assert sift_client.async_.tags + assert isinstance(sift_client.async_.tags, TagsAPIAsync) + + +@pytest.fixture +def tags_api_async(sift_client: SiftClient): + """Get the async tags API instance.""" + return sift_client.async_.tags + + +@pytest.fixture +def tags_api_sync(sift_client: SiftClient): + """Get the synchronous tags API instance.""" + return sift_client.tags + + +@pytest.fixture +def test_tag(tags_api_sync): + """Get an existing tag for testing.""" + tags = tags_api_sync.list_(limit=1) + if not tags: + # Create a tag if none exists + tag = tags_api_sync.create(f"test_tag_{datetime.now(timezone.utc).isoformat()}") + return tag + return tags[0] + + +class TestTagsAPIAsync: + """Test suite for the async Tags API functionality.""" + + class TestList: + """Tests for the async list method.""" + + @pytest.mark.asyncio + async def test_basic_list(self, tags_api_async): + """Test basic tag listing functionality.""" + tags = await tags_api_async.list_(limit=5) + + # Verify we get a list + assert isinstance(tags, list) + + # If we have tags, verify their structure + if tags: + tag = tags[0] + assert isinstance(tag, Tag) + assert tag.id_ is not None + assert tag.name is not None + + @pytest.mark.asyncio + async def test_list_with_name_filter(self, tags_api_async): + """Test tag listing with name filtering.""" + # Create a test tag with a unique name + test_tag_name = f"test_tag_exact_{datetime.now(timezone.utc).timestamp()}" + created_tag = await tags_api_async.create(test_tag_name) + + try: + # Filter by exact name + filtered_tags = await tags_api_async.list_(name=test_tag_name) + + # Should find exactly one tag with this name + assert isinstance(filtered_tags, list) + assert len(filtered_tags) == 1 + assert filtered_tags[0].name == test_tag_name + assert filtered_tags[0].id_ == created_tag.id_ + finally: + # Note: No archive method for tags, they persist + pass + + @pytest.mark.asyncio + async def test_list_with_name_contains_filter(self, tags_api_async): + """Test tag listing with name contains filtering.""" + # Create test tags with a common substring + test_prefix = f"test_contains_{datetime.now(timezone.utc).timestamp()}" + created_tag1 = await tags_api_async.create(f"{test_prefix}_alpha") + created_tag2 = await tags_api_async.create(f"{test_prefix}_beta") + + try: + # Filter by substring + filtered_tags = await tags_api_async.list_(name_contains=test_prefix) + + assert isinstance(filtered_tags, list) + assert len(filtered_tags) >= 2 + + # Verify all returned tags contain the substring + tag_names = [tag.name for tag in filtered_tags] + assert created_tag1.name in tag_names + assert created_tag2.name in tag_names + + for tag in filtered_tags: + assert test_prefix in tag.name + finally: + pass + + @pytest.mark.asyncio + async def test_list_with_name_regex_filter(self, tags_api_async): + """Test tag listing with regex filtering.""" + # Create test tags + test_prefix = f"test_regex_{datetime.now(timezone.utc).timestamp()}" + created_tag1 = await tags_api_async.create(f"{test_prefix}_123") + created_tag2 = await tags_api_async.create(f"{test_prefix}_456") + + try: + # Filter with regex pattern for tags ending in digits + filtered_tags = await tags_api_async.list_(name_regex=f"{test_prefix}_\\d+") + + assert isinstance(filtered_tags, list) + assert len(filtered_tags) >= 2 + + # Verify the tags match + tag_names = [tag.name for tag in filtered_tags] + assert created_tag1.name in tag_names + assert created_tag2.name in tag_names + finally: + pass + + @pytest.mark.asyncio + async def test_list_with_names_filter(self, tags_api_async): + """Test tag listing with multiple names filter.""" + # Create test tags + test_prefix = f"test_names_{datetime.now(timezone.utc).timestamp()}" + tag1_name = f"{test_prefix}_one" + tag2_name = f"{test_prefix}_two" + created_tag1 = await tags_api_async.create(tag1_name) + created_tag2 = await tags_api_async.create(tag2_name) + + try: + # Filter by list of names + filtered_tags = await tags_api_async.list_(names=[tag1_name, tag2_name]) + + assert isinstance(filtered_tags, list) + assert len(filtered_tags) == 2 + + tag_names = {tag.name for tag in filtered_tags} + assert tag_names == {tag1_name, tag2_name} + + # Verify the created tags are in the results + assert created_tag1.id_ in {tag.id_ for tag in filtered_tags} + assert created_tag2.id_ in {tag.id_ for tag in filtered_tags} + finally: + pass + + @pytest.mark.asyncio + async def test_list_with_tag_ids_filter(self, tags_api_async): + """Test tag listing with tag IDs filter.""" + # Create test tags + test_prefix = f"test_ids_{datetime.now(timezone.utc).timestamp()}" + created_tag1 = await tags_api_async.create(f"{test_prefix}_1") + created_tag2 = await tags_api_async.create(f"{test_prefix}_2") + + try: + # Filter by tag IDs + filtered_tags = await tags_api_async.list_( + tag_ids=[created_tag1.id_, created_tag2.id_] + ) + + assert isinstance(filtered_tags, list) + assert len(filtered_tags) == 2 + + tag_ids = {tag.id_ for tag in filtered_tags} + assert tag_ids == {created_tag1.id_, created_tag2.id_} + finally: + pass + + @pytest.mark.asyncio + async def test_list_with_limit(self, tags_api_async): + """Test tag listing with different limits.""" + # Test with limit of 1 + tags_1 = await tags_api_async.list_(limit=1) + assert isinstance(tags_1, list) + assert len(tags_1) <= 1 + + # Test with limit of 3 + tags_3 = await tags_api_async.list_(limit=3) + assert isinstance(tags_3, list) + assert len(tags_3) <= 3 + + @pytest.mark.asyncio + async def test_list_include_archived(self, tags_api_async): + """Test tag listing with archived tags included.""" + # Test without archived tags (default) + tags_active = await tags_api_async.list_(limit=5, include_archived=False) + assert isinstance(tags_active, list) + + # Test with archived tags included + tags_all = await tags_api_async.list_(limit=5, include_archived=True) + assert isinstance(tags_all, list) + + # Should have at least as many tags when including archived + assert len(tags_all) >= len(tags_active) + + class TestFind: + """Tests for the async find method.""" + + @pytest.mark.asyncio + async def test_find_tag(self, tags_api_async): + """Test finding a single tag.""" + # Create a test tag + test_tag_name = f"test_find_{datetime.now(timezone.utc).timestamp()}" + created_tag = await tags_api_async.create(test_tag_name) + + try: + # Find the tag by name + found_tag = await tags_api_async.find(name=test_tag_name) + + assert found_tag is not None + assert found_tag.id_ == created_tag.id_ + assert found_tag.name == test_tag_name + finally: + pass + + @pytest.mark.asyncio + async def test_find_nonexistent_tag(self, tags_api_async): + """Test finding a non-existent tag returns None.""" + found_tag = await tags_api_async.find( + name=f"nonexistent_tag_{datetime.now(timezone.utc).timestamp()}" + ) + assert found_tag is None + + @pytest.mark.asyncio + async def test_find_multiple_raises_error(self, tags_api_async): + """Test finding multiple tags raises an error.""" + # Create multiple tags with similar names + test_prefix = f"test_multi_{datetime.now(timezone.utc).timestamp()}" + created_tag1 = await tags_api_async.create(f"{test_prefix}_1") + created_tag2 = await tags_api_async.create(f"{test_prefix}_2") + + try: + # Verify both tags exist + assert created_tag1.id_ is not None + assert created_tag2.id_ is not None + + # Try to find with a filter that matches multiple tags + with pytest.raises(ValueError, match="Multiple tags found"): + await tags_api_async.find(name_contains=test_prefix) + finally: + pass + + class TestCreate: + """Tests for the async create method.""" + + @pytest.mark.asyncio + async def test_create_basic_tag(self, tags_api_async): + """Test creating a basic tag.""" + tag_name = f"test_create_{datetime.now(timezone.utc).timestamp()}" + + created_tag = await tags_api_async.create(tag_name) + + # Verify the tag was created + assert created_tag is not None + assert isinstance(created_tag, Tag) + assert created_tag.id_ is not None + assert created_tag.name == tag_name + assert created_tag.created_date is not None + + @pytest.mark.asyncio + async def test_create_tag_is_persisted(self, tags_api_async): + """Test that a created tag can be retrieved.""" + tag_name = f"test_persist_{datetime.now(timezone.utc).timestamp()}" + + created_tag = await tags_api_async.create(tag_name) + + # Retrieve the tag + found_tag = await tags_api_async.find(name=tag_name) + + assert found_tag is not None + assert found_tag.id_ == created_tag.id_ + assert found_tag.name == tag_name + + class TestFindOrCreate: + """Tests for the async find_or_create method.""" + + @pytest.mark.asyncio + async def test_find_or_create_existing_tags(self, tags_api_async): + """Test find_or_create with existing tags.""" + # Create test tags + test_prefix = f"test_foc_exist_{datetime.now(timezone.utc).timestamp()}" + tag1_name = f"{test_prefix}_1" + tag2_name = f"{test_prefix}_2" + + tag1 = await tags_api_async.create(tag1_name) + tag2 = await tags_api_async.create(tag2_name) + + try: + # Find or create the existing tags + result_tags = await tags_api_async.find_or_create([tag1_name, tag2_name]) + + assert len(result_tags) == 2 + result_ids = {tag.id_ for tag in result_tags} + assert tag1.id_ in result_ids + assert tag2.id_ in result_ids + finally: + pass + + @pytest.mark.asyncio + async def test_find_or_create_new_tags(self, tags_api_async): + """Test find_or_create with new tags.""" + test_prefix = f"test_foc_new_{datetime.now(timezone.utc).timestamp()}" + tag1_name = f"{test_prefix}_1" + tag2_name = f"{test_prefix}_2" + + # Find or create tags that don't exist + result_tags = await tags_api_async.find_or_create([tag1_name, tag2_name]) + + assert len(result_tags) == 2 + result_names = {tag.name for tag in result_tags} + assert result_names == {tag1_name, tag2_name} + + # Verify all tags have IDs (were created) + for tag in result_tags: + assert tag.id_ is not None + + @pytest.mark.asyncio + async def test_find_or_create_mixed(self, tags_api_async): + """Test find_or_create with a mix of existing and new tags.""" + test_prefix = f"test_foc_mixed_{datetime.now(timezone.utc).timestamp()}" + existing_name = f"{test_prefix}_existing" + new_name = f"{test_prefix}_new" + + # Create one tag + existing_tag = await tags_api_async.create(existing_name) + + try: + # Find or create with one existing and one new + result_tags = await tags_api_async.find_or_create([existing_name, new_name]) + + assert len(result_tags) == 2 + result_names = {tag.name for tag in result_tags} + assert result_names == {existing_name, new_name} + + # Verify the existing tag has the same ID + existing_result = next(tag for tag in result_tags if tag.name == existing_name) + assert existing_result.id_ == existing_tag.id_ + + # Verify the new tag was created + new_result = next(tag for tag in result_tags if tag.name == new_name) + assert new_result.id_ is not None + finally: + pass + + class TestUpdate: + """Tests for the async update method.""" + + @pytest.mark.asyncio + async def test_update_not_implemented(self, tags_api_async): + """Test that update raises NotImplementedError.""" + # Create a test tag + tag_name = f"test_update_{datetime.now(timezone.utc).timestamp()}" + created_tag = await tags_api_async.create(tag_name) + + # Try to update (should raise NotImplementedError) + with pytest.raises(NotImplementedError, match="not supported"): + await tags_api_async.update(created_tag, {"name": "new_name"}) + + +class TestTagsAPISync: + """Test suite for the synchronous Tags API functionality. + + Only includes a single test for basic sync generation. No specific sync behavior difference tests are needed. + """ + + class TestList: + """Tests for the sync list method.""" + + def test_basic_list(self, tags_api_sync): + """Test basic synchronous tag listing functionality.""" + tags = tags_api_sync.list_(limit=5) + + # Verify we get a list + assert isinstance(tags, list) + + # If we have tags, verify their structure + if tags: + assert isinstance(tags[0], Tag) + + class TestCreate: + """Tests for the sync create method.""" + + def test_create_basic_tag(self, tags_api_sync): + """Test creating a basic tag synchronously.""" + tag_name = f"test_sync_create_{datetime.now(timezone.utc).timestamp()}" + + created_tag = tags_api_sync.create(tag_name) + + # Verify the tag was created + assert created_tag is not None + assert isinstance(created_tag, Tag) + assert created_tag.id_ is not None + assert created_tag.name == tag_name + + class TestFindOrCreate: + """Tests for the sync find_or_create method.""" + + def test_find_or_create(self, tags_api_sync): + """Test synchronous find_or_create.""" + test_prefix = f"test_sync_foc_{datetime.now(timezone.utc).timestamp()}" + tag1_name = f"{test_prefix}_1" + tag2_name = f"{test_prefix}_2" + + # Find or create tags + result_tags = tags_api_sync.find_or_create([tag1_name, tag2_name]) + + assert len(result_tags) == 2 + result_names = {tag.name for tag in result_tags} + assert result_names == {tag1_name, tag2_name} diff --git a/python/lib/sift_client/resources/reports.py b/python/lib/sift_client/resources/reports.py index fa63e2525..e6fc111e3 100644 --- a/python/lib/sift_client/resources/reports.py +++ b/python/lib/sift_client/resources/reports.py @@ -90,6 +90,7 @@ async def list_( created_before: Filter reports created before this datetime. modified_after: Filter reports modified after this datetime. modified_before: Filter reports modified before this datetime. + Returns: A list of Reports that matches the filter. """ diff --git a/python/lib/sift_client/resources/tags.py b/python/lib/sift_client/resources/tags.py index e7ccbdcf5..9d12b32c2 100644 --- a/python/lib/sift_client/resources/tags.py +++ b/python/lib/sift_client/resources/tags.py @@ -69,7 +69,7 @@ async def list_( # Build CEL filter filter_parts = [ *self._build_name_cel_filters( - name=name, name_contains=name_contains, name_regex=name_regex, names=names + name=name, name_contains=name_contains, name_regex=name_regex ), *self._build_time_cel_filters( created_after=created_after, @@ -84,7 +84,9 @@ async def list_( filter_query=filter_query, ), ] - + + if names: + filter_parts.append(cel.in_("name", names)) if tag_ids: filter_parts.append(cel.in_("tag_id", tag_ids)) From 014cf0f2dbd3872d4be036d1270fd53cfe2e8e80 Mon Sep 17 00:00:00 2001 From: Ian Later Date: Mon, 13 Oct 2025 16:37:22 -0700 Subject: [PATCH 05/12] test tags working --- python/lib/sift_client/_tests/conftest.py | 2 +- .../sift_client/_tests/resources/test_tags.py | 516 ++++-------------- python/lib/sift_client/resources/_base.py | 1 + .../resources/sync_stubs/__init__.pyi | 36 +- python/lib/sift_client/resources/tags.py | 23 - 5 files changed, 148 insertions(+), 430 deletions(-) diff --git a/python/lib/sift_client/_tests/conftest.py b/python/lib/sift_client/_tests/conftest.py index 3378ec03f..d940027fd 100644 --- a/python/lib/sift_client/_tests/conftest.py +++ b/python/lib/sift_client/_tests/conftest.py @@ -25,7 +25,7 @@ def sift_client() -> SiftClient: api_key=api_key, grpc_url=grpc_url, rest_url=rest_url, - use_ssl=False, + # use_ssl=True, ) ) diff --git a/python/lib/sift_client/_tests/resources/test_tags.py b/python/lib/sift_client/_tests/resources/test_tags.py index 6f374e79f..48dfa2f8c 100644 --- a/python/lib/sift_client/_tests/resources/test_tags.py +++ b/python/lib/sift_client/_tests/resources/test_tags.py @@ -8,6 +8,7 @@ """ from datetime import datetime, timezone +import re import pytest @@ -25,403 +26,118 @@ def test_client_binding(sift_client): assert isinstance(sift_client.async_.tags, TagsAPIAsync) -@pytest.fixture -def tags_api_async(sift_client: SiftClient): - """Get the async tags API instance.""" - return sift_client.async_.tags - - -@pytest.fixture -def tags_api_sync(sift_client: SiftClient): - """Get the synchronous tags API instance.""" - return sift_client.tags - - -@pytest.fixture -def test_tag(tags_api_sync): - """Get an existing tag for testing.""" - tags = tags_api_sync.list_(limit=1) - if not tags: - # Create a tag if none exists - tag = tags_api_sync.create(f"test_tag_{datetime.now(timezone.utc).isoformat()}") - return tag - return tags[0] - - -class TestTagsAPIAsync: - """Test suite for the async Tags API functionality.""" - - class TestList: - """Tests for the async list method.""" - - @pytest.mark.asyncio - async def test_basic_list(self, tags_api_async): - """Test basic tag listing functionality.""" - tags = await tags_api_async.list_(limit=5) - - # Verify we get a list - assert isinstance(tags, list) - - # If we have tags, verify their structure - if tags: - tag = tags[0] - assert isinstance(tag, Tag) - assert tag.id_ is not None - assert tag.name is not None - - @pytest.mark.asyncio - async def test_list_with_name_filter(self, tags_api_async): - """Test tag listing with name filtering.""" - # Create a test tag with a unique name - test_tag_name = f"test_tag_exact_{datetime.now(timezone.utc).timestamp()}" - created_tag = await tags_api_async.create(test_tag_name) - - try: - # Filter by exact name - filtered_tags = await tags_api_async.list_(name=test_tag_name) - - # Should find exactly one tag with this name - assert isinstance(filtered_tags, list) - assert len(filtered_tags) == 1 - assert filtered_tags[0].name == test_tag_name - assert filtered_tags[0].id_ == created_tag.id_ - finally: - # Note: No archive method for tags, they persist - pass - - @pytest.mark.asyncio - async def test_list_with_name_contains_filter(self, tags_api_async): - """Test tag listing with name contains filtering.""" - # Create test tags with a common substring - test_prefix = f"test_contains_{datetime.now(timezone.utc).timestamp()}" - created_tag1 = await tags_api_async.create(f"{test_prefix}_alpha") - created_tag2 = await tags_api_async.create(f"{test_prefix}_beta") - - try: - # Filter by substring - filtered_tags = await tags_api_async.list_(name_contains=test_prefix) - - assert isinstance(filtered_tags, list) - assert len(filtered_tags) >= 2 - - # Verify all returned tags contain the substring - tag_names = [tag.name for tag in filtered_tags] - assert created_tag1.name in tag_names - assert created_tag2.name in tag_names - - for tag in filtered_tags: - assert test_prefix in tag.name - finally: - pass - - @pytest.mark.asyncio - async def test_list_with_name_regex_filter(self, tags_api_async): - """Test tag listing with regex filtering.""" - # Create test tags - test_prefix = f"test_regex_{datetime.now(timezone.utc).timestamp()}" - created_tag1 = await tags_api_async.create(f"{test_prefix}_123") - created_tag2 = await tags_api_async.create(f"{test_prefix}_456") - - try: - # Filter with regex pattern for tags ending in digits - filtered_tags = await tags_api_async.list_(name_regex=f"{test_prefix}_\\d+") - - assert isinstance(filtered_tags, list) - assert len(filtered_tags) >= 2 - - # Verify the tags match - tag_names = [tag.name for tag in filtered_tags] - assert created_tag1.name in tag_names - assert created_tag2.name in tag_names - finally: - pass - - @pytest.mark.asyncio - async def test_list_with_names_filter(self, tags_api_async): - """Test tag listing with multiple names filter.""" - # Create test tags - test_prefix = f"test_names_{datetime.now(timezone.utc).timestamp()}" - tag1_name = f"{test_prefix}_one" - tag2_name = f"{test_prefix}_two" - created_tag1 = await tags_api_async.create(tag1_name) - created_tag2 = await tags_api_async.create(tag2_name) - - try: - # Filter by list of names - filtered_tags = await tags_api_async.list_(names=[tag1_name, tag2_name]) - - assert isinstance(filtered_tags, list) - assert len(filtered_tags) == 2 - - tag_names = {tag.name for tag in filtered_tags} - assert tag_names == {tag1_name, tag2_name} - - # Verify the created tags are in the results - assert created_tag1.id_ in {tag.id_ for tag in filtered_tags} - assert created_tag2.id_ in {tag.id_ for tag in filtered_tags} - finally: - pass - - @pytest.mark.asyncio - async def test_list_with_tag_ids_filter(self, tags_api_async): - """Test tag listing with tag IDs filter.""" - # Create test tags - test_prefix = f"test_ids_{datetime.now(timezone.utc).timestamp()}" - created_tag1 = await tags_api_async.create(f"{test_prefix}_1") - created_tag2 = await tags_api_async.create(f"{test_prefix}_2") - - try: - # Filter by tag IDs - filtered_tags = await tags_api_async.list_( - tag_ids=[created_tag1.id_, created_tag2.id_] - ) - - assert isinstance(filtered_tags, list) - assert len(filtered_tags) == 2 - - tag_ids = {tag.id_ for tag in filtered_tags} - assert tag_ids == {created_tag1.id_, created_tag2.id_} - finally: - pass - - @pytest.mark.asyncio - async def test_list_with_limit(self, tags_api_async): - """Test tag listing with different limits.""" - # Test with limit of 1 - tags_1 = await tags_api_async.list_(limit=1) - assert isinstance(tags_1, list) - assert len(tags_1) <= 1 - - # Test with limit of 3 - tags_3 = await tags_api_async.list_(limit=3) - assert isinstance(tags_3, list) - assert len(tags_3) <= 3 - - @pytest.mark.asyncio - async def test_list_include_archived(self, tags_api_async): - """Test tag listing with archived tags included.""" - # Test without archived tags (default) - tags_active = await tags_api_async.list_(limit=5, include_archived=False) - assert isinstance(tags_active, list) - - # Test with archived tags included - tags_all = await tags_api_async.list_(limit=5, include_archived=True) - assert isinstance(tags_all, list) - - # Should have at least as many tags when including archived - assert len(tags_all) >= len(tags_active) - - class TestFind: - """Tests for the async find method.""" - - @pytest.mark.asyncio - async def test_find_tag(self, tags_api_async): - """Test finding a single tag.""" - # Create a test tag - test_tag_name = f"test_find_{datetime.now(timezone.utc).timestamp()}" - created_tag = await tags_api_async.create(test_tag_name) - - try: - # Find the tag by name - found_tag = await tags_api_async.find(name=test_tag_name) - - assert found_tag is not None - assert found_tag.id_ == created_tag.id_ - assert found_tag.name == test_tag_name - finally: - pass - - @pytest.mark.asyncio - async def test_find_nonexistent_tag(self, tags_api_async): - """Test finding a non-existent tag returns None.""" - found_tag = await tags_api_async.find( - name=f"nonexistent_tag_{datetime.now(timezone.utc).timestamp()}" - ) - assert found_tag is None - - @pytest.mark.asyncio - async def test_find_multiple_raises_error(self, tags_api_async): - """Test finding multiple tags raises an error.""" - # Create multiple tags with similar names - test_prefix = f"test_multi_{datetime.now(timezone.utc).timestamp()}" - created_tag1 = await tags_api_async.create(f"{test_prefix}_1") - created_tag2 = await tags_api_async.create(f"{test_prefix}_2") - - try: - # Verify both tags exist - assert created_tag1.id_ is not None - assert created_tag2.id_ is not None - - # Try to find with a filter that matches multiple tags - with pytest.raises(ValueError, match="Multiple tags found"): - await tags_api_async.find(name_contains=test_prefix) - finally: - pass - - class TestCreate: - """Tests for the async create method.""" - - @pytest.mark.asyncio - async def test_create_basic_tag(self, tags_api_async): - """Test creating a basic tag.""" - tag_name = f"test_create_{datetime.now(timezone.utc).timestamp()}" - - created_tag = await tags_api_async.create(tag_name) - - # Verify the tag was created - assert created_tag is not None - assert isinstance(created_tag, Tag) - assert created_tag.id_ is not None - assert created_tag.name == tag_name - assert created_tag.created_date is not None - - @pytest.mark.asyncio - async def test_create_tag_is_persisted(self, tags_api_async): - """Test that a created tag can be retrieved.""" - tag_name = f"test_persist_{datetime.now(timezone.utc).timestamp()}" - - created_tag = await tags_api_async.create(tag_name) - - # Retrieve the tag - found_tag = await tags_api_async.find(name=tag_name) - - assert found_tag is not None - assert found_tag.id_ == created_tag.id_ - assert found_tag.name == tag_name - - class TestFindOrCreate: - """Tests for the async find_or_create method.""" - - @pytest.mark.asyncio - async def test_find_or_create_existing_tags(self, tags_api_async): - """Test find_or_create with existing tags.""" - # Create test tags - test_prefix = f"test_foc_exist_{datetime.now(timezone.utc).timestamp()}" - tag1_name = f"{test_prefix}_1" - tag2_name = f"{test_prefix}_2" - - tag1 = await tags_api_async.create(tag1_name) - tag2 = await tags_api_async.create(tag2_name) - - try: - # Find or create the existing tags - result_tags = await tags_api_async.find_or_create([tag1_name, tag2_name]) - - assert len(result_tags) == 2 - result_ids = {tag.id_ for tag in result_tags} - assert tag1.id_ in result_ids - assert tag2.id_ in result_ids - finally: - pass - - @pytest.mark.asyncio - async def test_find_or_create_new_tags(self, tags_api_async): - """Test find_or_create with new tags.""" - test_prefix = f"test_foc_new_{datetime.now(timezone.utc).timestamp()}" - tag1_name = f"{test_prefix}_1" - tag2_name = f"{test_prefix}_2" - - # Find or create tags that don't exist - result_tags = await tags_api_async.find_or_create([tag1_name, tag2_name]) - - assert len(result_tags) == 2 - result_names = {tag.name for tag in result_tags} - assert result_names == {tag1_name, tag2_name} - - # Verify all tags have IDs (were created) - for tag in result_tags: - assert tag.id_ is not None - - @pytest.mark.asyncio - async def test_find_or_create_mixed(self, tags_api_async): - """Test find_or_create with a mix of existing and new tags.""" - test_prefix = f"test_foc_mixed_{datetime.now(timezone.utc).timestamp()}" - existing_name = f"{test_prefix}_existing" - new_name = f"{test_prefix}_new" - - # Create one tag - existing_tag = await tags_api_async.create(existing_name) - - try: - # Find or create with one existing and one new - result_tags = await tags_api_async.find_or_create([existing_name, new_name]) - - assert len(result_tags) == 2 - result_names = {tag.name for tag in result_tags} - assert result_names == {existing_name, new_name} - - # Verify the existing tag has the same ID - existing_result = next(tag for tag in result_tags if tag.name == existing_name) - assert existing_result.id_ == existing_tag.id_ - - # Verify the new tag was created - new_result = next(tag for tag in result_tags if tag.name == new_name) - assert new_result.id_ is not None - finally: - pass - - class TestUpdate: - """Tests for the async update method.""" - - @pytest.mark.asyncio - async def test_update_not_implemented(self, tags_api_async): - """Test that update raises NotImplementedError.""" - # Create a test tag - tag_name = f"test_update_{datetime.now(timezone.utc).timestamp()}" - created_tag = await tags_api_async.create(tag_name) - - # Try to update (should raise NotImplementedError) - with pytest.raises(NotImplementedError, match="not supported"): - await tags_api_async.update(created_tag, {"name": "new_name"}) - - -class TestTagsAPISync: - """Test suite for the synchronous Tags API functionality. - - Only includes a single test for basic sync generation. No specific sync behavior difference tests are needed. - """ - - class TestList: - """Tests for the sync list method.""" - - def test_basic_list(self, tags_api_sync): - """Test basic synchronous tag listing functionality.""" - tags = tags_api_sync.list_(limit=5) - - # Verify we get a list - assert isinstance(tags, list) - - # If we have tags, verify their structure - if tags: - assert isinstance(tags[0], Tag) - - class TestCreate: - """Tests for the sync create method.""" - - def test_create_basic_tag(self, tags_api_sync): - """Test creating a basic tag synchronously.""" - tag_name = f"test_sync_create_{datetime.now(timezone.utc).timestamp()}" - - created_tag = tags_api_sync.create(tag_name) - - # Verify the tag was created - assert created_tag is not None - assert isinstance(created_tag, Tag) - assert created_tag.id_ is not None - assert created_tag.name == tag_name - - class TestFindOrCreate: - """Tests for the sync find_or_create method.""" - - def test_find_or_create(self, tags_api_sync): - """Test synchronous find_or_create.""" - test_prefix = f"test_sync_foc_{datetime.now(timezone.utc).timestamp()}" - tag1_name = f"{test_prefix}_1" - tag2_name = f"{test_prefix}_2" - - # Find or create tags - result_tags = tags_api_sync.find_or_create([tag1_name, tag2_name]) - - assert len(result_tags) == 2 - result_names = {tag.name for tag in result_tags} - assert result_names == {tag1_name, tag2_name} +@pytest.fixture(scope="session") +def test_timestamp(): + """Setup a test tag for the session.""" + timestamp = datetime.now(timezone.utc) + yield timestamp + + +@pytest.fixture(scope="session") +def test_timestamp_str(test_timestamp): + """Setup a test tag for the session.""" + return test_timestamp.isoformat() + + +@pytest.fixture(scope="session") +def test_tags(sift_client, test_timestamp_str): + """Setup test tags for the session.""" + tag1 = sift_client.tags.create(f"test_tag1_{test_timestamp_str}") + tag2 = sift_client.tags.create(f"test_tag2_{test_timestamp_str}") + yield tag1, tag2 + # Would like to archive the tags, but this is not supported by the API + +class TestTags: + """Tests for the Tags API.""" + + def test_basic_list(self, sift_client, test_tags, test_timestamp_str): + """Test basic tag listing functionality.""" + tags = sift_client.tags.list_(limit=5) + + # Verify we get a list + assert isinstance(tags, list) + + # If we have tags, verify their structure + tag = tags[0] + assert isinstance(tag, Tag) + assert tag.id_ is not None + assert tag.name is not None + + def test_list_with_name_filter(self, sift_client, test_tags, test_timestamp_str): + """Test tag listing with name filtering.""" + # Create a test tag with a unique name + name_filter = f"test_tag1_{test_timestamp_str}" + name_filter_contains = f"tag1_{test_timestamp_str}" + name_filter_regex = re.compile(fr".*_tag.+_{re.escape(test_timestamp_str)}") + + filtered_tags = sift_client.tags.list_(name=name_filter) + filtered_tags_contains = sift_client.tags.list_(name_contains=name_filter_contains) + filtered_tags_regex = sift_client.tags.list_(name_regex=name_filter_regex) + # Should find exactly one tag with this name + assert isinstance(filtered_tags, list) + assert len(filtered_tags) == 1 + assert filtered_tags[0].name == name_filter + assert filtered_tags_contains[0].name == test_tags[0].name + assert filtered_tags_regex[0].name == test_tags[0].name + assert filtered_tags[0].id_ == test_tags[0].id_ + assert filtered_tags_contains[0].id_ == test_tags[0].id_ + assert filtered_tags_regex[0].id_ == test_tags[0].id_ + + + def test_find_tag(self, sift_client, test_tags, test_timestamp_str): + """Test finding a single tag. Excercises find and list_ limit functionality.""" + # Create a test tag + test_tag_name = f"test_tag1_{test_timestamp_str}" + + found_tag = sift_client.tags.find(name=test_tag_name) + + assert found_tag is not None + assert found_tag.id_ == test_tags[0].id_ + + def test_find_nonexistent_tag(self, sift_client): + """Test finding a non-existent tag returns None.""" + found_tag = sift_client.tags.find( + name=f"nonexistent_tag_{datetime.now(timezone.utc).timestamp()}" + ) + assert found_tag is None + + def test_find_multiple_raises_error(self, sift_client, test_timestamp_str, test_tags): + """Test finding multiple tags raises an error.""" + # Create multiple tags with similar names + name_filter_regex = re.compile(fr".*_tag(1|2)_{re.escape(test_timestamp_str)}") + with pytest.raises(ValueError, match="Multiple tags found"): + res = sift_client.tags.find(name_regex=name_filter_regex) + print(res) + + def test_find_or_create_existing_tags(self, sift_client, test_timestamp_str, test_tags): + """Test find_or_create with existing tags.""" + # Find or create the existing tags + existing_tag_names = [tag.name for tag in test_tags] + result_tags = sift_client.tags.find_or_create(existing_tag_names) + + assert len(result_tags) == 2 + result_ids = {tag.id_ for tag in result_tags} + assert test_tags[0].id_ in result_ids + assert test_tags[1].id_ in result_ids + + def test_find_or_create_new_tags(self, sift_client, test_timestamp_str, test_tags): + """Test find_or_create with new tags.""" + new_tag_name = f"test_find_or_create_new_{test_timestamp_str}" + + # Find or create tags that don't exist + existing_tag_names = [tag.name for tag in test_tags] + result_tags = sift_client.tags.find_or_create({*existing_tag_names, new_tag_name}) + + assert len(result_tags) == len(existing_tag_names) + 1 + result_names = {tag.name for tag in result_tags} + assert result_names == {*existing_tag_names, new_tag_name} + + # Verify all tags have IDs (were created) + for tag in result_tags: + assert tag.id_ is not None + + def test_update_not_implemented(self, sift_client, test_tags): + """Test that update raises NotImplementedError.""" + # Try to update (should raise NotImplementedError) + with pytest.raises(NotImplementedError, match="not supported"): + sift_client.tags.update(test_tags[0], {"name": "new_name"}) diff --git a/python/lib/sift_client/resources/_base.py b/python/lib/sift_client/resources/_base.py index 377ac0ebb..dad38eb4c 100644 --- a/python/lib/sift_client/resources/_base.py +++ b/python/lib/sift_client/resources/_base.py @@ -58,6 +58,7 @@ def _build_name_cel_filters( filter_parts.append(cel.contains("name", name_contains)) if name_regex: filter_parts.append(cel.match("name", name_regex)) + print(filter_parts) return filter_parts def _build_time_cel_filters( diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 1c4ec76c7..49c763e00 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -623,17 +623,23 @@ class ReportsAPI: name: str | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, - description: str | None = None, + names: list[str] | None = None, description_contains: str | None = None, run_id: str | None = None, organization_id: str | None = None, report_template_id: str | None = None, metadata: dict[str, str | float | bool] | None = None, tag_name: str | None = None, - created_by_user_id: str | None = None, + created_by: str | None = None, + modified_by: str | None = None, order_by: str | None = None, limit: int | None = None, include_archived: bool = False, + filter_query: str | None = None, + created_after: datetime | None = None, + created_before: datetime | None = None, + modified_after: datetime | None = None, + modified_before: datetime | None = None, ) -> list[Report]: """List reports with optional filtering. @@ -641,17 +647,23 @@ class ReportsAPI: name: Exact name of the report. name_contains: Partial name of the report. name_regex: Regular expression string to filter reports by name. - description: Exact description of the report. + names: List of report names to filter by. description_contains: Partial description of the report. run_id: Run ID to filter by. organization_id: Organization ID to filter by. report_template_id: Report template ID to filter by. metadata: Metadata to filter by. tag_name: Tag name to filter by. - created_by_user_id: The user ID of the creator of the reports. + created_by: The user ID of the creator of the reports. + modified_by: The user ID of the last modifier of the reports. order_by: How to order the retrieved reports. limit: How many reports to retrieve. If None, retrieves all matches. include_archived: Whether to include archived reports. + filter_query: Explicit CEL query to filter reports. + created_after: Filter reports created after this datetime. + created_before: Filter reports created before this datetime. + modified_after: Filter reports modified after this datetime. + modified_before: Filter reports modified before this datetime. Returns: A list of Reports that matches the filter. @@ -1049,7 +1061,13 @@ class TagsAPI: name_regex: str | re.Pattern | None = None, names: list[str] | None = None, tag_ids: list[str] | None = None, - created_by_user_id: str | None = None, + created_by: str | None = None, + modified_by: str | None = None, + filter_query: str | None = None, + created_after: datetime | None = None, + created_before: datetime | None = None, + modified_after: datetime | None = None, + modified_before: datetime | None = None, order_by: str | None = None, limit: int | None = None, ) -> list[Tag]: @@ -1061,7 +1079,13 @@ class TagsAPI: name_regex: Regular expression string to filter tags by name. names: List of tag names to filter by. tag_ids: List of tag IDs to filter by. - created_by_user_id: User ID who created the tag. + created_by: User ID who created the tag. + modified_by: User ID who last modified the tag. + filter_query: Explicit CEL query to filter tags. + created_after: Filter tags created after this datetime. + created_before: Filter tags created before this datetime. + modified_after: Filter tags modified after this datetime. + modified_before: Filter tags modified before this datetime. order_by: How to order the retrieved tags. limit: How many tags to retrieve. If None, retrieves all matches. diff --git a/python/lib/sift_client/resources/tags.py b/python/lib/sift_client/resources/tags.py index 9d12b32c2..15cc550d0 100644 --- a/python/lib/sift_client/resources/tags.py +++ b/python/lib/sift_client/resources/tags.py @@ -33,14 +33,7 @@ async def list_( name_regex: str | re.Pattern | None = None, names: list[str] | None = None, tag_ids: list[str] | None = None, - created_by: str | None = None, - modified_by: str | None = None, - include_archived: bool = False, filter_query: str | None = None, - created_after: datetime | None = None, - created_before: datetime | None = None, - modified_after: datetime | None = None, - modified_before: datetime | None = None, order_by: str | None = None, limit: int | None = None, ) -> list[Tag]: @@ -52,14 +45,7 @@ async def list_( name_regex: Regular expression string to filter tags by name. names: List of tag names to filter by. tag_ids: List of tag IDs to filter by. - created_by: User ID who created the tag. - modified_by: User ID who last modified the tag. - include_archived: Whether to include archived tags. filter_query: Explicit CEL query to filter tags. - created_after: Filter tags created after this datetime. - created_before: Filter tags created before this datetime. - modified_after: Filter tags modified after this datetime. - modified_before: Filter tags modified before this datetime. order_by: How to order the retrieved tags. limit: How many tags to retrieve. If None, retrieves all matches. @@ -71,16 +57,7 @@ async def list_( *self._build_name_cel_filters( name=name, name_contains=name_contains, name_regex=name_regex ), - *self._build_time_cel_filters( - created_after=created_after, - created_before=created_before, - modified_after=modified_after, - modified_before=modified_before, - created_by=created_by, - modified_by=modified_by, - ), *self._build_common_cel_filters( - include_archived=include_archived, filter_query=filter_query, ), ] From e3ab227049bde0156e539d5ab7f6f83acb1b6a32 Mon Sep 17 00:00:00 2001 From: Ian Later Date: Mon, 13 Oct 2025 19:00:56 -0700 Subject: [PATCH 06/12] Tests working --- .../_internal/low_level_wrappers/rules.py | 17 +- python/lib/sift_client/_tests/conftest.py | 14 + .../sift_client/_tests/integrated/reports.py | 149 --------- .../sift_client/_tests/integrated/rules.py | 298 ------------------ .../lib/sift_client/_tests/integrated/runs.py | 290 ----------------- .../_tests/resources/test_reports.py | 158 ++++++++++ .../_tests/resources/test_rules.py | 22 +- python/lib/sift_client/resources/_base.py | 6 +- python/lib/sift_client/resources/assets.py | 4 +- .../resources/calculated_channels.py | 8 +- python/lib/sift_client/resources/channels.py | 4 +- python/lib/sift_client/resources/reports.py | 49 +-- python/lib/sift_client/resources/rules.py | 19 +- python/lib/sift_client/resources/runs.py | 8 +- .../resources/sync_stubs/__init__.pyi | 32 +- python/lib/sift_client/resources/tags.py | 4 +- .../lib/sift_client/resources/test_results.py | 12 +- python/lib/sift_client/sift_types/report.py | 66 ++-- python/lib/sift_client/sift_types/rule.py | 70 ++-- python/lib/sift_client/sift_types/tag.py | 2 +- python/lib/sift_client/util/cel_utils.py | 4 +- 21 files changed, 377 insertions(+), 859 deletions(-) delete mode 100644 python/lib/sift_client/_tests/integrated/reports.py delete mode 100644 python/lib/sift_client/_tests/integrated/rules.py delete mode 100644 python/lib/sift_client/_tests/integrated/runs.py diff --git a/python/lib/sift_client/_internal/low_level_wrappers/rules.py b/python/lib/sift_client/_internal/low_level_wrappers/rules.py index 2843eef49..88483806b 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/rules.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/rules.py @@ -39,6 +39,7 @@ from sift_client._internal.low_level_wrappers.base import LowLevelClientBase from sift_client._internal.low_level_wrappers.reports import ReportsLowLevelClient +from sift_client._internal.util.timestamp import to_pb_timestamp from sift_client.sift_types.rule import ( Rule, RuleCreate, @@ -503,20 +504,21 @@ async def evaluate_rules( if start_time and end_time: if run_id: kwargs["run_time_range"] = RunTimeRange( - run=run_id, start_time=start_time, end_time=end_time + run=run_id, + start_time=to_pb_timestamp(start_time), + end_time=to_pb_timestamp(end_time), # type: ignore ) - else: kwargs["assets"] = AssetsTimeRange( - assets={"ids": {"ids": asset_ids}}, - start_time=start_time, - end_time=end_time, + assets={"ids": {"ids": asset_ids}}, # type: ignore + start_time=to_pb_timestamp(start_time), + end_time=to_pb_timestamp(end_time), ) elif run_id: kwargs["run"] = ResourceIdentifier(id=run_id) if all_applicable_rules: kwargs["all_applicable_rules"] = all_applicable_rules if rule_ids: - kwargs["rules"] = {"rules": ResourceIdentifiers(ids={"ids": rule_ids})} + kwargs["rules"] = {"rules": ResourceIdentifiers(ids={"ids": rule_ids})} # type: ignore if rule_version_ids: kwargs["rule_versions"] = rule_version_ids if report_template_id: @@ -528,9 +530,8 @@ async def evaluate_rules( if organization_id: kwargs["organization_id"] = organization_id - print("kwargs: ", kwargs) - request = EvaluateRulesRequest(**kwargs) + print("request: ", request) response = await self._grpc_client.get_stub(RuleEvaluationServiceStub).EvaluateRules( request ) diff --git a/python/lib/sift_client/_tests/conftest.py b/python/lib/sift_client/_tests/conftest.py index 6d232fb80..456e39bac 100644 --- a/python/lib/sift_client/_tests/conftest.py +++ b/python/lib/sift_client/_tests/conftest.py @@ -44,3 +44,17 @@ def mock_client(): client.async_ = MagicMock(spec=AsyncAPIs) client.async_.ingestion = MagicMock() return client + + +@pytest.fixture(scope="session") +def test_tag(sift_client): + tag = sift_client.tags.find_or_create(names=["test"])[0] + assert tag is not None + yield tag + + +@pytest.fixture(scope="session") +def ci_pytest_tag(sift_client): + tag = sift_client.tags.find_or_create(names=["sift-client-pytest"])[0] + assert tag is not None + yield tag diff --git a/python/lib/sift_client/_tests/integrated/reports.py b/python/lib/sift_client/_tests/integrated/reports.py deleted file mode 100644 index f3891bfb9..000000000 --- a/python/lib/sift_client/_tests/integrated/reports.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env python3 -"""This test demonstrates the usage of the Runs API. - -It creates a new run, updates it, and associates assets with it. -It also lists runs, filters them, and deletes the run. - -It uses the SiftClient to interact with the API. -""" - -import asyncio -import os - -from sift_client import SiftClient, SiftConnectionConfig -from sift_client.sift_types import ( - ChannelReference, - ReportRuleStatus, - RuleAction, - RuleAnnotationType, -) - - -async def main(): - """Main function demonstrating the Runs API usage.""" - # Initialize the client - # You can set these environment variables or pass them directly - grpc_url = os.getenv("SIFT_GRPC_URI", "localhost:50051") - rest_url = os.getenv("SIFT_REST_URI", "localhost:8080") - api_key = os.getenv("SIFT_LOCAL_API_KEY", "") - - client = SiftClient( - connection_config=SiftConnectionConfig( - api_key=api_key, - grpc_url=grpc_url, - rest_url=rest_url, - use_ssl=False, - ), - ) - - asset = client.assets.find(name="NostromoLV426") - asset_id = asset.id_ - print(f"Using asset: {asset.name} (ID: {asset_id})") - - # List runs for this asset - runs = asset.runs - print( - f"Found {len(runs)} run(s): {[run.name for run in runs]} for asset {asset.name} (ID: {asset_id})" - ) - - # Pick one. - run = runs[0] - run_id = run.id_ - print(f"Using run: {run.name} (ID: {run_id})") - - tags = client.tags.find_or_create(names=["test", "api-created"]) - tag_ids = [tag.id_ for tag in tags] - - rule = client.rules.create( - name="test_rule", - description="Test rule", - expression="$1 > 0.1", - channel_references=[ - ChannelReference(channel_reference="$1", channel_identifier="mainmotor.velocity"), - ], - action=RuleAction.annotation( - annotation_type=RuleAnnotationType.DATA_REVIEW, - tags_ids=tag_ids, - default_assignee_user_id=None, - ), - asset_ids=[asset_id], - ) - print(f"Created rule: {rule.name} (ID: {rule.id_})") - - report_from_rules = client.reports.create_from_rules( - name="report_from_rules", - run_id=run_id, - rule_ids=[rule.id_], - ) - print(f"Created report: {report_from_rules.name} (ID: {report_from_rules.id_})") - print(f"Report summaries: {report_from_rules.summaries}") - print(f"Report tags: {report_from_rules.tags}") - - report_from_applicable_rules_run = client.reports.create_from_applicable_rules( - organization_id=asset.organization_id, - name="report_from_applicable_rules_run", - run_id=run_id, - ) - print( - f"Created report: {report_from_applicable_rules_run.name} (ID: {report_from_applicable_rules_run.id_})" - ) - print(f"Report summaries: {report_from_applicable_rules_run.summaries}") - print(f"Report tags: {report_from_applicable_rules_run.tags}") - - job_id, rerun_report_id = client.reports.rerun(report=report_from_rules) - rerun_report = client.reports.get(report_id=rerun_report_id) - print(f"Rerun report: {rerun_report.name} (ID: {rerun_report.id_})") - print(f"Report summaries: {rerun_report.summaries}") - print(f"Report tags: {rerun_report.tags}") - - assert rerun_report.metadata == {} - updated_report = client.reports.update( - report=rerun_report, - update={ - "metadata": { - "test_type": "ci", - }, - }, - ) - print(f"Updated report: {updated_report.name} (ID: {updated_report.id_})") - - reports = client.reports.list_( - run_id=run_id, - organization_id=asset.organization_id, - ) - print(f"Found {len(reports)} report(s): {[report.name for report in reports]}") - - try: - client.reports.find(name=rerun_report.name) - except ValueError as e: - assert "Multiple reports found for query" in str(e) - - find_report = client.reports.find(name=rerun_report.name, metadata={"test_type": "ci"}) - - job_id, second_rerun_report_id = client.reports.rerun(report=report_from_rules) - print(f"Second rerun report: {second_rerun_report_id} (ID: {second_rerun_report_id})") - - client.reports.cancel(report=second_rerun_report_id) - canceled_report = client.reports.get(report_id=second_rerun_report_id) - print(f"Canceled report: {canceled_report.name} (ID: {canceled_report.id_})") - - client.rules.archive(rule_ids=[rule.id_]) - - report_ids = { - report_from_rules.id_, - report_from_applicable_rules_run.id_, - rerun_report.id_, - second_rerun_report_id, - } - for report_id in report_ids: - client.reports.archive(report=report_id) - - assert rerun_report.rerun_from_report_id == report_from_rules.id_ - assert find_report.id_ == rerun_report.id_ - for summary in canceled_report.summaries: - assert summary.status == ReportRuleStatus.CANCELED - print("All tests passed") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/lib/sift_client/_tests/integrated/rules.py b/python/lib/sift_client/_tests/integrated/rules.py deleted file mode 100644 index 8307ee4c2..000000000 --- a/python/lib/sift_client/_tests/integrated/rules.py +++ /dev/null @@ -1,298 +0,0 @@ -import os -from datetime import datetime, timezone - -from sift_client.client import SiftClient, SiftConnectionConfig - -# Import sift_client types for calculated channels and rules -from sift_client.sift_types import ( - ChannelReference, - RuleAction, - RuleAnnotationType, - RuleUpdate, -) - -""" -Comprehensive test script for rules with extensive update field exercises. - -This test demonstrates all available update fields for rules: -- name: Update the rule name -- description: Update the rule description -- expression: Update the rule expression -- channel_references: Update channel references (must be updated with expression) -- action: Update the rule action (annotation, notification, webhook) -- tag_ids: Update associated tags (TBD) -- contextual_channels: Update contextual channels -- version_notes: Update version notes - -The test also includes: -- Edge case testing (invalid expressions) -- Batch operations demonstration -- Comprehensive validation -- Archive operations - - -If we keep it as a test, we should ideally have a setup that populates data, and then ensure we teardown all the test assets/channels/rules etc. -""" - - -def main(): - grpc_url = os.getenv("SIFT_GRPC_URI", "http://localhost:50051") - api_key = os.getenv("SIFT_API_KEY", "") - rest_url = os.getenv("SIFT_REST_URI", "localhost:8080") - api_key = os.getenv("SIFT_LOCAL_API_KEY", "") - client = SiftClient( - connection_config=SiftConnectionConfig( - grpc_url=grpc_url, api_key=api_key, rest_url=rest_url, use_ssl=False - ) - ) - - asset = client.assets.find(name="NostromoLV426") - asset_id = asset.id_ - print(f"Using asset: {asset.name} (ID: {asset_id})") - - unique_name_suffix = datetime.now(tz=timezone.utc).strftime("%Y%m%d%H%M%S") - num_rules = 8 - print(f"\n=== Creating {num_rules} rules with unique suffix: {unique_name_suffix} ===") - tags = client.tags.find_or_create(names=["test", "initial"]) - tag_ids = [tag.id_ for tag in tags] - created_rules = [] - for i in range(num_rules): - rule = client.rules.create( - name=f"test_rule_{unique_name_suffix}_{i}", - description=f"Test rule {i} - initial description", - expression="$1 > 0.1", # Simple threshold check - channel_references=[ - ChannelReference(channel_reference="$1", channel_identifier="mainmotor.velocity"), - ], - action=RuleAction.annotation( - annotation_type=RuleAnnotationType.DATA_REVIEW, - tags_ids=tag_ids, - default_assignee_user_id=None, - ), - asset_ids=[asset_id], - ) - created_rules.append(rule) - print(f"Created rule: {rule.name} (ID: {rule.id_})") - - # Find the rules we just created - search_results = client.rules.list_( - name_regex=f"test_rule_{unique_name_suffix}.*", - ) - assert len(search_results) == num_rules, ( - f"Expected {num_rules} created rules, got {len(search_results)}" - ) - - print("\n=== Testing comprehensive update scenarios ===") - - # Test 1: Update expression and channel references together - print("\n--- Test 1: Update expression and channel references ---") - rule_1 = created_rules[0] - rule_1_model_dump = rule_1.model_dump() - updated_rule_1 = rule_1.update( - RuleUpdate( - expression="$1 > 0.5", # Higher threshold - channel_references=[ - ChannelReference(channel_reference="$1", channel_identifier="mainmotor.velocity"), - ], - ) - ) - updated_rule_1_model_dump = updated_rule_1.model_dump() - print(f"Updated {updated_rule_1.name}: expression = {updated_rule_1.expression}") - - # Test 2: Update description - print("\n--- Test 2: Update description ---") - rule_2 = created_rules[1] - updated_rule_2 = rule_2.update( - RuleUpdate( - description="Updated description with more details about velocity-to-voltage ratio monitoring", - ) - ) - print(f"Updated {updated_rule_2.name}: description = {updated_rule_2.description}") - - # Test 3: Update action (change annotation type and tags) - print("\n--- Test 3: Update action ---") - rule_3 = created_rules[2] - updated_tags = client.tags.find_or_create(names=["updated", "phase", "alert"]) - updated_tag_ids = [tag.id_ for tag in updated_tags] - updated_rule_3 = rule_3.update( - RuleUpdate( - action=RuleAction.annotation( - annotation_type=RuleAnnotationType.PHASE, - tags_ids=updated_tag_ids, - default_assignee_user_id=rule_3.created_by_user_id, - ), - ) - ) - print(f"Updated {updated_rule_3.name}: action type = {updated_rule_3.action.action_type}") - print(f" - annotation type: {updated_rule_3.action.annotation_type}") - print(f" - tags: {updated_rule_3.action.tags_ids}") - print(f" - assignee: {updated_rule_3.action.default_assignee_user_id}") - - # Test 4: Update name - print("\n--- Test 4: Update name ---") - rule_4 = created_rules[3] - new_name = f"renamed_rule_{unique_name_suffix}_4" - updated_rule_4 = rule_4.update( - RuleUpdate( - name=new_name, - ) - ) - print(f"Updated {rule_4.name} -> {updated_rule_4.name}") - - # Test 5: Update multiple fields at once - print("\n--- Test 5: Update multiple fields simultaneously ---") - rule_5 = created_rules[4] - updated_rule_5 = rule_5.update( - RuleUpdate( - description="Multi-field update test", - ), - version_notes="Updated via multi-field update", - ) - print(f"Updated {updated_rule_5.name}:") - print(f" - description: {updated_rule_5.description}") - print( - f" - version_notes: {updated_rule_5.rule_version.version_notes if updated_rule_5.rule_version else None}" - ) - - # Test 6: Update with complex expression - print("\n--- Test 6: Update with complex expression ---") - rule_6 = created_rules[5] - updated_rule_6 = rule_6.update( - RuleUpdate( - expression="$1 > 0.3 && $1 < 0.8", # Range check - channel_references=[ - ChannelReference(channel_reference="$1", channel_identifier="mainmotor.velocity"), - ], - ) - ) - print(f"Updated {updated_rule_6.name}: complex expression = {updated_rule_6.expression}") - - # Test 7: Update action to notification type - print("\n--- Test 7: Update action to notification ---") - rule_7 = created_rules[6] - updated_rule_7 = rule_7 - # Note: Notification actions are not supported yet. - # updated_rule_7 = rule_7.update( - # RuleUpdate( - # action=RuleAction.notification( - # notify_recipients=[rule_7.created_by_user_id] - # ), - # ) - # ) - # print(f"Updated {updated_rule_7.name}: action type = {updated_rule_7.action.action_type}") - # print(f" - notification recipients: {updated_rule_7.action.notification_recipients}") - - # Test 8: Update tag_ids and contextual_channels - print("\n--- Test 8: Update tag_ids and contextual_channels ---") - rule_8 = created_rules[7] - updated_rule_8 = rule_8.update( - RuleUpdate( - # tag_ids=["tag-123", "tag-456"], # Example tag IDs # TODO: Where are these IDs supposed to come from? They're supposed to be uuids? {grpc_message:"invalid argument: invalid input syntax for type uuid: \"tag-123\" - contextual_channels=["temperature", "pressure"], # Example contextual channels - ) - ) - print(f"Updated {updated_rule_8.name}:") - print(f" - asset_tag_ids: {updated_rule_8.asset_tag_ids}") - print(f" - contextual_channels: {updated_rule_8.contextual_channels}") - - # Test 8b: Edge case - Update with invalid expression (should fail gracefully) - print("\n--- Test 8b: Edge case - Invalid expression test ---") - try: - invalid_update = rule_8.update( - RuleUpdate( - expression="invalid_expression", - channel_references=[ - ChannelReference( - channel_reference="$1", channel_identifier="mainmotor.velocity" - ), - ], - ) - ) - print(f"Invalid expression update succeeded (unexpected): {invalid_update.expression}") - except Exception as e: - print(f"Invalid expression update failed as expected: {e}") - - # Test 9: Batch operations demonstration - print("\n--- Test 9: Batch operations demonstration ---") - all_updated_rules = [ - updated_rule_1, - updated_rule_2, - updated_rule_3, - updated_rule_4, - updated_rule_5, - updated_rule_6, - updated_rule_7, - updated_rule_8, - ] - - # Batch get the updated rules - rule_ids = [rule.id_ for rule in all_updated_rules] - batch_rules = client.rules.batch_get(rule_ids=rule_ids) - print(f"Batch retrieved {len(batch_rules)} rules:") - for rule in batch_rules: - print(f" - {rule.name}: {rule.expression}") - - # Test 10: Archive rules - print("\n--- Test 10: Archive rules ---") - client.rules.archive(rules=created_rules) - - print("\n=== Test Summary ===") - print(f"Created: {len(created_rules)} rules") - print(f"Updated: {len(all_updated_rules)} rules") - - # Verify all rules were processed - assert len(created_rules) == num_rules, ( - f"Expected {num_rules} created rules, got {len(created_rules)}" - ) - assert len(all_updated_rules) == num_rules, ( - f"Expected {num_rules} updated rules, got {len(all_updated_rules)}" - ) - - # Additional validation - print("\n=== Validation Checks ===") - - # Verify that updates actually changed the values - assert updated_rule_1.expression == "$1 > 0.5", ( - f"Expression update failed: {updated_rule_1.expression}" - ) - # For update 1, also verify that the fields that were not updated are not reset. - assert updated_rule_1_model_dump["description"] == rule_1_model_dump["description"], ( - f"Expected no description change, got {rule_1_model_dump['description']} -> {updated_rule_1.description}" - ) - assert ( - updated_rule_1_model_dump["channel_references"] == rule_1_model_dump["channel_references"] - ), ( - f"Expected no channel references change, got {rule_1_model_dump['channel_references']} -> {updated_rule_1.channel_references}" - ) - assert updated_rule_1_model_dump["asset_ids"] == rule_1_model_dump["asset_ids"], ( - f"Expected no asset IDs change, got {rule_1_model_dump['asset_ids']} -> {updated_rule_1.asset_ids}" - ) - assert updated_rule_1_model_dump["asset_tag_ids"] == rule_1_model_dump["asset_tag_ids"], ( - f"Expected no tag IDs change, got {rule_1_model_dump['asset_tag_ids']} -> {updated_rule_1.asset_tag_ids}" - ) - assert ( - updated_rule_1_model_dump["contextual_channels"] == rule_1_model_dump["contextual_channels"] - ), f"Contextual channels update failed: {updated_rule_1.contextual_channels}" - assert "more details" in updated_rule_2.description, ( - f"Description update failed: {updated_rule_2.description}" - ) - assert updated_rule_3.action.annotation_type == RuleAnnotationType.PHASE, ( - f"Action update failed: {updated_rule_3.action.annotation_type}" - ) - assert updated_rule_4.name == new_name, f"Name update failed: {updated_rule_4.name}" - - assert updated_rule_6.expression == "$1 > 0.3 && $1 < 0.8", ( - f"Complex expression update failed: {updated_rule_6.expression}" - ) - # assert updated_rule_7.action.action_type == RuleActionType.NOTIFICATION, f"Action type update failed: {updated_rule_7.action.action_type}" - # assert len(updated_rule_8.tag_ids) == 2, f"Tag IDs update failed: {updated_rule_8.tag_ids}" - assert len(updated_rule_8.contextual_channels) == 2, ( - f"Contextual channels update failed: {updated_rule_8.contextual_channels}" - ) - - print("All validation checks passed!") - print("\n=== Test completed successfully ===") - - -if __name__ == "__main__": - main() diff --git a/python/lib/sift_client/_tests/integrated/runs.py b/python/lib/sift_client/_tests/integrated/runs.py deleted file mode 100644 index 669cad507..000000000 --- a/python/lib/sift_client/_tests/integrated/runs.py +++ /dev/null @@ -1,290 +0,0 @@ -#!/usr/bin/env python3 -"""This test demonstrates the usage of the Runs API. - -It creates a new run, updates it, and associates assets with it. -It also lists runs, filters them, and deletes the run. - -It uses the SiftClient to interact with the API. -""" - -import asyncio -import os -from datetime import datetime, timedelta, timezone - -from sift_client import SiftClient, SiftConnectionConfig - - -async def main(): - """Main function demonstrating the Runs API usage.""" - # Initialize the client - # You can set these environment variables or pass them directly - grpc_url = os.getenv("SIFT_GRPC_URI", "localhost:50051") - rest_url = os.getenv("SIFT_REST_URI", "localhost:8080") - api_key = os.getenv("SIFT_API_KEY", "") - api_key = os.getenv("SIFT_LOCAL_API_KEY", "") - client = SiftClient( - connection_config=SiftConnectionConfig( - api_key=api_key, - grpc_url=grpc_url, - rest_url=rest_url, - use_ssl=False, - ), - ) - - # Use a known asset to fetch a run. - asset = client.assets.find(name="NostromoLV426") - asset_id = asset.id_ - print(f"Using asset: {asset.name} (ID: {asset_id})") - - # List runs for this asset - runs = asset.runs - print( - f"Found {len(runs)} run(s): {[run.name for run in runs]} for asset {asset.name} (ID: {asset_id})" - ) - - # Pick one. - run = runs[0] - run_id = run.id_ - print(f"Using run: {run.name} (ID: {run_id})") - - # List other assets for this run. - all_assets = run.assets - other_assets = [asset for asset in all_assets if asset.id_ != asset_id] - print( - f"Found {len(other_assets)} other asset(s): {other_assets} for run {run.name} (ID: {run_id})" - ) - - # Example 1: List all runs - print("\n1. Listing all runs...") - runs = client.runs.list_(limit=5) - print(f" Found {len(runs)} runs:") - for run in runs: - print(f" - {run.name} (ID: {run.id_}), Organization ID: {run.organization_id}") - - # Example 2: Test different filter options - print("\n2. Testing different filter options...") - - # Get a sample run for testing filters - sample_runs = client.runs.list_(limit=3) - if not sample_runs: - print(" No runs available for filter testing") - return - - sample_run = sample_runs[0] - - # 2a: Filter by exact name - print("\n 2a. Filter by exact name...") - run_name = sample_run.name - runs = client.runs.list_(name=run_name, limit=5) - print(f" Found {len(runs)} runs with exact name '{run_name}':") - for run in runs: - print(f" - {run.name} (ID: {run.id_})") - - # 2b: Filter by name containing text - print("\n 2b. Filter by name containing text...") - runs = client.runs.list_(name_contains="test", limit=5) - print(f" Found {len(runs)} runs with 'test' in name:") - for run in runs: - print(f" - {run.name}") - - # 2c: Filter by name using regex - print("\n 2c. Filter by name using regex...") - runs = client.runs.list_(name_regex=".*test.*", limit=5) - print(f" Found {len(runs)} runs with 'test' in name (regex):") - for run in runs: - print(f" - {run.name}") - - # 2d: Filter by exact description - print("\n 2d. Filter by exact description...") - if sample_run.description: - runs = client.runs.list_(description=sample_run.description, limit=5) - print(f" Found {len(runs)} runs with exact description '{sample_run.description}':") - for run in runs: - print(f" - {run.name}: {run.description}") - else: - print(" No description available for testing") - - # 2e: Filter by description containing text - print("\n 2e. Filter by description containing text...") - runs = client.runs.list_(description_contains="test", limit=5) - print(f" Found {len(runs)} runs with 'test' in description:") - for run in runs: - print(f" - {run.name}: {run.description}") - - # 2f: Filter by duration seconds - print("\n 2f. Filter by duration seconds...") - # Calculate duration for sample run if it has start and stop times - if sample_run.start_time and sample_run.stop_time: - duration_seconds = int((sample_run.stop_time - sample_run.start_time).total_seconds()) - runs = client.runs.list_(duration_seconds=duration_seconds, limit=5) - print(f" Found {len(runs)} runs with duration {duration_seconds} seconds:") - for run in runs: - if run.start_time and run.stop_time: - run_duration = int((run.stop_time - run.start_time).total_seconds()) - print(f" - {run.name} (duration: {run_duration}s)") - else: - print(" No start/stop times available for duration testing") - - # 2g: Filter by client key - print("\n 2g. Filter by client key...") - if sample_run.client_key: - runs = client.runs.list_(client_key=sample_run.client_key, limit=5) - print(f" Found {len(runs)} runs with client key '{sample_run.client_key}':") - for run in runs: - print(f" - {run.name} (client_key: {run.client_key})") - else: - print(" No client key available for testing") - - # 2h: Filter by asset ID - print("\n 2h. Filter by asset ID...") - if sample_run.asset_ids: - asset_id = sample_run.asset_ids[0] - runs = client.runs.list_(asset_id=asset_id, limit=5) - print(f" Found {len(runs)} runs associated with asset {asset_id}:") - for run in runs: - print(f" - {run.name} (asset_ids: {list(run.asset_ids)})") - else: - print(" No asset IDs available for testing") - - # 2i: Filter by asset name - print("\n 2i. Filter by asset name...") - runs = client.runs.list_(asset_name="NostromoLV426", limit=5) - print(f" Found {len(runs)} runs associated with asset 'NostromoLV426':") - for run in runs: - print(f" - {run.name}") - - # 2j: Filter by created by user ID - print("\n 2j. Filter by created by user ID...") - created_by_user_id = sample_run.created_by_user_id - runs = client.runs.list_(created_by_user_id=created_by_user_id, limit=5) - print(f" Found {len(runs)} runs created by user {created_by_user_id}:") - for run in runs: - print(f" - {run.name} (created by: {run.created_by_user_id})") - - # 2l: Test ordering options - print("\n 2l. Testing ordering options...") - - # Order by name ascending - runs = client.runs.list_(order_by="name", limit=3) - print(" First 3 runs ordered by name (ascending):") - for run in runs: - print(f" - {run.name}") - - # Order by name descending - runs = client.runs.list_(order_by="name desc", limit=3) - print(" First 3 runs ordered by name (descending):") - for run in runs: - print(f" - {run.name}") - - # Order by creation date (newest first - default) - runs = client.runs.list_(order_by="created_date desc", limit=3) - print(" First 3 runs ordered by creation date (newest first):") - for run in runs: - print(f" - {run.name} (created: {run.created_date})") - - # Order by creation date (oldest first) - runs = client.runs.list_(order_by="created_date", limit=3) - print(" First 3 runs ordered by creation date (oldest first):") - for run in runs: - print(f" - {run.name} (created: {run.created_date})") - - # Example 3: Find a single run by name - print("\n3. Finding a single run by name...") - run_name = "test-run" # Replace with an actual run name - run = client.runs.find(name=run_name) - if run: - print(f" Found run: {run.name}") - print(f" Description: {run.description}") - else: - print(f" No run found with name '{run_name}'") - - # Example 4: Create a new run - print("\n4. Creating a new run...") - # Create metadata for the run - metadata = { - "environment": "production", - "test_type": "integration", - } - - # Create a run with start and stop times - start_time = datetime.now(timezone.utc) - stop_time = start_time + timedelta(minutes=2) - - previously_created_runs = client.runs.list_(name_regex="Example Test Run.*") - if previously_created_runs: - print(f" Deleting previously created runs: {previously_created_runs}") - for run in previously_created_runs: - print(f" Deleting run: {run.name}") - client.runs.archive(run=run) - - new_run = client.runs.create( - name=f"Example Test Run {datetime.now(tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}", - description="A test run created via the API", - tags=["api-created", "test"], - start_time=start_time, - stop_time=stop_time, - # Use a unique client key for each run - client_key=f"example-run-key-{datetime.now(tz=timezone.utc).timestamp()}", - metadata=metadata, - ) - print(f" Created run: {new_run.name} (ID: {new_run.id_})") - print(f" Client key: {new_run.client_key}") - print(f" Tags: {new_run.tags}") - - # Example 5: Update a run - print("\n5. Updating a run...") - - run_to_update = new_run - print(f" Updating run: {run_to_update.name}") - - # Update the run - new_description = "Updated description via API" - new_metadata = { - "test_type": "ci", - } - new_tags = ["updated", "api-modified"] - updated_run = client.runs.update( - run=run_to_update, - update={ - "description": new_description, - "tags": new_tags, - "metadata": new_metadata, - }, - ) - print(f" Updated run: {updated_run.name}") - print(f" New description: {updated_run.description}") - print(f" New tags: {updated_run.tags}") - print(f" New metadata: {updated_run.metadata}") - assert updated_run.description == new_description - assert sorted(updated_run.tags) == sorted(new_tags) - assert updated_run.metadata == new_metadata - - # Example 6: Associate assets with a run - print("\n6. Associating assets with a run...") - ongoing_runs = client.runs.list_( - name_regex="Example Test Run.*", include_archived=True, is_stopped=False - ) - if ongoing_runs: - print(" Ensuring previously created runs are stopped:") - for run in ongoing_runs: - if run.stop_time is None: - print(f" Stopping run: {run.name}") - client.runs.stop(run=run) - - # Get a run to associate assets with - asset_names = ["asset1", "asset2"] # Replace with actual asset names - print(f" Associating assets {asset_names} with run: {new_run.name}") - - client.runs.create_automatic_association_for_assets(run=new_run, asset_names=asset_names) - print(f" Successfully associated assets with run: {new_run.name}") - - # Example 7: Delete a run - print("\n7. Deleting a run") - run_to_delete = new_run - print(f" Deleting run: {run_to_delete.name}") - client.runs.archive(run=run_to_delete) - print(f" Successfully archived run: {run_to_delete.name}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/lib/sift_client/_tests/resources/test_reports.py b/python/lib/sift_client/_tests/resources/test_reports.py index e69de29bb..b723dccf7 100644 --- a/python/lib/sift_client/_tests/resources/test_reports.py +++ b/python/lib/sift_client/_tests/resources/test_reports.py @@ -0,0 +1,158 @@ +import pytest + +from sift_client.resources import ReportsAPI, ReportsAPIAsync +from sift_client.sift_types import ( + ChannelReference, + ReportRuleStatus, + RuleAction, + RuleAnnotationType, +) + + +@pytest.fixture(scope="session") +def nostromo_asset(sift_client): + return sift_client.assets.find(name="NostromoLV426") + + +@pytest.fixture(scope="session") +def nostromo_run(nostromo_asset): + return nostromo_asset.runs[0] + + +@pytest.fixture(scope="session") +def tags(sift_client): + tags = sift_client.tags.find_or_create(names=["test", "api-created"]) + return tags + + +@pytest.fixture(scope="session") +def test_rule(sift_client, nostromo_asset, ci_pytest_tag): + rule = sift_client.rules.find(name="test_rule") + created_rule = None + if not rule: + created_rule = sift_client.rules.create( + { + "name": "test_rule", + "description": "Test rule", + "expression": "$1 > 0.1", + "assets": [nostromo_asset], + "channel_references": [ + ChannelReference( + channel_reference="$1", channel_identifier="mainmotor.velocity" + ), + ], + "action": RuleAction.annotation( + annotation_type=RuleAnnotationType.DATA_REVIEW, + tags=[ci_pytest_tag], + ), + }, + ) + rule = created_rule + if rule.is_archived: + rule = sift_client.rules.unarchive(rule_ids=[rule.id_]) + yield rule + if created_rule: + sift_client.rules.archive(rule_ids=[created_rule.id_]) + + +def test_client_binding(sift_client): + assert sift_client.reports + assert isinstance(sift_client.reports, ReportsAPI) + assert sift_client.async_.reports + assert isinstance(sift_client.async_.reports, ReportsAPIAsync) + + +@pytest.mark.integration +class TestReports: + def test_create_from_rules(self, nostromo_run, test_rule, sift_client): + report_from_rules = sift_client.reports.create_from_rules( + name="report_from_rules", + run=nostromo_run, + rules=[test_rule], + ) + assert report_from_rules is not None + assert report_from_rules.run_id == nostromo_run.id_ + + def test_create_from_applicable_rules( + self, test_rule, nostromo_asset, nostromo_run, sift_client + ): + if not test_rule.asset_ids: + # Test rule may exist but be in a state where it no longer applies to the asset associated w/ the run so re-attach it if necessary. + test_rule = test_rule.update(update={"asset_ids": [nostromo_asset.id_]}) + report_from_applicable_rules = sift_client.reports.create_from_applicable_rules( + name="report_from_applicable_rules_run", + run=nostromo_run, + organization_id=nostromo_run.organization_id, + ) + assert report_from_applicable_rules is not None + assert report_from_applicable_rules.run_id == nostromo_run.id_ + + def test_list(self, nostromo_asset, nostromo_run, tags, sift_client): + reports = sift_client.reports.list_( + run=nostromo_run, + organization_id=nostromo_asset.organization_id, + ) + assert len(reports) > 0 + + def test_rerun(self, nostromo_asset, nostromo_run, test_rule, sift_client): + report_from_rules = sift_client.reports.create_from_rules( + name="report_from_rules", + run=nostromo_run, + rules=[test_rule], + ) + assert report_from_rules is not None + job_id, rerun_report_id = sift_client.reports.rerun(report=report_from_rules) + rerun_report = sift_client.reports.get(report_id=rerun_report_id) + assert rerun_report is not None + assert rerun_report.run_id == nostromo_run.id_ + assert rerun_report.rerun_from_report_id == report_from_rules.id_ + + def test_update(self, nostromo_asset, nostromo_run, test_rule, sift_client): + report_from_rules = sift_client.reports.create_from_rules( + name="report_from_rules", + run=nostromo_run, + rules=[test_rule], + ) + assert report_from_rules is not None + updated_report = sift_client.reports.update( + report=report_from_rules, + update={ + "metadata": { + "test_type": "ci", + }, + }, + ) + assert updated_report is not None + assert updated_report.metadata == {"test_type": "ci"} + + def test_find_multiple(self, sift_client): + try: + sift_client.reports.find(name="report_from_rules") + except ValueError as e: + assert "Multiple reports found for query" in str(e) + + def test_cancel(self, nostromo_asset, nostromo_run, test_rule, sift_client): + report_from_rules = sift_client.reports.create_from_rules( + name="report_from_rules", + run=nostromo_run, + rules=[test_rule], + ) + assert report_from_rules is not None + job_id, second_rerun_report_id = sift_client.reports.rerun(report=report_from_rules) + assert second_rerun_report_id is not None + sift_client.reports.cancel(report=second_rerun_report_id) + canceled_report = sift_client.reports.find(report_ids=[second_rerun_report_id]) + assert canceled_report is not None + for summary in canceled_report.summaries: + assert summary.status == ReportRuleStatus.CANCELED + + def test_archive(self, nostromo_asset, nostromo_run, test_rule, sift_client): + report_from_rules = sift_client.reports.create_from_rules( + name="report_from_rules", + run=nostromo_run, + rules=[test_rule], + ) + assert report_from_rules is not None + archived_report = sift_client.reports.archive(report=report_from_rules) + assert archived_report is not None + assert archived_report.is_archived == True diff --git a/python/lib/sift_client/_tests/resources/test_rules.py b/python/lib/sift_client/_tests/resources/test_rules.py index 6036bec19..49db64187 100644 --- a/python/lib/sift_client/_tests/resources/test_rules.py +++ b/python/lib/sift_client/_tests/resources/test_rules.py @@ -267,7 +267,7 @@ async def test_create_basic_rule(self, rules_api_async): annotation_type=RuleAnnotationType.DATA_REVIEW, tags=[], ), - asset_ids=[assets[0].id_], + assets=[assets[0]], ) created_rule = await rules_api_async.create(rule_create) @@ -395,15 +395,15 @@ async def test_update_with_version_notes(self, rules_api_async, new_rule): await rules_api_async.archive(new_rule.id_) @pytest.mark.asyncio - async def test_update_rule_action(self, rules_api_async, new_rule): + async def test_update_rule_action(self, rules_api_async, new_rule, ci_pytest_tag): """Test updating a rule's action including annotation type, tags, and assignee.""" try: # Update the action with new annotation type, tags, and assignee update = RuleUpdate( action=RuleAction.annotation( annotation_type=RuleAnnotationType.PHASE, - tags=["sift-client-pytest"], - default_assignee_user_id=new_rule.created_by_user_id, + tags=[ci_pytest_tag], + default_assignee_user=new_rule.created_by_user_id, ), ) updated_rule = await rules_api_async.update(new_rule, update) @@ -412,8 +412,8 @@ async def test_update_rule_action(self, rules_api_async, new_rule): assert updated_rule.id_ == new_rule.id_ assert updated_rule.action.action_type == RuleActionType.ANNOTATION assert updated_rule.action.annotation_type == RuleAnnotationType.PHASE - assert set(updated_rule.action.tags) == {"sift-client-pytest"} - assert updated_rule.action.default_assignee_user_id == new_rule.created_by_user_id + assert set(updated_rule.action.tags_ids) == {ci_pytest_tag.id_} + assert updated_rule.action.default_assignee_user == new_rule.created_by_user_id # Verify other fields remain unchanged assert updated_rule.name == new_rule.name @@ -422,7 +422,7 @@ async def test_update_rule_action(self, rules_api_async, new_rule): await rules_api_async.archive(new_rule.id_) @pytest.mark.asyncio - async def test_update_with_complex_expression(self, rules_api_async, sift_client): + async def test_update_with_complex_expression(self, rules_api_async, sift_client, test_tag): """Test updating a rule with a complex expression (range check).""" # Get channels and assets channels = await sift_client.async_.channels.list_(limit=2) @@ -441,7 +441,7 @@ async def test_update_with_complex_expression(self, rules_api_async, sift_client ], action=RuleAction.annotation( annotation_type=RuleAnnotationType.DATA_REVIEW, - tags=["test"], + tags=[test_tag], ), asset_ids=[assets[0].id_], ) @@ -472,7 +472,9 @@ async def test_update_with_complex_expression(self, rules_api_async, sift_client await rules_api_async.archive(created_rule.id_) @pytest.mark.asyncio - async def test_update_with_multiple_channel_references(self, rules_api_async, sift_client): + async def test_update_with_multiple_channel_references( + self, rules_api_async, sift_client, test_tag + ): """Test updating a rule expression to use multiple channel references.""" # Get channels and assets channels = await sift_client.async_.channels.list_(limit=3) @@ -492,7 +494,7 @@ async def test_update_with_multiple_channel_references(self, rules_api_async, si ], action=RuleAction.annotation( annotation_type=RuleAnnotationType.DATA_REVIEW, - tags=["test"], + tags=[test_tag], ), asset_ids=[assets[0].id_], ) diff --git a/python/lib/sift_client/resources/_base.py b/python/lib/sift_client/resources/_base.py index e9ad07cca..5e8a9817f 100644 --- a/python/lib/sift_client/resources/_base.py +++ b/python/lib/sift_client/resources/_base.py @@ -47,13 +47,17 @@ def _apply_client_to_instances(self, instances: list[T]) -> list[T]: # Common CEL filters used in resources def _build_name_cel_filters( self, + *, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, ) -> list[str]: filter_parts = [] if name: filter_parts.append(cel.equals("name", name)) + if names: + filter_parts.append(cel.in_("name", names)) if name_contains: filter_parts.append(cel.contains("name", name_contains)) if name_regex: @@ -147,7 +151,7 @@ def _build_common_cel_filters( filter_parts = [] if description_contains: filter_parts.append(cel.contains("description", description_contains)) - if include_archived is not None: + if include_archived is not None and not include_archived: filter_parts.append(cel.equals("is_archived", include_archived)) if filter_query: filter_parts.append(filter_query) diff --git a/python/lib/sift_client/resources/assets.py b/python/lib/sift_client/resources/assets.py index 21fa7e8ec..d8482061a 100644 --- a/python/lib/sift_client/resources/assets.py +++ b/python/lib/sift_client/resources/assets.py @@ -66,6 +66,7 @@ async def list_( *, # name name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, # self ids @@ -93,6 +94,7 @@ async def list_( Args: name: Exact name of the asset. + names: List of asset names to filter by. name_contains: Partial name of the asset. name_regex: Regular expression to filter assets by name. asset_ids: Filter to assets with any of these Ids. @@ -115,7 +117,7 @@ async def list_( """ filter_parts = [ *self._build_name_cel_filters( - name=name, name_contains=name_contains, name_regex=name_regex + name=name, names=names, name_contains=name_contains, name_regex=name_regex ), *self._build_time_cel_filters( created_after=created_after, diff --git a/python/lib/sift_client/resources/calculated_channels.py b/python/lib/sift_client/resources/calculated_channels.py index 05c1505d5..789d52be1 100644 --- a/python/lib/sift_client/resources/calculated_channels.py +++ b/python/lib/sift_client/resources/calculated_channels.py @@ -73,6 +73,7 @@ async def list_( self, *, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, # self ids @@ -105,6 +106,7 @@ async def list_( Args: name: Exact name of the calculated channel. + names: List of calculated channel names to filter by. name_contains: Partial name of the calculated channel. name_regex: Regular expression string to filter calculated channels by name. calculated_channel_ids: Filter to calculated channels with any of these IDs. @@ -131,7 +133,7 @@ async def list_( """ filter_parts = [ *self._build_name_cel_filters( - name=name, name_contains=name_contains, name_regex=name_regex + name=name, names=names, name_contains=name_contains, name_regex=name_regex ), *self._build_time_cel_filters( created_after=created_after, @@ -281,6 +283,7 @@ async def list_versions( calculated_channel: CalculatedChannel | str | None = None, client_key: str | None = None, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, # created/modified ranges @@ -308,6 +311,7 @@ async def list_versions( calculated_channel: The CalculatedChannel or ID of the calculated channel to get versions for. client_key: The client key of the calculated channel. name: Exact name of the calculated channel. + names: List of calculated channel names to filter by. name_contains: Partial name of the calculated channel. name_regex: Regular expression string to filter calculated channels by name. created_after: Filter versions created after this datetime. @@ -329,7 +333,7 @@ async def list_versions( """ filter_parts = [ *self._build_name_cel_filters( - name=name, name_contains=name_contains, name_regex=name_regex + name=name, names=names, name_contains=name_contains, name_regex=name_regex ), *self._build_time_cel_filters( created_after=created_after, diff --git a/python/lib/sift_client/resources/channels.py b/python/lib/sift_client/resources/channels.py index 7a5bb1d9b..439774222 100644 --- a/python/lib/sift_client/resources/channels.py +++ b/python/lib/sift_client/resources/channels.py @@ -61,6 +61,7 @@ async def list_( self, *, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, # self ids @@ -84,6 +85,7 @@ async def list_( Args: name: Exact name of the channel. + names: List of channel names to filter by. name_contains: Partial name of the channel. name_regex: Regular expression to filter channels by name. channel_ids: Filter to channels with any of these IDs. @@ -104,7 +106,7 @@ async def list_( """ filter_parts = [ *self._build_name_cel_filters( - name=name, name_contains=name_contains, name_regex=name_regex + name=name, names=names, name_contains=name_contains, name_regex=name_regex ), *self._build_time_cel_filters( created_after=created_after, diff --git a/python/lib/sift_client/resources/reports.py b/python/lib/sift_client/resources/reports.py index f528371b5..23b2dc9d5 100644 --- a/python/lib/sift_client/resources/reports.py +++ b/python/lib/sift_client/resources/reports.py @@ -1,18 +1,21 @@ from __future__ import annotations -from datetime import datetime, timezone from typing import TYPE_CHECKING from sift_client._internal.low_level_wrappers.reports import ReportsLowLevelClient from sift_client._internal.low_level_wrappers.rules import RulesLowLevelClient from sift_client.resources._base import ResourceBase from sift_client.sift_types.report import Report, ReportUpdate +from sift_client.sift_types.rule import Rule +from sift_client.sift_types.run import Run from sift_client.util import cel_utils as cel if TYPE_CHECKING: import re + from datetime import datetime from sift_client.client import SiftClient + from sift_client.sift_types.tag import Tag class ReportsAPIAsync(ResourceBase): @@ -52,11 +55,12 @@ async def list_( name_regex: str | re.Pattern | None = None, names: list[str] | None = None, description_contains: str | None = None, - run_id: str | None = None, + run: Run | str | None = None, organization_id: str | None = None, + report_ids: list[str] | None = None, report_template_id: str | None = None, metadata: dict[str, str | float | bool] | None = None, - tag_name: str | None = None, + tag_names: list[str] | list[Tag] | None = None, created_by: str | None = None, modified_by: str | None = None, order_by: str | None = None, @@ -76,11 +80,12 @@ async def list_( name_regex: Regular expression string to filter reports by name. names: List of report names to filter by. description_contains: Partial description of the report. - run_id: Run ID to filter by. + run: Run/run ID to filter by. organization_id: Organization ID to filter by. + report_ids: List of report IDs to filter by. report_template_id: Report template ID to filter by. metadata: Metadata to filter by. - tag_name: Tag name to filter by. + tag_names: List of tags or tag names to filter by. created_by: The user ID of the creator of the reports. modified_by: The user ID of the last modifier of the reports. order_by: How to order the retrieved reports. @@ -98,7 +103,10 @@ async def list_( # Build CEL filter filter_parts = [ *self._build_name_cel_filters( - name=name, name_contains=name_contains, name_regex=name_regex, names=names + name=name, + names=names, + name_contains=name_contains, + name_regex=name_regex, ), *self._build_time_cel_filters( created_after=created_after, @@ -108,7 +116,7 @@ async def list_( created_by=created_by, modified_by=modified_by, ), - *self._build_tags_metadata_cel_filters(tag_names=[tag_name], metadata=metadata), + *self._build_tags_metadata_cel_filters(tag_names=tag_names, metadata=metadata), *self._build_common_cel_filters( description_contains=description_contains, include_archived=include_archived, @@ -116,9 +124,13 @@ async def list_( ), ] - if run_id: + if run: + run_id = run.id_ if isinstance(run, Run) else run filter_parts.append(cel.equals("run_id", run_id)) + if report_ids: + filter_parts.append(cel.in_("report_id", report_ids)) + if report_template_id: filter_parts.append(cel.equals("report_template_id", report_template_id)) @@ -186,17 +198,17 @@ async def create_from_rules( self, *, name: str, - run_id: str | None = None, + run: Run | str | None = None, organization_id: str | None = None, - rule_ids: list[str] | None = None, + rules: list[Rule] | list[str], ) -> Report | None: """Create a new report from rules. Args: name: The name of the report. - run_id: The run ID to associate with the report. + run: The run or run ID to associate with the report. organization_id: The organization ID. - rule_ids: List of rule IDs to include in the report. + rules: List of rules or rule IDs to include in the report. Returns: The created Report or None if no report was created. @@ -206,9 +218,10 @@ async def create_from_rules( created_report, job_id, ) = await self._rules_low_level_client.evaluate_rules( - run_id=run_id, + run_id=run._id_or_error if isinstance(run, Run) else run, organization_id=organization_id, - rule_ids=rule_ids, + rule_ids=[rule._id_or_error if isinstance(rule, Rule) else rule for rule in rules] + or [], report_name=name, ) if created_report: @@ -218,7 +231,7 @@ async def create_from_rules( async def create_from_applicable_rules( self, *, - run_id: str | None = None, + run: Run | str | None = None, organization_id: str | None = None, name: str | None = None, start_time: datetime | None = None, @@ -228,7 +241,7 @@ async def create_from_applicable_rules( If you want to evaluate against assets, use the rules client instead since no report is created in that case. Args: - run_id: The run ID to associate with the report. + run: The run or run ID to associate with the report. organization_id: The organization ID. name: Optional name for the report. start_time: Optional start time to evaluate rules against. @@ -242,7 +255,7 @@ async def create_from_applicable_rules( created_report, job_id, ) = await self._rules_low_level_client.evaluate_rules( - run_id=run_id, + run_id=run._id_or_error if isinstance(run, Run) else run, organization_id=organization_id, start_time=start_time, end_time=end_time, @@ -308,7 +321,7 @@ async def archive( ) -> Report: """Archive a report.""" report_id = report.id_ if isinstance(report, Report) else report - update = ReportUpdate(archived_date=datetime.now(timezone.utc)) + update = ReportUpdate(is_archived=True) update.resource_id = report_id updated_report = await self._low_level_client.update_report(update=update) return self._apply_client_to_instance(updated_report) diff --git a/python/lib/sift_client/resources/rules.py b/python/lib/sift_client/resources/rules.py index 1983d0727..03662dda4 100644 --- a/python/lib/sift_client/resources/rules.py +++ b/python/lib/sift_client/resources/rules.py @@ -4,6 +4,7 @@ from sift_client._internal.low_level_wrappers.rules import RulesLowLevelClient from sift_client.resources._base import ResourceBase +from sift_client.sift_types.asset import Asset from sift_client.sift_types.rule import Rule, RuleCreate, RuleUpdate from sift_client.util import cel_utils as cel @@ -56,6 +57,7 @@ async def list_( self, *, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, # self ids @@ -72,7 +74,7 @@ async def list_( # metadata metadata: list[Any] | None = None, # rule specific - asset_ids: list[str] | None = None, + assets: list[str] | list[Asset] | None = None, asset_tags: list[str | Tag] | None = None, # common filters description_contains: str | None = None, @@ -85,10 +87,11 @@ async def list_( Args: name: Exact name of the rule. + names: List of rule names to filter by. name_contains: Partial name of the rule. name_regex: Regular expression string to filter rules by name. - rule_ids: IDs of rules to filter to. client_keys: Client keys of rules to filter to. + rule_ids: IDs of rules to filter to. created_after: Rules created after this datetime. created_before: Rules created before this datetime. modified_after: Rules modified after this datetime. @@ -96,7 +99,7 @@ async def list_( created_by: Filter rules created by this User or user ID. modified_by: Filter rules last modified by this User or user ID. metadata: Filter rules by metadata criteria. - asset_ids: Filter rules associated with any of these Asset IDs. + assets: Filter rules associated with any of these Assets. asset_tags: Filter rules associated with any Assets that have these Tag IDs. description_contains: Partial description of the rule. include_archived: If True, include archived rules in results. @@ -109,7 +112,10 @@ async def list_( """ filter_parts = [ *self._build_name_cel_filters( - name=name, name_contains=name_contains, name_regex=name_regex + name=name, + names=names, + name_contains=name_contains, + name_regex=name_regex, ), *self._build_time_cel_filters( created_after=created_after, @@ -130,8 +136,9 @@ async def list_( filter_parts.append(cel.in_("rule_id", rule_ids)) if client_keys: filter_parts.append(cel.in_("client_key", client_keys)) - if asset_ids: - filter_parts.append(cel.in_("asset_id", asset_ids)) + if assets: + ids = [a._id_or_error if isinstance(a, Asset) else a or "" for a in assets] + filter_parts.append(cel.in_("asset_id", ids)) query_filter = cel.and_(*filter_parts) rules = await self._low_level_client.list_all_rules( filter_query=query_filter, diff --git a/python/lib/sift_client/resources/runs.py b/python/lib/sift_client/resources/runs.py index bba84f77e..844139a7b 100644 --- a/python/lib/sift_client/resources/runs.py +++ b/python/lib/sift_client/resources/runs.py @@ -60,6 +60,7 @@ async def list_( self, *, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, # self ids @@ -98,6 +99,7 @@ async def list_( Args: name: Exact name of the run. + names: List of run names to filter by. name_contains: Partial name of the run. name_regex: Regular expression to filter runs by name. run_ids: Filter to runs with any of these IDs. @@ -130,7 +132,7 @@ async def list_( """ filter_parts = [ *self._build_name_cel_filters( - name=name, name_contains=name_contains, name_regex=name_regex + name=name, names=names, name_contains=name_contains, name_regex=name_regex ), *self._build_time_cel_filters( created_after=created_after, @@ -159,7 +161,9 @@ async def list_( asset = cast("list[Asset]", assets) # linting filter_parts.append(cel.in_("asset_id", [a._id_or_error for a in asset])) if asset_tags: - asset_tag_ids = [tag.id_ if isinstance(tag, Tag) else tag for tag in asset_tags] + asset_tag_ids = [ + tag._id_or_error if isinstance(tag, Tag) else tag or "" for tag in asset_tags + ] filter_parts.append(cel.in_("asset_tag_id", asset_tag_ids)) if duration_less_than: filter_parts.append(cel.less_than("duration_string", duration_less_than)) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index cd1090665..cdd36b35a 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -98,6 +98,7 @@ class AssetsAPI: self, *, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, asset_ids: list[str] | None = None, @@ -119,6 +120,7 @@ class AssetsAPI: Args: name: Exact name of the asset. + names: List of asset names to filter by. name_contains: Partial name of the asset. name_regex: Regular expression to filter assets by name. asset_ids: Filter to assets with any of these Ids. @@ -241,6 +243,7 @@ class CalculatedChannelsAPI: self, *, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, calculated_channel_ids: list[str] | None = None, @@ -266,6 +269,7 @@ class CalculatedChannelsAPI: Args: name: Exact name of the calculated channel. + names: List of calculated channel names to filter by. name_contains: Partial name of the calculated channel. name_regex: Regular expression string to filter calculated channels by name. calculated_channel_ids: Filter to calculated channels with any of these IDs. @@ -298,6 +302,7 @@ class CalculatedChannelsAPI: calculated_channel: CalculatedChannel | str | None = None, client_key: str | None = None, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, created_after: datetime | None = None, @@ -320,6 +325,7 @@ class CalculatedChannelsAPI: calculated_channel: The CalculatedChannel or ID of the calculated channel to get versions for. client_key: The client key of the calculated channel. name: Exact name of the calculated channel. + names: List of calculated channel names to filter by. name_contains: Partial name of the calculated channel. name_regex: Regular expression string to filter calculated channels by name. created_after: Filter versions created after this datetime. @@ -454,6 +460,7 @@ class ChannelsAPI: self, *, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, channel_ids: list[str] | None = None, @@ -473,6 +480,7 @@ class ChannelsAPI: Args: name: Exact name of the channel. + names: List of channel names to filter by. name_contains: Partial name of the channel. name_regex: Regular expression to filter channels by name. channel_ids: Filter to channels with any of these IDs. @@ -640,11 +648,11 @@ class ReportsAPI: name_regex: str | re.Pattern | None = None, names: list[str] | None = None, description_contains: str | None = None, - run_id: str | None = None, + run: Run | str | None = None, organization_id: str | None = None, report_template_id: str | None = None, metadata: dict[str, str | float | bool] | None = None, - tag_name: str | None = None, + tag_names: list[str] | list[Tag] | None = None, created_by: str | None = None, modified_by: str | None = None, order_by: str | None = None, @@ -664,11 +672,11 @@ class ReportsAPI: name_regex: Regular expression string to filter reports by name. names: List of report names to filter by. description_contains: Partial description of the report. - run_id: Run ID to filter by. + run: Run/run ID to filter by. organization_id: Organization ID to filter by. report_template_id: Report template ID to filter by. metadata: Metadata to filter by. - tag_name: Tag name to filter by. + tag_names: List of tags or tag names to filter by. created_by: The user ID of the creator of the reports. modified_by: The user ID of the last modifier of the reports. order_by: How to order the retrieved reports. @@ -776,6 +784,7 @@ class RulesAPI: self, *, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, rule_ids: list[str] | None = None, @@ -787,7 +796,7 @@ class RulesAPI: created_by: Any | str | None = None, modified_by: Any | str | None = None, metadata: list[Any] | None = None, - asset_ids: list[str] | None = None, + assets: list[str] | list[Asset] | None = None, asset_tags: list[str | Tag] | None = None, description_contains: str | None = None, include_archived: bool = False, @@ -799,10 +808,11 @@ class RulesAPI: Args: name: Exact name of the rule. + names: List of rule names to filter by. name_contains: Partial name of the rule. name_regex: Regular expression string to filter rules by name. - rule_ids: IDs of rules to filter to. client_keys: Client keys of rules to filter to. + rule_ids: IDs of rules to filter to. created_after: Rules created after this datetime. created_before: Rules created before this datetime. modified_after: Rules modified after this datetime. @@ -810,7 +820,7 @@ class RulesAPI: created_by: Filter rules created by this User or user ID. modified_by: Filter rules last modified by this User or user ID. metadata: Filter rules by metadata criteria. - asset_ids: Filter rules associated with any of these Asset IDs. + assets: Filter rules associated with any of these Assets. asset_tags: Filter rules associated with any Assets that have these Tag IDs. description_contains: Partial description of the rule. include_archived: If True, include archived rules in results. @@ -928,6 +938,7 @@ class RunsAPI: self, *, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, run_ids: list[str] | None = None, @@ -959,6 +970,7 @@ class RunsAPI: Args: name: Exact name of the run. + names: List of run names to filter by. name_contains: Partial name of the run. name_regex: Regular expression to filter runs by name. run_ids: Filter to runs with any of these IDs. @@ -1255,6 +1267,7 @@ class TestResultsAPI: self, *, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, test_report_ids: list[str] | None = None, @@ -1280,6 +1293,7 @@ class TestResultsAPI: Args: name: Exact name of the test report. + names: List of test report names to filter by. name_contains: Partial name of the test report. name_regex: Regular expression string to filter test reports by name. test_report_ids: Test report IDs to filter by. @@ -1313,6 +1327,7 @@ class TestResultsAPI: test_steps: list[str] | list[TestStep] | None = None, test_reports: list[str] | list[TestReport] | None = None, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, measurement_type: TestMeasurementType | None = None, @@ -1328,6 +1343,7 @@ class TestResultsAPI: test_steps: Test steps to filter by. test_reports: Test reports to filter by. name: Exact name of the test measurement. + names: List of test measurement names to filter by. name_contains: Partial name of the test measurement. name_regex: Regular expression string to filter test measurements by name. measurement_type: Measurement type to filter by (TestMeasurementType enum). @@ -1348,6 +1364,7 @@ class TestResultsAPI: test_reports: list[str] | list[TestReport] | None = None, parent_steps: list[str] | list[TestStep] | None = None, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, status: TestStatus | None = None, @@ -1363,6 +1380,7 @@ class TestResultsAPI: test_reports: Test reports to filter by. parent_steps: Parent steps to filter by. name: Exact name of the test step. + names: List of test step names to filter by. name_contains: Partial name of the test step. name_regex: Regular expression string to filter test steps by name. status: Status to filter by (TestStatus enum). diff --git a/python/lib/sift_client/resources/tags.py b/python/lib/sift_client/resources/tags.py index e3a3a7fdf..6187ff7f9 100644 --- a/python/lib/sift_client/resources/tags.py +++ b/python/lib/sift_client/resources/tags.py @@ -55,15 +55,13 @@ async def list_( # Build CEL filter filter_parts = [ *self._build_name_cel_filters( - name=name, name_contains=name_contains, name_regex=name_regex + name=name, names=names, name_contains=name_contains, name_regex=name_regex ), *self._build_common_cel_filters( filter_query=filter_query, ), ] - if names: - filter_parts.append(cel.in_("name", names)) if tag_ids: filter_parts.append(cel.in_("tag_id", tag_ids)) diff --git a/python/lib/sift_client/resources/test_results.py b/python/lib/sift_client/resources/test_results.py index 9b978bd56..5c4f6abda 100644 --- a/python/lib/sift_client/resources/test_results.py +++ b/python/lib/sift_client/resources/test_results.py @@ -98,6 +98,7 @@ async def list_( self, *, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, test_report_ids: list[str] | None = None, @@ -123,6 +124,7 @@ async def list_( Args: name: Exact name of the test report. + names: List of test report names to filter by. name_contains: Partial name of the test report. name_regex: Regular expression string to filter test reports by name. test_report_ids: Test report IDs to filter by. @@ -150,7 +152,7 @@ async def list_( # Build CEL filter filter_parts = [ *self._build_name_cel_filters( - name=name, name_contains=name_contains, name_regex=name_regex + name=name, names=names, name_contains=name_contains, name_regex=name_regex ), *self._build_time_cel_filters( created_after=created_after, @@ -289,6 +291,7 @@ async def list_steps( test_reports: list[str] | list[TestReport] | None = None, parent_steps: list[str] | list[TestStep] | None = None, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, status: TestStatus | None = None, @@ -304,6 +307,7 @@ async def list_steps( test_reports: Test reports to filter by. parent_steps: Parent steps to filter by. name: Exact name of the test step. + names: List of test step names to filter by. name_contains: Partial name of the test step. name_regex: Regular expression string to filter test steps by name. status: Status to filter by (TestStatus enum). @@ -318,7 +322,7 @@ async def list_steps( # Build CEL filter filter_parts = [ *self._build_name_cel_filters( - name=name, name_contains=name_contains, name_regex=name_regex + name=name, names=names, name_contains=name_contains, name_regex=name_regex ), *self._build_common_cel_filters( filter_query=filter_query, @@ -453,6 +457,7 @@ async def list_measurements( test_steps: list[str] | list[TestStep] | None = None, test_reports: list[str] | list[TestReport] | None = None, name: str | None = None, + names: list[str] | None = None, name_contains: str | None = None, name_regex: str | re.Pattern | None = None, measurement_type: TestMeasurementType | None = None, @@ -468,6 +473,7 @@ async def list_measurements( test_steps: Test steps to filter by. test_reports: Test reports to filter by. name: Exact name of the test measurement. + names: List of test measurement names to filter by. name_contains: Partial name of the test measurement. name_regex: Regular expression string to filter test measurements by name. measurement_type: Measurement type to filter by (TestMeasurementType enum). @@ -482,7 +488,7 @@ async def list_measurements( # Build CEL filter filter_parts = [ *self._build_name_cel_filters( - name=name, name_contains=name_contains, name_regex=name_regex + name=name, names=names, name_contains=name_contains, name_regex=name_regex ), *self._build_common_cel_filters( filter_query=filter_query, diff --git a/python/lib/sift_client/sift_types/report.py b/python/lib/sift_client/sift_types/report.py index c28719588..40169deef 100644 --- a/python/lib/sift_client/sift_types/report.py +++ b/python/lib/sift_client/sift_types/report.py @@ -9,7 +9,7 @@ from sift.reports.v1.reports_pb2 import ReportRuleSummary as ReportRuleSummaryProto from sift.reports.v1.reports_pb2 import ReportTag as ReportTagProto -from sift_client.sift_types._base import BaseType, MappingHelper, ModelCreateUpdateBase, ModelUpdate +from sift_client.sift_types._base import BaseType, MappingHelper, ModelUpdate from sift_client.sift_types.tag import Tag from sift_client.util.metadata import metadata_dict_to_proto, metadata_proto_to_dict @@ -70,23 +70,25 @@ def _from_proto( _client=sift_client, ) - def _to_proto(self) -> ReportRuleSummaryProto: + def to_proto(self) -> ReportRuleSummaryProto: """Convert to protobuf message.""" - return ReportRuleSummaryProto( - rule_id=self.rule_id, - rule_client_key=self.rule_client_key, - rule_version_id=self.rule_version_id, + proto = ReportRuleSummaryProto( + rule_id=self.rule_id or "", + rule_client_key=self.rule_client_key or "", + rule_version_id=self.rule_version_id or "", rule_version_number=self.rule_version_number, - report_rule_version_id=self.report_rule_version_id, + report_rule_version_id=self.report_rule_version_id or "", num_open=self.num_open, num_failed=self.num_failed, num_passed=self.num_passed, - status=self.status.value, - created_date=self.created_date, - modified_date=self.modified_date, + status=self.status.value, # type: ignore asset_id=self.asset_id, - deleted_date=self.deleted_date, ) + proto.created_date.FromDatetime(self.created_date) + proto.modified_date.FromDatetime(self.modified_date) + if self.deleted_date: + proto.deleted_date.FromDatetime(self.deleted_date) + return proto class Report(BaseType[ReportProto, "Report"]): @@ -105,9 +107,11 @@ class Report(BaseType[ReportProto, "Report"]): modified_date: datetime summaries: list[ReportRuleSummary] tags: list[str] - rerun_from_report_id: str + rerun_from_report_id: str | None = None metadata: dict[str, str | float | bool] + job_id: str archived_date: datetime | None = None + is_archived: bool @classmethod def _from_proto(cls, proto: ReportProto, sift_client: SiftClient | None = None) -> Report: @@ -127,36 +131,44 @@ def _from_proto(cls, proto: ReportProto, sift_client: SiftClient | None = None) ], tags=[tag.tag_name for tag in proto.tags], rerun_from_report_id=proto.rerun_from_report_id, - metadata=metadata_proto_to_dict(proto.metadata), + metadata=metadata_proto_to_dict(proto.metadata), # type: ignore + job_id=proto.job_id, archived_date=proto.archived_date.ToDatetime(tzinfo=timezone.utc) if proto.HasField("archived_date") else None, + is_archived=proto.is_archived, _client=sift_client, ) - def _to_proto(self) -> ReportProto: + def to_proto(self) -> ReportProto: """Convert to protobuf message.""" proto = ReportProto( report_id=self.id_ or "", - run_id=self.run_id, - organization_id=self.organization_id, + run_id=self.run_id or "", + organization_id=self.organization_id or "", + created_by_user_id=self.created_by_user_id, + modified_by_user_id=self.modified_by_user_id, name=self.name, description=self.description, - report_template_id=self.report_template_id, + report_template_id=self.report_template_id or "", tags=[ReportTagProto(tag_name=tag) for tag in self.tags], - summaries=[summary._to_proto() for summary in self.summaries], + summaries=[summary.to_proto() for summary in self.summaries], + job_id=self.job_id or "", + is_archived=self.is_archived, ) - + proto.created_date.FromDatetime(self.created_date) + proto.modified_date.FromDatetime(self.modified_date) + if self.archived_date: + proto.archived_date.FromDatetime(self.archived_date) return proto -class ReportCreateUpdateBase(ModelCreateUpdateBase): - """Base model for Report create and update.""" +class ReportUpdate(ModelUpdate[ReportProto]): + """Model of the Report fields that can be updated.""" - name: str - description: str | None = None - tags: list[str] | list[Tag] | None = None + is_archived: bool | None = None metadata: dict[str, str | float | bool] | None = None + tags: list[str | Tag] | None = None _to_proto_helpers: ClassVar[dict[str, MappingHelper]] = { "metadata": MappingHelper( @@ -172,12 +184,6 @@ class ReportCreateUpdateBase(ModelCreateUpdateBase): def _get_proto_class(self) -> type[ReportProto]: return ReportProto - -class ReportUpdate(ReportCreateUpdateBase, ModelUpdate[ReportProto]): - """Model of the Report fields that can be updated.""" - - is_archived: bool | None = None - def _add_resource_id_to_proto(self, proto_msg: ReportProto): if self._resource_id is None: raise ValueError("Resource ID must be set before adding to proto") diff --git a/python/lib/sift_client/sift_types/rule.py b/python/lib/sift_client/sift_types/rule.py index 387ce4c44..1ac2fbb80 100644 --- a/python/lib/sift_client/sift_types/rule.py +++ b/python/lib/sift_client/sift_types/rule.py @@ -30,8 +30,9 @@ RuleVersion as RuleVersionProto, ) -from sift_client.sift_types._base import BaseType, ModelCreate, ModelUpdate +from sift_client.sift_types._base import BaseType, ModelCreate, ModelCreateUpdateBase, ModelUpdate from sift_client.sift_types.channel import ChannelReference +from sift_client.sift_types.tag import Tag if TYPE_CHECKING: from sift_client.client import SiftClient @@ -67,7 +68,7 @@ class Rule(BaseType[RuleProto, "Rule"]): @property def assets(self) -> list[Asset]: """Get the assets that this rule applies to.""" - return self.client.assets.list_(asset_ids=self.asset_ids, _tag_ids=self.asset_tag_ids) + return self.client.assets.list_(asset_ids=self.asset_ids, tags=self.asset_tag_ids) @property def organization(self): @@ -85,9 +86,9 @@ def modified_by(self): raise NotImplementedError("Modified by is not supported yet.") @property - def tags(self): + def tags(self) -> list[Tag]: """Get the tags that this rule applies to.""" - raise NotImplementedError("Tags is not supported yet.") + return self.client.tags.list_(tag_ids=self.asset_tag_ids) def update(self, update: RuleUpdate | dict, version_notes: str | None = None) -> Rule: """Update the Rule. @@ -156,7 +157,18 @@ def _from_proto(cls, proto: RuleProto, sift_client: SiftClient | None = None) -> ) -class RuleCreate(ModelCreate[CreateRuleRequest]): +class RuleCreateUpdateBase(ModelCreateUpdateBase): + """Base class for Rule create and update models with shared fields and validation.""" + + organization_id: str | None = None + client_key: str | None = None + asset_ids: list[str] | None = None + asset_tag_ids: list[str] | None = None + contextual_channels: list[str] | None = None + is_external: bool = False + + +class RuleCreate(RuleCreateUpdateBase, ModelCreate[CreateRuleRequest]): """Model for creating a new Rule. Note: @@ -169,23 +181,18 @@ class RuleCreate(ModelCreate[CreateRuleRequest]): expression: str channel_references: list[ChannelReference] action: RuleAction - organization_id: str | None = None - client_key: str | None = None - asset_ids: list[str] | None = None - asset_tag_ids: list[str] | None = None - contextual_channels: list[str] | None = None - is_external: bool = False def _get_proto_class(self) -> type[CreateRuleRequest]: return CreateRuleRequest -class RuleUpdate(ModelUpdate[RuleProto]): +class RuleUpdate(RuleCreateUpdateBase, ModelUpdate[RuleProto]): """Model of the Rule fields that can be updated. Note: - - asset_ids applies this rule to those assets. - - asset_tag_ids applies this rule to assets with those tags. + - assets applies this rule to those assets. + - asset_tags applies this rule to assets with those tags. + - contextual_channels applies this rule to assets with those channels. """ name: str | None = None @@ -193,9 +200,6 @@ class RuleUpdate(ModelUpdate[RuleProto]): expression: str | None = None channel_references: list[ChannelReference] | None = None action: RuleAction | None = None - asset_ids: list[str] | None = None - asset_tag_ids: list[str] | None = None - contextual_channels: list[str] | None = None is_archived: bool | None = None def _get_proto_class(self) -> type[RuleProto]: @@ -269,27 +273,33 @@ class RuleAction(BaseType[RuleActionProto, "RuleAction"]): version_id: str | None = None annotation_type: RuleAnnotationType | None = None tags_ids: list[str] | None = None - default_assignee_user_id: str | None = None + default_assignee_user: str | None = None @classmethod def annotation( cls, annotation_type: RuleAnnotationType, - tags_ids: list[str], - default_assignee_user_id: str | None = None, + tags: list[str | Tag], + default_assignee_user: str | None = None, ) -> RuleAction: """Create an annotation action. Args: annotation_type: Type of annotation to create. - default_assignee_user_id: User ID to assign the annotation to. - tags_ids: List of tag IDs to add to the annotation. + default_assignee_user: User ID to assign the annotation to. + tags: List of tags or tag IDs to add to the annotation. """ + validated_tags = ( + [str(UUID(tag.id_)) if isinstance(tag, Tag) else str(UUID(tag)) for tag in tags] + if tags + else None + ) + return cls( action_type=RuleActionType.ANNOTATION, annotation_type=annotation_type, - tags_ids=[str(UUID(tag_id)) for tag_id in tags_ids], - default_assignee_user_id=default_assignee_user_id, + tags_ids=validated_tags, + default_assignee_user=default_assignee_user, ) @classmethod @@ -310,7 +320,7 @@ def _from_proto( if proto.configuration.annotation.tag_ids else None ), - default_assignee_user_id=( + default_assignee_user=( proto.configuration.annotation.assigned_to_user_id if proto.configuration.annotation.assigned_to_user_id else None @@ -327,13 +337,14 @@ def _from_proto( ) def _to_update_request(self) -> UpdateActionRequest: + tags_ids = [str(UUID(tag)) for tag in self.tags_ids] if self.tags_ids else None return UpdateActionRequest( action_type=self.action_type.value, configuration=RuleActionConfiguration( annotation=( AnnotationActionConfiguration( - assigned_to_user_id=self.default_assignee_user_id, - tag_ids=self.tags_ids, + assigned_to_user_id=self.default_assignee_user, + tag_ids=tags_ids, annotation_type=self.annotation_type.value, # type: ignore ) if self.action_type == RuleActionType.ANNOTATION @@ -342,6 +353,11 @@ def _to_update_request(self) -> UpdateActionRequest: ), ) + @property + def tags(self) -> list[Tag]: + """Get the tags that this rule action applies to.""" + return self.client.tags.list_(tag_ids=self.tags_ids) if self.tags_ids else [] + class RuleVersion(BaseType[RuleVersionProto, "RuleVersion"]): """Model of a Rule Version.""" diff --git a/python/lib/sift_client/sift_types/tag.py b/python/lib/sift_client/sift_types/tag.py index 690e46576..b0bdc4e38 100644 --- a/python/lib/sift_client/sift_types/tag.py +++ b/python/lib/sift_client/sift_types/tag.py @@ -23,7 +23,7 @@ class TagCreateUpdateBase(ModelCreateUpdateBase): name: str -class TagCreate(TagCreateUpdateBase, ModelCreate[TagProto]): +class TagCreate(TagCreateUpdateBase, ModelCreate[CreateTagRequestProto]): """Create model for Tag.""" def _get_proto_class(self) -> type[CreateTagRequestProto]: diff --git a/python/lib/sift_client/util/cel_utils.py b/python/lib/sift_client/util/cel_utils.py index bc8593afe..ea74f2171 100644 --- a/python/lib/sift_client/util/cel_utils.py +++ b/python/lib/sift_client/util/cel_utils.py @@ -202,7 +202,7 @@ def greater_than(field: str, value: int | float | datetime | timedelta) -> str: as_string = f"duration('{value.total_seconds()}s')" else: as_string = str(value) - return f"{field} > timestamp('{as_string}')" + return f"{field} > {as_string}" def less_than(field: str, value: int | float | datetime | timedelta) -> str: @@ -221,4 +221,4 @@ def less_than(field: str, value: int | float | datetime | timedelta) -> str: as_string = f"duration('{value.total_seconds()}s')" else: as_string = str(value) - return f"{field} < timestamp('{as_string}')" + return f"{field} < {as_string}" From f0ecbb72a0ab8e7676b15aa95c0ce830dfaa3bb4 Mon Sep 17 00:00:00 2001 From: Ian Later Date: Tue, 14 Oct 2025 13:18:40 -0700 Subject: [PATCH 07/12] Add uts for tag and report --- .../_internal/low_level_wrappers/rules.py | 2 +- python/lib/sift_client/_tests/conftest.py | 2 + .../_tests/resources/test_reports.py | 15 ++ .../_tests/sift_types/test_report.py | 192 ++++++++++++++++++ .../sift_client/_tests/sift_types/test_tag.py | 71 +++++++ python/lib/sift_client/resources/_base.py | 3 +- python/lib/sift_client/resources/reports.py | 12 ++ .../resources/sync_stubs/__init__.pyi | 18 +- python/lib/sift_client/sift_types/report.py | 12 ++ 9 files changed, 319 insertions(+), 8 deletions(-) create mode 100644 python/lib/sift_client/_tests/sift_types/test_report.py create mode 100644 python/lib/sift_client/_tests/sift_types/test_tag.py diff --git a/python/lib/sift_client/_internal/low_level_wrappers/rules.py b/python/lib/sift_client/_internal/low_level_wrappers/rules.py index 88483806b..dd821741e 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/rules.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/rules.py @@ -504,7 +504,7 @@ async def evaluate_rules( if start_time and end_time: if run_id: kwargs["run_time_range"] = RunTimeRange( - run=run_id, + run=run_id, # type: ignore start_time=to_pb_timestamp(start_time), end_time=to_pb_timestamp(end_time), # type: ignore ) diff --git a/python/lib/sift_client/_tests/conftest.py b/python/lib/sift_client/_tests/conftest.py index 456e39bac..42f47cc82 100644 --- a/python/lib/sift_client/_tests/conftest.py +++ b/python/lib/sift_client/_tests/conftest.py @@ -36,10 +36,12 @@ def mock_client(): client = MagicMock(spec=SiftClient) # Configure the mock to have the necessary API attributes client.assets = MagicMock() + client.reports = MagicMock() client.runs = MagicMock() client.channels = MagicMock() client.calculated_channels = MagicMock() client.rules = MagicMock() + client.tags = MagicMock() client.test_results = MagicMock() client.async_ = MagicMock(spec=AsyncAPIs) client.async_.ingestion = MagicMock() diff --git a/python/lib/sift_client/_tests/resources/test_reports.py b/python/lib/sift_client/_tests/resources/test_reports.py index b723dccf7..b4a26d998 100644 --- a/python/lib/sift_client/_tests/resources/test_reports.py +++ b/python/lib/sift_client/_tests/resources/test_reports.py @@ -156,3 +156,18 @@ def test_archive(self, nostromo_asset, nostromo_run, test_rule, sift_client): archived_report = sift_client.reports.archive(report=report_from_rules) assert archived_report is not None assert archived_report.is_archived == True + + def test_unarchive(self, nostromo_asset, nostromo_run, test_rule, sift_client): + reports_from_rules = sift_client.reports.list_( + name="report_from_rules", include_archived=True + ) + report_from_rules = None + for report_from_rules in reports_from_rules: + if report_from_rules.is_archived: + report_from_rules = report_from_rules + break + assert report_from_rules is not None + assert report_from_rules.is_archived == True + unarchived_report = sift_client.reports.unarchive(report=report_from_rules) + assert unarchived_report is not None + assert unarchived_report.is_archived == False diff --git a/python/lib/sift_client/_tests/sift_types/test_report.py b/python/lib/sift_client/_tests/sift_types/test_report.py new file mode 100644 index 000000000..e35f99e66 --- /dev/null +++ b/python/lib/sift_client/_tests/sift_types/test_report.py @@ -0,0 +1,192 @@ +"""Tests for sift_types.Report model.""" + +from datetime import datetime, timezone +from unittest.mock import MagicMock + +import pytest + +from sift_client.sift_types.report import ( + Report, + ReportRuleStatus, + ReportRuleSummary, + ReportUpdate, +) + + +class TestReportUpdate: + """Unit tests for ReportUpdate model - tests _to_proto_helpers.""" + + def test_metadata_converter(self): + """Test that metadata is converted using _to_proto_helpers.""" + metadata = {"key1": "value1", "key2": 42.5, "key3": True} + update = ReportUpdate(metadata=metadata) + update.resource_id = "test_report_id" + + proto, mask = update.to_proto_with_mask() + + assert proto.report_id == "test_report_id" + # Verify metadata was converted using the helper (returns a list) + assert len(proto.metadata) == 3 + + # Find each metadata value in the list + metadata_dict = {md.key.name: md for md in proto.metadata} + assert metadata_dict["key1"].string_value == "value1" + assert metadata_dict["key2"].number_value == 42.5 + assert metadata_dict["key3"].boolean_value is True + assert "metadata" in mask.paths + + def test_is_archive(self, mock_report, mock_client): + """Test that is_archived field is properly set.""" + archived_report = MagicMock() + archived_report.is_archived = True + mock_client.reports.archive.return_value = archived_report + with MagicMock() as mock_update: + mock_report._update = mock_update + result = mock_report.archive() + mock_client.reports.archive.assert_called_once_with(report=mock_report) + mock_update.assert_called_once_with(archived_report) + assert result is mock_report + + def test_unarchive(self, mock_report, mock_client): + """Test that unarchive() calls client.reports.unarchive and calls _update.""" + unarchived_report = MagicMock() + unarchived_report.is_archived = False + mock_client.reports.unarchive.return_value = unarchived_report + with MagicMock() as mock_update: + mock_report._update = mock_update + result = mock_report.unarchive() + mock_client.reports.unarchive.assert_called_once_with(report=mock_report) + mock_update.assert_called_once_with(unarchived_report) + assert result is mock_report + + def test_metadata_and_is_archived_update(self): + """Test updating multiple fields at once.""" + metadata = {"key": "value"} + update = ReportUpdate( + metadata=metadata, + is_archived=True, + ) + update.resource_id = "test_report_id" + + proto, mask = update.to_proto_with_mask() + + assert proto.report_id == "test_report_id" + assert proto.is_archived is True + assert len(proto.metadata) == 1 + assert "is_archived" in mask.paths + assert "metadata" in mask.paths + + +@pytest.fixture +def mock_report_rule_summary(): + """Create a mock ReportRuleSummary instance for testing.""" + return ReportRuleSummary( + id_="summary_id", + rule_id="rule1", + rule_client_key="rule_key", + rule_version_id="version1", + rule_version_number=1, + report_rule_version_id="report_version1", + num_open=5, + num_failed=2, + num_passed=3, + status=ReportRuleStatus.FINISHED, + created_date=datetime.now(timezone.utc), + modified_date=datetime.now(timezone.utc), + asset_id="asset1", + deleted_date=None, + ) + + +@pytest.fixture +def mock_report(mock_client, mock_report_rule_summary): + """Create a mock Report instance for testing.""" + report = Report( + proto=MagicMock(), + id_="test_report_id", + report_template_id="template1", + run_id="run1", + organization_id="org1", + name="test_report", + description="test description", + created_by_user_id="user1", + modified_by_user_id="user1", + created_date=datetime.now(timezone.utc), + modified_date=datetime.now(timezone.utc), + summaries=[mock_report_rule_summary], + tags=["tag1", "tag2"], + rerun_from_report_id=None, + metadata={"key": "value"}, + job_id="job1", + archived_date=None, + is_archived=False, + ) + report._apply_client_to_instance(mock_client) + return report + + +class TestReport: + """Unit tests for Report model - tests properties and methods.""" + + def test_report_properties(self, mock_report): + """Test that Report properties are accessible.""" + assert mock_report.id_ == "test_report_id" + assert mock_report.name == "test_report" + assert mock_report.description == "test description" + assert mock_report.report_template_id == "template1" + assert mock_report.run_id == "run1" + assert mock_report.organization_id == "org1" + assert mock_report.tags == ["tag1", "tag2"] + assert mock_report.metadata == {"key": "value"} + assert mock_report.is_archived is False + assert len(mock_report.summaries) == 1 + + def test_report_to_proto(self, mock_report): + """Test that Report can be converted to proto.""" + proto = mock_report.to_proto() + + assert proto.report_id == "test_report_id" + assert proto.name == "test_report" + assert proto.description == "test description" + assert proto.report_template_id == "template1" + assert proto.run_id == "run1" + assert proto.organization_id == "org1" + assert proto.is_archived is False + assert len(proto.tags) == 2 + assert proto.tags[0].tag_name == "tag1" + assert proto.tags[1].tag_name == "tag2" + assert len(proto.summaries) == 1 + + +class TestReportRuleSummary: + """Unit tests for ReportRuleSummary model.""" + + def test_report_rule_summary_properties(self, mock_report_rule_summary): + """Test that ReportRuleSummary properties are accessible.""" + assert mock_report_rule_summary.id_ == "summary_id" + assert mock_report_rule_summary.rule_id == "rule1" + assert mock_report_rule_summary.rule_client_key == "rule_key" + assert mock_report_rule_summary.rule_version_id == "version1" + assert mock_report_rule_summary.rule_version_number == 1 + assert mock_report_rule_summary.report_rule_version_id == "report_version1" + assert mock_report_rule_summary.num_open == 5 + assert mock_report_rule_summary.num_failed == 2 + assert mock_report_rule_summary.num_passed == 3 + assert mock_report_rule_summary.status == ReportRuleStatus.FINISHED + assert mock_report_rule_summary.asset_id == "asset1" + assert mock_report_rule_summary.deleted_date is None + + def test_report_rule_summary_to_proto(self, mock_report_rule_summary): + """Test that ReportRuleSummary can be converted to proto.""" + proto = mock_report_rule_summary.to_proto() + + assert proto.rule_id == "rule1" + assert proto.rule_client_key == "rule_key" + assert proto.rule_version_id == "version1" + assert proto.rule_version_number == 1 + assert proto.report_rule_version_id == "report_version1" + assert proto.num_open == 5 + assert proto.num_failed == 2 + assert proto.num_passed == 3 + assert proto.status == ReportRuleStatus.FINISHED.value + assert proto.asset_id == "asset1" diff --git a/python/lib/sift_client/_tests/sift_types/test_tag.py b/python/lib/sift_client/_tests/sift_types/test_tag.py new file mode 100644 index 000000000..0d078d1fa --- /dev/null +++ b/python/lib/sift_client/_tests/sift_types/test_tag.py @@ -0,0 +1,71 @@ +"""Tests for sift_types.Tag model.""" + +from datetime import datetime, timezone +from unittest.mock import MagicMock + +import pytest + +from sift_client.sift_types.tag import Tag, TagCreate + + +@pytest.fixture +def mock_tag(mock_client): + """Create a mock Tag instance for testing.""" + tag = Tag( + proto=MagicMock(), + id_="test_tag_id", + name="test_tag", + created_date=datetime.now(timezone.utc), + created_by_user_id="user1", + ) + tag._apply_client_to_instance(mock_client) + return tag + + +class TestTagCreate: + """Unit tests for TagCreate model.""" + + def test_tag_create_basic(self): + """Test basic TagCreate instantiation.""" + create = TagCreate(name="test_tag") + + assert create.name == "test_tag" + + def test_tag_create_to_proto(self): + """Test that TagCreate converts to proto correctly.""" + create = TagCreate(name="test_tag") + proto = create.to_proto() + + assert proto.name == "test_tag" + + +class TestTag: + """Unit tests for Tag model - tests properties and methods.""" + + def test_tag_properties(self, mock_tag): + """Test that Tag properties are accessible.""" + assert mock_tag.id_ == "test_tag_id" + assert mock_tag.name == "test_tag" + assert mock_tag.created_by_user_id == "user1" + assert mock_tag.created_date is not None + assert mock_tag.created_date.tzinfo == timezone.utc + + def test_tag_to_proto(self, mock_tag): + """Test that Tag can be converted to proto.""" + proto = mock_tag._to_proto() + + assert proto.tag_id == "test_tag_id" + assert proto.name == "test_tag" + assert proto.created_by_user_id == "user1" + + def test_tag_without_client_raises_error(self): + """Test that accessing client without setting it raises an error.""" + tag = Tag( + id_="test_tag_id", + name="test_tag", + created_date=datetime.now(timezone.utc), + created_by_user_id="user1", + ) + + with pytest.raises(AttributeError, match="Sift client not set"): + _ = tag.client diff --git a/python/lib/sift_client/resources/_base.py b/python/lib/sift_client/resources/_base.py index 5e8a9817f..57681dae7 100644 --- a/python/lib/sift_client/resources/_base.py +++ b/python/lib/sift_client/resources/_base.py @@ -152,7 +152,8 @@ def _build_common_cel_filters( if description_contains: filter_parts.append(cel.contains("description", description_contains)) if include_archived is not None and not include_archived: - filter_parts.append(cel.equals("is_archived", include_archived)) + # By default, archived resources are included so only need to set if included_archived is explicitly false + filter_parts.append(cel.equals("is_archived", False)) if filter_query: filter_parts.append(filter_query) return filter_parts diff --git a/python/lib/sift_client/resources/reports.py b/python/lib/sift_client/resources/reports.py index 23b2dc9d5..86a19d5ae 100644 --- a/python/lib/sift_client/resources/reports.py +++ b/python/lib/sift_client/resources/reports.py @@ -325,3 +325,15 @@ async def archive( update.resource_id = report_id updated_report = await self._low_level_client.update_report(update=update) return self._apply_client_to_instance(updated_report) + + async def unarchive( + self, + *, + report: str | Report, + ) -> Report: + """Unarchive a report.""" + report_id = report.id_ if isinstance(report, Report) else report + update = ReportUpdate(is_archived=False) + update.resource_id = report_id + updated_report = await self._low_level_client.update_report(update=update) + return self._apply_client_to_instance(updated_report) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index cdd36b35a..cf0ee1247 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -554,7 +554,7 @@ class ReportsAPI: def create_from_applicable_rules( self, *, - run_id: str | None = None, + run: Run | str | None = None, organization_id: str | None = None, name: str | None = None, start_time: datetime | None = None, @@ -564,7 +564,7 @@ class ReportsAPI: If you want to evaluate against assets, use the rules client instead since no report is created in that case. Args: - run_id: The run ID to associate with the report. + run: The run or run ID to associate with the report. organization_id: The organization ID. name: Optional name for the report. start_time: Optional start time to evaluate rules against. @@ -579,17 +579,17 @@ class ReportsAPI: self, *, name: str, - run_id: str | None = None, + run: Run | str | None = None, organization_id: str | None = None, - rule_ids: list[str] | None = None, + rules: list[Rule] | list[str], ) -> Report | None: """Create a new report from rules. Args: name: The name of the report. - run_id: The run ID to associate with the report. + run: The run or run ID to associate with the report. organization_id: The organization ID. - rule_ids: List of rule IDs to include in the report. + rules: List of rules or rule IDs to include in the report. Returns: The created Report or None if no report was created. @@ -650,6 +650,7 @@ class ReportsAPI: description_contains: str | None = None, run: Run | str | None = None, organization_id: str | None = None, + report_ids: list[str] | None = None, report_template_id: str | None = None, metadata: dict[str, str | float | bool] | None = None, tag_names: list[str] | list[Tag] | None = None, @@ -674,6 +675,7 @@ class ReportsAPI: description_contains: Partial description of the report. run: Run/run ID to filter by. organization_id: Organization ID to filter by. + report_ids: List of report IDs to filter by. report_template_id: Report template ID to filter by. metadata: Metadata to filter by. tag_names: List of tags or tag names to filter by. @@ -704,6 +706,10 @@ class ReportsAPI: """ ... + def unarchive(self, *, report: str | Report) -> Report: + """Unarchive a report.""" + ... + def update(self, report: str | Report, update: ReportUpdate | dict) -> Report: """Update a report. diff --git a/python/lib/sift_client/sift_types/report.py b/python/lib/sift_client/sift_types/report.py index 40169deef..42f349f42 100644 --- a/python/lib/sift_client/sift_types/report.py +++ b/python/lib/sift_client/sift_types/report.py @@ -162,6 +162,18 @@ def to_proto(self) -> ReportProto: proto.archived_date.FromDatetime(self.archived_date) return proto + def archive(self) -> Report: + """Archive the Report.""" + updated_report = self.client.reports.archive(report=self) + self._update(updated_report) + return self + + def unarchive(self) -> Report: + """Unarchive the Report.""" + updated_report = self.client.reports.unarchive(report=self) + self._update(updated_report) + return self + class ReportUpdate(ModelUpdate[ReportProto]): """Model of the Report fields that can be updated.""" From 827f2aed15b82321767f348249b4e162632a227d Mon Sep 17 00:00:00 2001 From: Ian Later Date: Tue, 14 Oct 2025 14:39:37 -0700 Subject: [PATCH 08/12] fixup tests --- .../_internal/low_level_wrappers/rules.py | 2 -- python/lib/sift_client/_tests/conftest.py | 2 +- .../resources/test_calculated_channels.py | 28 ++++++++++++------- .../_tests/resources/test_channels.py | 7 +++-- .../_tests/resources/test_reports.py | 2 +- .../_tests/resources/test_rules.py | 2 +- .../sift_client/_tests/resources/test_tags.py | 3 +- .../_tests/sift_types/test_rule.py | 2 +- python/lib/sift_client/resources/channels.py | 4 ++- 9 files changed, 31 insertions(+), 21 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/rules.py b/python/lib/sift_client/_internal/low_level_wrappers/rules.py index dd821741e..8c732e18c 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/rules.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/rules.py @@ -531,12 +531,10 @@ async def evaluate_rules( kwargs["organization_id"] = organization_id request = EvaluateRulesRequest(**kwargs) - print("request: ", request) response = await self._grpc_client.get_stub(RuleEvaluationServiceStub).EvaluateRules( request ) response = cast("EvaluateRulesResponse", response) - print("response: ", response) created_annotation_count = response.created_annotation_count report_id = response.report_id job_id = response.job_id diff --git a/python/lib/sift_client/_tests/conftest.py b/python/lib/sift_client/_tests/conftest.py index 42f47cc82..238347b69 100644 --- a/python/lib/sift_client/_tests/conftest.py +++ b/python/lib/sift_client/_tests/conftest.py @@ -25,7 +25,7 @@ def sift_client() -> SiftClient: api_key=api_key, grpc_url=grpc_url, rest_url=rest_url, - # use_ssl=True, + use_ssl=True, ) ) diff --git a/python/lib/sift_client/_tests/resources/test_calculated_channels.py b/python/lib/sift_client/_tests/resources/test_calculated_channels.py index 3a39bb4b6..6710f9144 100644 --- a/python/lib/sift_client/_tests/resources/test_calculated_channels.py +++ b/python/lib/sift_client/_tests/resources/test_calculated_channels.py @@ -46,7 +46,7 @@ def calculated_channels_api_sync(sift_client: SiftClient): @pytest.fixture def test_calculated_channel(calculated_channels_api_sync): - calculated_channels = calculated_channels_api_sync.list_(limit=1) + calculated_channels = calculated_channels_api_sync.list_(limit=1, include_archived=True) assert calculated_channels assert len(calculated_channels) >= 1 return calculated_channels[0] @@ -115,7 +115,9 @@ class TestList: @pytest.mark.asyncio async def test_basic_list(self, calculated_channels_api_async): """Test basic calculated channel listing functionality.""" - calc_channels = await calculated_channels_api_async.list_(limit=5) + calc_channels = await calculated_channels_api_async.list_( + limit=5, include_archived=True + ) assert isinstance(calc_channels, list) assert len(calc_channels) == 5 @@ -126,11 +128,13 @@ async def test_basic_list(self, calculated_channels_api_async): @pytest.mark.asyncio async def test_list_with_name_filter(self, calculated_channels_api_async): """Test calculated channel listing with name filtering.""" - all_calc_channels = await calculated_channels_api_async.list_(limit=10) + all_calc_channels = await calculated_channels_api_async.list_( + limit=10, include_archived=True + ) test_calc_channel_name = all_calc_channels[0].name filtered_calc_channels = await calculated_channels_api_async.list_( - name=test_calc_channel_name + name=test_calc_channel_name, include_archived=True ) assert isinstance(filtered_calc_channels, list) @@ -142,7 +146,9 @@ async def test_list_with_name_filter(self, calculated_channels_api_async): @pytest.mark.asyncio async def test_list_with_name_contains_filter(self, calculated_channels_api_async): """Test calculated channel listing with name contains filtering.""" - calc_channels = await calculated_channels_api_async.list_(name_contains="test", limit=5) + calc_channels = await calculated_channels_api_async.list_( + name_contains="test", limit=5, include_archived=True + ) assert isinstance(calc_channels, list) assert calc_channels @@ -154,7 +160,7 @@ async def test_list_with_name_contains_filter(self, calculated_channels_api_asyn async def test_list_with_name_regex_filter(self, calculated_channels_api_async): """Test calculated channel listing with regex name filtering.""" calc_channels = await calculated_channels_api_async.list_( - name_regex=r".*test.*", limit=5 + name_regex=r".*test.*", limit=5, include_archived=True ) assert isinstance(calc_channels, list) @@ -186,7 +192,7 @@ async def test_find_calculated_channel( ): """Test finding a single calculated channel.""" found_calc_channel = await calculated_channels_api_async.find( - name=test_calculated_channel.name + name=test_calculated_channel.name, include_archived=True ) assert found_calc_channel is not None @@ -196,7 +202,7 @@ async def test_find_calculated_channel( async def test_find_nonexistent_calculated_channel(self, calculated_channels_api_async): """Test finding a non-existent calculated channel returns None.""" found_calc_channel = await calculated_channels_api_async.find( - name="nonexistent-calculated-channel-name-12345" + name="nonexistent-calculated-channel-name-12345", include_archived=True ) assert found_calc_channel is None @@ -204,7 +210,9 @@ async def test_find_nonexistent_calculated_channel(self, calculated_channels_api async def test_find_multiple_raises_error(self, calculated_channels_api_async): """Test finding multiple calculated channels raises an error.""" with pytest.raises(ValueError, match="Multiple"): - await calculated_channels_api_async.find(name_contains="test", limit=5) + await calculated_channels_api_async.find( + name_contains="test", limit=5, include_archived=True + ) class TestCreate: """Tests for the async create method.""" @@ -517,7 +525,7 @@ class TestListVersions: async def test_list_versions(self, calculated_channels_api_async, test_calculated_channel): """Test listing versions of a calculated channel.""" versions = await calculated_channels_api_async.list_versions( - calculated_channel=test_calculated_channel + calculated_channel=test_calculated_channel, include_archived=True ) assert isinstance(versions, list) diff --git a/python/lib/sift_client/_tests/resources/test_channels.py b/python/lib/sift_client/_tests/resources/test_channels.py index 8afdb60be..e369652db 100644 --- a/python/lib/sift_client/_tests/resources/test_channels.py +++ b/python/lib/sift_client/_tests/resources/test_channels.py @@ -158,14 +158,17 @@ async def test_list_with_asset_filter(self, channels_api_async): async def test_list_with_description_contains_filter(self, channels_api_async): """Test channel listing with description contains filtering.""" # Test with a common substring that might exist in descriptions - channels = await channels_api_async.list_(description_contains="test", limit=5) + description_contains = "the" + channels = await channels_api_async.list_( + description_contains=description_contains, limit=5 + ) assert isinstance(channels, list) assert channels # If we found channels, verify they contain the substring in description for channel in channels: - assert "test" in channel.description.lower() + assert description_contains in channel.description.lower() @pytest.mark.asyncio async def test_list_with_limit(self, channels_api_async): diff --git a/python/lib/sift_client/_tests/resources/test_reports.py b/python/lib/sift_client/_tests/resources/test_reports.py index b4a26d998..0fe0448f4 100644 --- a/python/lib/sift_client/_tests/resources/test_reports.py +++ b/python/lib/sift_client/_tests/resources/test_reports.py @@ -157,7 +157,7 @@ def test_archive(self, nostromo_asset, nostromo_run, test_rule, sift_client): assert archived_report is not None assert archived_report.is_archived == True - def test_unarchive(self, nostromo_asset, nostromo_run, test_rule, sift_client): + def test_unarchive(self, sift_client): reports_from_rules = sift_client.reports.list_( name="report_from_rules", include_archived=True ) diff --git a/python/lib/sift_client/_tests/resources/test_rules.py b/python/lib/sift_client/_tests/resources/test_rules.py index 49db64187..a8094afb9 100644 --- a/python/lib/sift_client/_tests/resources/test_rules.py +++ b/python/lib/sift_client/_tests/resources/test_rules.py @@ -267,7 +267,7 @@ async def test_create_basic_rule(self, rules_api_async): annotation_type=RuleAnnotationType.DATA_REVIEW, tags=[], ), - assets=[assets[0]], + asset_ids=[assets[0]._id_or_error], ) created_rule = await rules_api_async.create(rule_create) diff --git a/python/lib/sift_client/_tests/resources/test_tags.py b/python/lib/sift_client/_tests/resources/test_tags.py index 1129d7d24..e81929f36 100644 --- a/python/lib/sift_client/_tests/resources/test_tags.py +++ b/python/lib/sift_client/_tests/resources/test_tags.py @@ -105,8 +105,7 @@ def test_find_multiple_raises_error(self, sift_client, test_timestamp_str, test_ # Create multiple tags with similar names name_filter_regex = re.compile(rf".*_tag(1|2)_{re.escape(test_timestamp_str)}") with pytest.raises(ValueError, match="Multiple tags found"): - res = sift_client.tags.find(name_regex=name_filter_regex) - print(res) + _ = sift_client.tags.find(name_regex=name_filter_regex) def test_find_or_create_existing_tags(self, sift_client, test_timestamp_str, test_tags): """Test find_or_create with existing tags.""" diff --git a/python/lib/sift_client/_tests/sift_types/test_rule.py b/python/lib/sift_client/_tests/sift_types/test_rule.py index de9dbe7b1..b1ee9342e 100644 --- a/python/lib/sift_client/_tests/sift_types/test_rule.py +++ b/python/lib/sift_client/_tests/sift_types/test_rule.py @@ -63,7 +63,7 @@ def test_assets_property_calls_client(self, mock_rule, mock_client): # Verify client method was called with correct parameters mock_client.assets.list_.assert_called_once_with( - asset_ids=["asset1", "asset2"], _tag_ids=["tag1"] + asset_ids=["asset1", "asset2"], tags=["tag1"] ) def test_update_calls_client_and_updates_self(self, mock_rule, mock_client): diff --git a/python/lib/sift_client/resources/channels.py b/python/lib/sift_client/resources/channels.py index 439774222..c65ade03c 100644 --- a/python/lib/sift_client/resources/channels.py +++ b/python/lib/sift_client/resources/channels.py @@ -115,7 +115,9 @@ async def list_( modified_before=modified_before, ), *self._build_common_cel_filters( - description_contains=description_contains, filter_query=filter_query + description_contains=description_contains, + filter_query=filter_query, + include_archived=include_archived, ), ] if channel_ids: From 383d91469911bbd06316c66106a0504b2e011265 Mon Sep 17 00:00:00 2001 From: Ian Later Date: Tue, 14 Oct 2025 14:45:23 -0700 Subject: [PATCH 09/12] rule action tag_ids --- python/lib/sift_client/_tests/sift_types/test_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/_tests/sift_types/test_rule.py b/python/lib/sift_client/_tests/sift_types/test_rule.py index b1ee9342e..31dc2fa74 100644 --- a/python/lib/sift_client/_tests/sift_types/test_rule.py +++ b/python/lib/sift_client/_tests/sift_types/test_rule.py @@ -38,7 +38,7 @@ def mock_rule(mock_client): action=RuleAction( action_type=RuleActionType.ANNOTATION, annotation_type=RuleAnnotationType.DATA_REVIEW, - tags=["tag1"], + tags_ids=["tag1"], ), asset_ids=["asset1", "asset2"], asset_tag_ids=["tag1"], From 70e765656af19591a3f285331ac31a03b4e871c6 Mon Sep 17 00:00:00 2001 From: Ian Later Date: Tue, 14 Oct 2025 14:54:04 -0700 Subject: [PATCH 10/12] teardown edge case --- python/lib/sift_client/_tests/resources/test_reports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/lib/sift_client/_tests/resources/test_reports.py b/python/lib/sift_client/_tests/resources/test_reports.py index 0fe0448f4..d06bf1504 100644 --- a/python/lib/sift_client/_tests/resources/test_reports.py +++ b/python/lib/sift_client/_tests/resources/test_reports.py @@ -49,10 +49,10 @@ def test_rule(sift_client, nostromo_asset, ci_pytest_tag): ) rule = created_rule if rule.is_archived: - rule = sift_client.rules.unarchive(rule_ids=[rule.id_]) + rule = rule.unarchive() yield rule if created_rule: - sift_client.rules.archive(rule_ids=[created_rule.id_]) + created_rule.archive() def test_client_binding(sift_client): From 08a4c79e4561d893aeb406b4ad174b9c695be8df Mon Sep 17 00:00:00 2001 From: Ian Later Date: Tue, 14 Oct 2025 15:45:39 -0700 Subject: [PATCH 11/12] nit cleanups --- ...s--2025--09--26--00001075_00000005.parquet | Bin 56755 -> 0 bytes ...n--2025--09--16--00000002_00000001.parquet | Bin 1988 -> 0 bytes .../_internal/low_level_wrappers/__init__.py | 2 ++ python/lib/sift_client/_tests/conftest.py | 10 +++++++++ .../_tests/resources/test_reports.py | 20 +++++------------- .../sift_client/_tests/resources/test_runs.py | 4 ++-- python/lib/sift_client/client.py | 4 ++-- python/lib/sift_client/resources/_base.py | 2 +- 8 files changed, 22 insertions(+), 20 deletions(-) delete mode 100644 python/examples/data_import/parquet/flat_dataset/B101--CAN2_PCM_thermals--2025--09--26--00001075_00000005.parquet delete mode 100644 python/examples/data_import/parquet/flat_dataset/B229--CAN2_HU_firmwareVersion--2025--09--16--00000002_00000001.parquet diff --git a/python/examples/data_import/parquet/flat_dataset/B101--CAN2_PCM_thermals--2025--09--26--00001075_00000005.parquet b/python/examples/data_import/parquet/flat_dataset/B101--CAN2_PCM_thermals--2025--09--26--00001075_00000005.parquet deleted file mode 100644 index fc13f0bf0f19315498a49c52453e7a806029d80b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56755 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 BGz0(u diff --git a/python/lib/sift_client/_internal/low_level_wrappers/__init__.py b/python/lib/sift_client/_internal/low_level_wrappers/__init__.py index 8078840d9..d4122d3aa 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/__init__.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/__init__.py @@ -8,6 +8,7 @@ from sift_client._internal.low_level_wrappers.reports import ReportsLowLevelClient from sift_client._internal.low_level_wrappers.rules import RulesLowLevelClient from sift_client._internal.low_level_wrappers.runs import RunsLowLevelClient +from sift_client._internal.low_level_wrappers.tags import TagsLowLevelClient from sift_client._internal.low_level_wrappers.test_results import TestResultsLowLevelClient from sift_client._internal.low_level_wrappers.upload import UploadLowLevelClient @@ -20,6 +21,7 @@ "ReportsLowLevelClient", "RulesLowLevelClient", "RunsLowLevelClient", + "TagsLowLevelClient", "TestResultsLowLevelClient", "UploadLowLevelClient", ] diff --git a/python/lib/sift_client/_tests/conftest.py b/python/lib/sift_client/_tests/conftest.py index 238347b69..75a073359 100644 --- a/python/lib/sift_client/_tests/conftest.py +++ b/python/lib/sift_client/_tests/conftest.py @@ -48,6 +48,16 @@ def mock_client(): return client +@pytest.fixture(scope="session") +def nostromo_asset(sift_client): + return sift_client.assets.find(name="NostromoLV426") + + +@pytest.fixture(scope="session") +def nostromo_run(nostromo_asset): + return nostromo_asset.runs[0] + + @pytest.fixture(scope="session") def test_tag(sift_client): tag = sift_client.tags.find_or_create(names=["test"])[0] diff --git a/python/lib/sift_client/_tests/resources/test_reports.py b/python/lib/sift_client/_tests/resources/test_reports.py index d06bf1504..29318d8e8 100644 --- a/python/lib/sift_client/_tests/resources/test_reports.py +++ b/python/lib/sift_client/_tests/resources/test_reports.py @@ -10,18 +10,8 @@ @pytest.fixture(scope="session") -def nostromo_asset(sift_client): - return sift_client.assets.find(name="NostromoLV426") - - -@pytest.fixture(scope="session") -def nostromo_run(nostromo_asset): - return nostromo_asset.runs[0] - - -@pytest.fixture(scope="session") -def tags(sift_client): - tags = sift_client.tags.find_or_create(names=["test", "api-created"]) +def tags(sift_client, test_tag, ci_pytest_tag): + tags = sift_client.tags.find_or_create(names=[test_tag.name, ci_pytest_tag.name]) return tags @@ -35,7 +25,7 @@ def test_rule(sift_client, nostromo_asset, ci_pytest_tag): "name": "test_rule", "description": "Test rule", "expression": "$1 > 0.1", - "assets": [nostromo_asset], + "asset_ids": [nostromo_asset._id_or_error], "channel_references": [ ChannelReference( channel_reference="$1", channel_identifier="mainmotor.velocity" @@ -78,7 +68,7 @@ def test_create_from_applicable_rules( ): if not test_rule.asset_ids: # Test rule may exist but be in a state where it no longer applies to the asset associated w/ the run so re-attach it if necessary. - test_rule = test_rule.update(update={"asset_ids": [nostromo_asset.id_]}) + test_rule = test_rule.update(update={"asset_ids": [nostromo_asset._id_or_error]}) report_from_applicable_rules = sift_client.reports.create_from_applicable_rules( name="report_from_applicable_rules_run", run=nostromo_run, @@ -146,7 +136,7 @@ def test_cancel(self, nostromo_asset, nostromo_run, test_rule, sift_client): for summary in canceled_report.summaries: assert summary.status == ReportRuleStatus.CANCELED - def test_archive(self, nostromo_asset, nostromo_run, test_rule, sift_client): + def test_archive(self, nostromo_run, test_rule, sift_client): report_from_rules = sift_client.reports.create_from_rules( name="report_from_rules", run=nostromo_run, diff --git a/python/lib/sift_client/_tests/resources/test_runs.py b/python/lib/sift_client/_tests/resources/test_runs.py index f687ecaed..d60251df5 100644 --- a/python/lib/sift_client/_tests/resources/test_runs.py +++ b/python/lib/sift_client/_tests/resources/test_runs.py @@ -47,7 +47,7 @@ def test_run(runs_api_sync): @pytest.fixture(scope="function") -def new_run(runs_api_sync): +def new_run(runs_api_sync, ci_pytest_tag): """Create a test run for update tests.""" run_name = f"test_run_update_{datetime.now(timezone.utc).isoformat()}" description = "Test run created by Sift Client pytest" @@ -55,7 +55,7 @@ def new_run(runs_api_sync): RunCreate( name=run_name, description=description, - tags=["sift-client-pytest"], + tags=[ci_pytest_tag.name], ) ) return created_run diff --git a/python/lib/sift_client/client.py b/python/lib/sift_client/client.py index 0e013fa76..2a2252ef8 100644 --- a/python/lib/sift_client/client.py +++ b/python/lib/sift_client/client.py @@ -142,8 +142,8 @@ def __init__( self.calculated_channels = CalculatedChannelsAPI(self) self.channels = ChannelsAPI(self) self.rules = RulesAPI(self) - self.runs = RunsAPI(self) self.reports = ReportsAPI(self) + self.runs = RunsAPI(self) self.tags = TagsAPI(self) self.test_results = TestResultsAPI(self) # Accessor for the asynchronous APIs @@ -153,9 +153,9 @@ def __init__( calculated_channels=CalculatedChannelsAPIAsync(self), channels=ChannelsAPIAsync(self), ingestion=IngestionAPIAsync(self), + reports=ReportsAPIAsync(self), rules=RulesAPIAsync(self), runs=RunsAPIAsync(self), - reports=ReportsAPIAsync(self), tags=TagsAPIAsync(self), test_results=TestResultsAPIAsync(self), ) diff --git a/python/lib/sift_client/resources/_base.py b/python/lib/sift_client/resources/_base.py index 57681dae7..228313d5a 100644 --- a/python/lib/sift_client/resources/_base.py +++ b/python/lib/sift_client/resources/_base.py @@ -135,7 +135,7 @@ def _build_tags_metadata_cel_filters( tag_names = [tag.name if isinstance(tag, Tag) else tag for tag in tag_names] filter_parts.append(cel.in_("tag_name", tag_names)) if tag_ids: - tag_ids = [tag.id_ if isinstance(tag, Tag) else tag for tag in tag_ids] + tag_ids = [tag._id_or_error if isinstance(tag, Tag) else tag for tag in tag_ids] filter_parts.append(cel.in_("tag_id", tag_ids)) if metadata: filter_parts.extend(self._build_metadata_cel_filters(metadata)) From a4ba03af57a7a67ab28520ea1f0a87c24a47e5a8 Mon Sep 17 00:00:00 2001 From: Ian Later Date: Wed, 15 Oct 2025 11:38:42 -0700 Subject: [PATCH 12/12] cleanup --- .../lib/sift_client/_internal/low_level_wrappers/rules.py | 2 +- python/lib/sift_client/resources/__init__.py | 2 -- python/lib/sift_client/resources/_base.py | 2 +- python/lib/sift_client/resources/sync_stubs/__init__.py | 3 +-- python/lib/sift_client/sift_types/rule.py | 2 +- python/lib/sift_client/sift_types/tag.py | 7 ++++++- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/rules.py b/python/lib/sift_client/_internal/low_level_wrappers/rules.py index 8c732e18c..5b093b6b7 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/rules.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/rules.py @@ -47,7 +47,7 @@ ) from sift_client.sift_types.tag import Tag from sift_client.transport import GrpcClient, WithGrpcClient -from sift_client.util.util import count_non_none +from sift_client._internal.util.util import count_non_none if TYPE_CHECKING: from datetime import datetime diff --git a/python/lib/sift_client/resources/__init__.py b/python/lib/sift_client/resources/__init__.py index 0a2e46a3d..968fabdb3 100644 --- a/python/lib/sift_client/resources/__init__.py +++ b/python/lib/sift_client/resources/__init__.py @@ -159,8 +159,6 @@ async def main(): from sift_client.resources.rules import RulesAPIAsync from sift_client.resources.runs import RunsAPIAsync from sift_client.resources.tags import TagsAPIAsync - -# ruff: noqa TagsAPIAsync needs to be imported before sync_stubs to avoid circular import from sift_client.resources.test_results import TestResultsAPIAsync # ruff: noqa All imports needs to be imported before sync_stubs to avoid circular import diff --git a/python/lib/sift_client/resources/_base.py b/python/lib/sift_client/resources/_base.py index 228313d5a..676dedbca 100644 --- a/python/lib/sift_client/resources/_base.py +++ b/python/lib/sift_client/resources/_base.py @@ -115,7 +115,7 @@ def _build_metadata_cel_filters( def _build_tags_metadata_cel_filters( self, *, - tag_names: list[Any] | list[str] | None = None, + tag_names: list[Tag] | list[str] | None = None, tag_ids: list[Any] | list[str] | None = None, metadata: list[Any] | dict[str, Any] | None = None, ) -> list[str]: diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.py b/python/lib/sift_client/resources/sync_stubs/__init__.py index 9c6b682e4..6664a5e0f 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.py +++ b/python/lib/sift_client/resources/sync_stubs/__init__.py @@ -24,7 +24,6 @@ ReportsAPI = generate_sync_api(ReportsAPIAsync, "ReportsAPI") TagsAPI = generate_sync_api(TagsAPIAsync, "TagsAPI") -__all__ = ["AssetsAPI", "CalculatedChannelsAPI", "PingAPI", "ReportsAPI", "RunsAPI", "TagsAPI"] TestResultsAPI = generate_sync_api(TestResultsAPIAsync, "TestResultsAPI") -__all__ = ["AssetsAPI", "CalculatedChannelsAPI", "PingAPI", "RunsAPI", "TestResultsAPI"] +__all__ = ["AssetsAPI", "CalculatedChannelsAPI", "PingAPI", "ReportsAPI", "RunsAPI", "TagsAPI"] diff --git a/python/lib/sift_client/sift_types/rule.py b/python/lib/sift_client/sift_types/rule.py index 1ac2fbb80..5d2d786ab 100644 --- a/python/lib/sift_client/sift_types/rule.py +++ b/python/lib/sift_client/sift_types/rule.py @@ -192,7 +192,7 @@ class RuleUpdate(RuleCreateUpdateBase, ModelUpdate[RuleProto]): Note: - assets applies this rule to those assets. - asset_tags applies this rule to assets with those tags. - - contextual_channels applies this rule to assets with those channels. + - contextual_channels are shown by UI to give context when viewing an annotation, but are not part of rule evaluation. """ name: str | None = None diff --git a/python/lib/sift_client/sift_types/tag.py b/python/lib/sift_client/sift_types/tag.py index b0bdc4e38..75afd6e8c 100644 --- a/python/lib/sift_client/sift_types/tag.py +++ b/python/lib/sift_client/sift_types/tag.py @@ -6,6 +6,7 @@ from sift.tags.v2.tags_pb2 import CreateTagRequest as CreateTagRequestProto from sift.tags.v2.tags_pb2 import Tag as TagProto +from sift_client._internal.util.timestamp import to_pb_timestamp from sift_client.sift_types._base import ( BaseType, ModelCreate, @@ -53,6 +54,7 @@ class Tag(BaseType[TagProto, "Tag"]): def _from_proto(cls, proto: TagProto, sift_client: SiftClient | None = None) -> Tag: return cls( id_=proto.tag_id, + proto=proto, name=proto.name, created_date=proto.created_date.ToDatetime(tzinfo=timezone.utc), created_by_user_id=proto.created_by_user_id, @@ -65,6 +67,9 @@ def _to_proto(self) -> TagProto: tag_id=self.id_ or "", name=self.name, created_by_user_id=self.created_by_user_id, - created_date=self.created_date, # type: ignore + created_date=to_pb_timestamp(self.created_date), ) return proto + + def __str__(self) -> str: + return self.name \ No newline at end of file