aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/celpy/__init__.py
blob: 0306530f977b501969316711db588dc1576b4133 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# SPDX-Copyright: Copyright (c) Capital One Services, LLC
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 Capital One Services, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

"""
Pure Python implementation of CEL.

..  todo:: Consolidate __init__ and parser into one module?

Visible interface to CEL. This exposes the :py:class:`Environment`,
the :py:class:`Evaluator` run-time, and the :py:mod:`celtypes` module
with Python types wrapped to be CEL compatible.

Example
=======

Here's an example with some details::

    >>> import celpy

    # A list of type names and class bindings used to create an environment.
    >>> types = []
    >>> env = celpy.Environment(types)

    # Parse the code to create the CEL AST.
    >>> ast = env.compile("355. / 113.")

    # Use the AST and any overriding functions to create an executable program.
    >>> functions = {}
    >>> prgm = env.program(ast, functions)

    # Variable bindings.
    >>> activation = {}

    # Final evaluation.
    >>> try:
    ...    result = prgm.evaluate(activation)
    ...    error = None
    ... except CELEvalError as ex:
    ...    result = None
    ...    error = ex.args[0]

    >>> result  # doctest: +ELLIPSIS
    DoubleType(3.14159...)

Another Example
===============

See https://github.com/google/cel-go/blob/master/examples/simple_test.go

The model Go we're sticking close to::

    d := cel.Declarations(decls.NewVar("name", decls.String))
    env, err := cel.NewEnv(d)
    if err != nil {
        log.Fatalf("environment creation error: %v\\n", err)
    }
    ast, iss := env.Compile(`"Hello world! I'm " + name + "."`)
    // Check iss for compilation errors.
    if iss.Err() != nil {
        log.Fatalln(iss.Err())
    }
    prg, err := env.Program(ast)
    if err != nil {
        log.Fatalln(err)
    }
    out, _, err := prg.Eval(map[string]interface{}{
        "name": "CEL",
    })
    if err != nil {
        log.Fatalln(err)
    }
    fmt.Println(out)
    // Output:Hello world! I'm CEL.

Here's the Pythonic approach, using concept patterned after the Go implementation::

    >>> from celpy import *
    >>> decls = {"name": celtypes.StringType}
    >>> env = Environment(annotations=decls)
    >>> ast = env.compile('"Hello world! I\\'m " + name + "."')
    >>> out = env.program(ast).evaluate({"name": "CEL"})
    >>> print(out)
    Hello world! I'm CEL.

"""
import json  # noqa: F401
import logging
import sys
from typing import Any, Dict, Optional, Type, cast

import lark

import celpy.celtypes
from celpy.adapter import (CELJSONDecoder, CELJSONEncoder,  # noqa: F401
                           json_to_cel)
from celpy.celparser import CELParseError, CELParser  # noqa: F401
from celpy.evaluation import (Activation, Annotation,  # noqa: F401
                              CELEvalError, CELFunction, Context, Evaluator,
                              Result, base_functions)

# A parsed AST.
Expression = lark.Tree


class Runner:
    """Abstract runner.

    Given an AST, this can evaluate the AST in the context of a specific activation
    with any override function definitions.

    ..  todo:: add type adapter and type provider registries.
    """
    def __init__(
            self,
            environment: 'Environment',
            ast: lark.Tree,
            functions: Optional[Dict[str, CELFunction]] = None
    ) -> None:
        self.logger = logging.getLogger(self.__class__.__name__)
        self.environment = environment
        self.ast = ast
        self.functions = functions

    def new_activation(self, context: Context) -> Activation:
        """
        Builds the working activation from the environmental defaults.
        """
        return self.environment.activation().nested_activation(vars=context)

    def evaluate(self, activation: Context) -> celpy.celtypes.Value:  # pragma: no cover
        raise NotImplementedError


class InterpretedRunner(Runner):
    """
    Pure AST expression evaluator. Uses :py:class:`evaluation.Evaluator` class.

    Given an AST, this evauates the AST in the context of a specific activation.

    The returned value will be a celtypes type.

    Generally, this should raise an :exc:`CELEvalError` for most kinds of ordinary problems.
    It may raise an :exc:`CELUnsupportedError` for future features.

    ..  todo:: Refractor the Evaluator constructor from evaluation.
    """
    def evaluate(self, context: Context) -> celpy.celtypes.Value:
        e = Evaluator(
            ast=self.ast,
            activation=self.new_activation(context),
            functions=self.functions
        )
        value = e.evaluate()
        return value


class CompiledRunner(Runner):
    """
    Python compiled expression evaluator. Uses Python byte code and :py:func:`eval`.

    Given an AST, this evauates the AST in the context of a specific activation.

    Transform the AST into Python, uses :py:func:`compile` to create a code object.
    Uses :py:func:`eval` to evaluate.
    """
    def __init__(
            self,
            environment: 'Environment',
            ast: lark.Tree,
            functions: Optional[Dict[str, CELFunction]] = None
    ) -> None:
        super().__init__(environment, ast, functions)
        # Transform AST to Python.
        # compile()
        # cache executable code object.

    def evaluate(self, activation: Context) -> celpy.celtypes.Value:
        # eval() code object with activation as locals, and built-ins as gobals.
        return super().evaluate(activation)


# TODO: Refactor classes into a separate "cel_protobuf" module.
# TODO: Becomes cel_protobuf.Int32Value
class Int32Value(celpy.celtypes.IntType):
    def __new__(
            cls: Type['Int32Value'],
            value: Any = 0,
    ) -> 'Int32Value':
        """TODO: Check range. This seems to matter for protobuf."""
        if isinstance(value, celpy.celtypes.IntType):
            return cast(Int32Value, super().__new__(cls, value))
        # TODO: elif other type conversions...
        else:
            convert = celpy.celtypes.int64(int)
        return cast(Int32Value, super().__new__(cls, convert(value)))


# The "well-known" types in a google.protobuf package.
# We map these to CEl types instead of defining additional Protobuf Types.
# This approach bypasses some of the range constraints that are part of these types.
# It may also cause values to compare as equal when they were originally distinct types.
googleapis = {
    'google.protobuf.Int32Value': celpy.celtypes.IntType,
    'google.protobuf.UInt32Value': celpy.celtypes.UintType,
    'google.protobuf.Int64Value': celpy.celtypes.IntType,
    'google.protobuf.UInt64Value': celpy.celtypes.UintType,
    'google.protobuf.FloatValue': celpy.celtypes.DoubleType,
    'google.protobuf.DoubleValue': celpy.celtypes.DoubleType,
    'google.protobuf.BoolValue': celpy.celtypes.BoolType,
    'google.protobuf.BytesValue': celpy.celtypes.BytesType,
    'google.protobuf.StringValue': celpy.celtypes.StringType,
    'google.protobuf.ListValue': celpy.celtypes.ListType,
    'google.protobuf.Struct': celpy.celtypes.MessageType,
}


class Environment:
    """Compiles CEL text to create an Expression object.

    From the Go implementation, there are things to work with the type annotations:

    -   type adapters registry make other native types available for CEL.

    -   type providers registry make ProtoBuf types available for CEL.

    ..  todo:: Add adapter and provider registries to the Environment.
    """
    def __init__(
            self,
            package: Optional[str] = None,
            annotations: Optional[Dict[str, Annotation]] = None,
            runner_class: Optional[Type[Runner]] = None
    ) -> None:
        """
        Create a new environment.

        This also increases the default recursion limit to handle the defined minimums for CEL.

        :param package: An optional package name used to resolve names in an Activation
        :param annotations: Names with type annotations.
            There are two flavors of names provided here.

            - Variable names based on :py:mod:``celtypes``

            - Function names, using ``typing.Callable``.
        :param runner_class: the class of Runner to use, either InterpretedRunner or CompiledRunner
        """
        sys.setrecursionlimit(2500)
        self.logger = logging.getLogger(self.__class__.__name__)
        self.package: Optional[str] = package
        self.annotations: Dict[str, Annotation] = annotations or {}
        self.logger.info("Type Annotations %r", self.annotations)
        self.runner_class: Type[Runner] = runner_class or InterpretedRunner
        self.cel_parser = CELParser()
        self.runnable: Runner

        # Fold in standard annotations. These (generally) define well-known protobuf types.
        self.annotations.update(googleapis)
        # We'd like to add 'type.googleapis.com/google' directly, but it seems to be an alias
        # for 'google', the path after the '/' in the uri.

    def compile(self, text: str) -> Expression:
        """Compile the CEL source. This can raise syntax error exceptions."""
        ast = self.cel_parser.parse(text)
        return ast

    def program(
            self,
            expr: lark.Tree,
            functions: Optional[Dict[str, CELFunction]] = None
    ) -> Runner:
        """Transforms the AST into an executable runner."""
        self.logger.info("Package %r", self.package)
        runner_class = self.runner_class
        self.runnable = runner_class(self, expr, functions)
        return self.runnable

    def activation(self) -> Activation:
        """Returns a base activation"""
        activation = Activation(package=self.package, annotations=self.annotations)
        return activation