diff options
Diffstat (limited to 'third_party/python/json-e')
-rw-r--r-- | third_party/python/json-e/MANIFEST.in | 3 | ||||
-rw-r--r-- | third_party/python/json-e/PKG-INFO | 11 | ||||
-rw-r--r-- | third_party/python/json-e/README.md | 730 | ||||
-rw-r--r-- | third_party/python/json-e/jsone/__init__.py | 21 | ||||
-rw-r--r-- | third_party/python/json-e/jsone/builtins.py | 121 | ||||
-rw-r--r-- | third_party/python/json-e/jsone/interpreter.py | 289 | ||||
-rw-r--r-- | third_party/python/json-e/jsone/prattparser.py | 191 | ||||
-rw-r--r-- | third_party/python/json-e/jsone/render.py | 354 | ||||
-rw-r--r-- | third_party/python/json-e/jsone/shared.py | 131 | ||||
-rw-r--r-- | third_party/python/json-e/jsone/six.py | 23 | ||||
-rw-r--r-- | third_party/python/json-e/package.json | 35 | ||||
-rw-r--r-- | third_party/python/json-e/setup.cfg | 8 | ||||
-rw-r--r-- | third_party/python/json-e/setup.py | 31 |
13 files changed, 1948 insertions, 0 deletions
diff --git a/third_party/python/json-e/MANIFEST.in b/third_party/python/json-e/MANIFEST.in new file mode 100644 index 0000000000..a6995977cb --- /dev/null +++ b/third_party/python/json-e/MANIFEST.in @@ -0,0 +1,3 @@ +include jsone *.py +include package.json +recursive-exclude test * diff --git a/third_party/python/json-e/PKG-INFO b/third_party/python/json-e/PKG-INFO new file mode 100644 index 0000000000..bf41f82167 --- /dev/null +++ b/third_party/python/json-e/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 2.1 +Name: json-e +Version: 2.7.0 +Summary: A data-structure parameterization system written for embedding context in JSON objects +Home-page: https://taskcluster.github.io/json-e/ +Author: Dustin J. Mitchell +Author-email: dustin@mozilla.com +License: MPL2 +Description: UNKNOWN +Platform: UNKNOWN +Provides-Extra: release diff --git a/third_party/python/json-e/README.md b/third_party/python/json-e/README.md new file mode 100644 index 0000000000..155b2e6ded --- /dev/null +++ b/third_party/python/json-e/README.md @@ -0,0 +1,730 @@ +* [Full documentation](https://taskcluster.github.io/json-e) + +# JSON-e + +JSON-e is a data-structure parameterization system for embedding context in +JSON objects. + +The central idea is to treat a data structure as a "template" and transform it, +using another data structure as context, to produce an output data structure. + +There are countless libraries to do this with strings, such as +[mustache](https://mustache.github.io/). What makes JSON-e unique is that it +operates on data structures, not on their textual representation. This allows +input to be written in a number of formats (JSON, YAML, etc.) or even generated +dynamically. It also means that the output cannot be "invalid", even when +including large chunks of contextual data. + +JSON-e is also designed to be safe for use on untrusted data. It never uses +`eval` or any other function that might result in arbitrary code execution. It +also disallows unbounded iteration, so any JSON-e rendering operation will +finish in finite time. + +## Changes + +See +[CHANGELOG.rst](https://github.com/taskcluster/json-e/blob/master/CHANGELOG.rst) +for the changes in each version of this library. + +# Interface + +## JavaScript + +The JS module is installed with either of + +```shell +npm install --save json-e +yarn add json-e +``` + +The module exposes following interface: + +```javascript +import jsone from 'json-e'; + +var template = {a: {$eval: "foo.bar"}}; +var context = {foo: {bar: "zoo"}}; +console.log(jsone(template, context)); +// -> { a: 'zoo' } +``` + +Note that the context can contain functions, and those functions can be called +from the template: + +```javascript +var template = {$eval: "foo(1)"}; +var context = {"foo": function(x) { return x + 2; }}; +console.log(jsone(template, context)); // -> 3 +``` + +*NOTE*: Context functions are called synchronously. Any complex asynchronous +operations should be handled before rendering the template. + +*NOTE*: If the template is untrusted, it can pass arbitrary data to functions +in the context, which must guard against such behavior. + +### Browser + +JSON-e is distributed as a CommonJS package is not designed to be included +directly in a browser with `<script>`. Instead, it must be incorproated using a +tool that understands CommonJS such as Webpack. See +[Neutrino](https://neutrino.js.org/) for an easy, configuration-free way to +build such applications. + +## Python + +The Python distribution exposes a `render` function: + +```python +import jsone + +template = {"a": {"$eval": "foo.bar"}} +context = {"foo": {"bar": "zoo"}} +print(jsone.render(template, context)) # -> {"a": "zoo"} +``` + +and also allows custom functions in the context: + +```python +template = {"$eval": "foo(1)"} +context = {"foo": lambda x: x + 2} +print(jsone.render(template, context)) # -> 3 +``` + +## Go (golang) + +The [golang package for json-e](https://godoc.org/github.com/taskcluster/json-e) exposes a `Render` function: + +```golang +import ( + "fmt" + "github.com/taskcluster/json-e" +) + +// Template must be given using types: +// map[string]interface{}, []interface{}, float64, string, bool, nil +// The same types that json.Unmarshal() will create when targeting an interface{} +template := map[string]interface{}{ + "result": map[string]interface{}{ + "$eval": "f() + 5", + }, +} +// Context can be JSON types just like template, but may also contain functions +// these can JSON types as arguments, and return a value and optionally an error. +context := map[string]interface{}{ + "f": func() int { return 37 }, +} + +func main() { + value, err := jsone.Render(template, context) + fmt.Printf("%#v\n", value) +} +``` + +## CLI + +You can use the 3rd party package [rjsone](https://wryun.github.io/rjsone/) to template +JSON-e from the command line, passing templates/contexts as files or arguments and using +stdout for the result. + + +# Language Reference + +The examples here are given in YAML for ease of reading. Of course, the +rendering operation takes place on the parsed data, so the input format is +irrelevant to its operation. + +## Simple Operations + +All JSON-e directives involve the `$` character, so a template without any directives is +rendered unchanged: + +```yaml +template: {key: [1,2,{key2: 'val', key3: 1}, true], f: false} +context: {} +result: {key: [1,2,{key2: 'val', key3: 1}, true], f: false} +``` + +## String Interpolation + +The simplest form of substitution occurs within strings, using `${..}`: + +```yaml +template: {message: 'hello ${key}', 'k=${num}': true} +context: {key: 'world', num: 1} +result: {message: 'hello world', 'k=1': true} +``` + +The bit inside the `${..}` is an expression, and must evaluate to something +that interpolates obviously into a string (so, a string, number, boolean,). +If it is null, then the expression interpolates into an empty string. +The expression syntax is described in more detail below. + +Values interpolate as their JSON literal values: + +```yaml +template: ["number: ${num}", "booleans: ${t} ${f}", "null: ${nil}"] +context: {num: 3, t: true, f: false, nil: null} +result: ["number: 3", "booleans: true false", "null: "] +``` + +Note that object keys can be interpolated, too: + +```yaml +template: {"tc_${name}": "${value}"} +context: {name: 'foo', value: 'bar'} +result: {"tc_foo": "bar"} +``` + +The string `${` can be escaped as `$${`. + +## Operators + +JSON-e defines a bunch of operators. Each is represented as an object with a +property beginning with `$`. This object can be buried deeply within the +template. Some operators take additional arguments as properties of the same +object. + +### `$eval` + +The `$eval` operator evaluates the given expression and is replaced with the +result of that evaluation. Unlike with string interpolation, the result need +not be a string, but can be an arbitrary data structure. + +```yaml +template: {config: {$eval: 'settings.staging'}} +context: + settings: + staging: + transactionBackend: mock + production: + transactionBackend: customerdb +result: {config: {transactionBackend: 'mock'}} +``` + +The expression syntax is described in more detail below. + +Note that `$eval`'s value must be a string. "Metaprogramming" by providing a +calculated value to eval is not allowed. For example, `{$eval: {$eval: +"${var1} + ${var2}"}}` is not valid JSON-e. + +### `$json` + +The `$json` operator formats the given value as JSON with sorted keys. It does +not evaluate the value (use `$eval` for that). While this can be useful in some +cases, it is an unusual case to include a JSON string in a larger data +structure. + +```yaml +template: {$json: [a, b, {$eval: 'a+b'}, 4]} +context: {a: 1, b: 2} +result: '["a", "b", 3, 4]' +``` + +### `$if` - `then` - `else` + +The `$if` operator supports conditionals. It evaluates the given value, and +replaces itself with the `then` or `else` properties. If either property is +omitted, then the expression is omitted from the parent object. + +```yaml +template: {key: {$if: 'cond', then: 1}, k2: 3} +context: {cond: true} +result: {key: 1, k2: 3} +``` + +```yaml +template: {$if: 'x > 5', then: 1, else: -1} +context: {x: 10} +result: 1 +``` + +```yaml +template: [1, {$if: 'cond', else: 2}, 3] +context: {cond: false} +result: [1,2,3] +``` + +```yaml +template: {key: {$if: 'cond', then: 2}, other: 3} +context: {cond: false} +result: {other: 3} +``` + +### `$flatten` + +The `$flatten` operator flattens an array of arrays into one array. + +```yaml +template: {$flatten: [[1, 2], [3, 4], [5]]} +context: {} +result: [1, 2, 3, 4, 5] +``` + +### `$flattenDeep` + +The `$flattenDeep` operator deeply flattens an array of arrays into one array. + +```yaml +template: {$flattenDeep: [[1, [2, [3]]]]} +context: {} +result: [1, 2, 3] +``` + +### `$fromNow` + +The `$fromNow` operator is a shorthand for the built-in function `fromNow`. It +creates a JSON (ISO 8601) datestamp for a time relative to the current time +(see the `now` builtin, below) or, if `from` is given, relative to that time. +The offset is specified by a sequence of number/unit pairs in a string. For +example: + +```yaml +template: {$fromNow: '2 days 1 hour'} +context: {} +result: '2017-01-19T16:27:20.974Z' +``` + +```yaml +template: {$fromNow: '1 hour', from: '2017-01-19T16:27:20.974Z'} +context: {} +result: '2017-01-19T17:27:20.974Z' +``` + +The available units are `day`, `hour`, and `minute`, for all of which a plural +is also accepted. + +### `$let` + +The `$let` operator evaluates an expression using a context amended with the +given values. It is analogous to the Haskell `where` clause. + +```yaml +template: {$let: {ts: 100, foo: 200}, + in: [{$eval: "ts+foo"}, {$eval: "ts-foo"}, {$eval: "ts*foo"}]} +context: {} +result: [300, -100, 20000] +``` + +The `$let` operator here added the `ts` and `foo` variables to the scope of +the context and accordingly evaluated the `in` clause using those variables +to return the correct result. + +The variable names in the `$let` value must be valid context variable names and +must be written literally. That is, an expression like `{$let: {$eval: +"extraVariables"}, in : ..}` is not allowed. + +### `$map` + +The `$map` operator evaluates an expression for each value of the given array or object, +constructing the result as an array or object of the evaluated values. + +When given an array, map always returns an array. + +```yaml +template: + $map: [2, 4, 6] + each(x): {$eval: 'x + a'} +context: {a: 1} +result: [3, 5, 7] +``` +The array or object is the value of the `$map` property, and the expression to evaluate +is given by `each(var)` where `var` is the name of the variable containing each +element. In the case of iterating over an object, `var` will be an object with two keys: +`key` and `val`. These keys correspond to a key in the object and its corresponding value. + +When $map is given an object, the expression defined by `each(var)` must evaluate to an +object for each key/value pair (`key` and `val`).The objects constructed by each 'each(var)' +can then be merged internally to give the resulting object with later keys overwriting +the previous ones.Otherwise the expression becomes invalid for the $map operator + +```yaml +template: + $map: {a: 1, b: 2, c: 3} + each(y): {'${y.key}x': {$eval: 'y.val + 1'}} +context: {} +result: {ax: 2, bx: 3, cx: 4} +``` + +### `$match` + +The `$match` operator is not dissimilar to pattern matching operators. It gets an object, in which every key is a string expression(s) to evaluate to `true` or `false` based on the context. The result will be an array of things (all types are supported) that were values corresponding to the keys that were evaluated to `true`. The order of the things in the array will be arbitrary. If there are no matches, the result is an empty array. + +```yaml +template: {$match: {"x == 10": "ten", "x == 20": "twenty"}} +context: {x: 10} +result: ["ten"] +``` + +```yaml +template: {$match: {"x == 10 || x == 20": "tens", "x == 10": "ten"}} +context: {x: 10} +one possible result: ["tens", "ten"] +another possible result: ["ten", "tens"] +``` +```yaml +template: {$match: {"x < 10": "tens"}} +context: {x: 10} +result: [] +``` + +### `$merge` + +The `$merge` operator merges an array of objects, returning a single object +that combines all of the objects in the array, where the right-side objects +overwrite the values of the left-side ones. + +```yaml +template: {$merge: [{a: 1, b: 1}, {b: 2, c: 3}, {d: 4}]} +context: {} +result: {a: 1, b: 2, c: 3, d: 4} +``` + +### `$mergeDeep` + +The `$mergeDeep` operator is like `$merge`, but it recurses into objects to +combine their contents property by property. Arrays are concatenated. + +```yaml +template: + $mergeDeep: + - task: + payload: + command: [a, b] + - task: + extra: + foo: bar + - task: + payload: + command: [c] +context: {} +result: + task: + extra: + foo: bar + payload: + command: [a, b, c] +``` + +### `$sort` + +The `$sort` operator sorts the given array. It takes a `by(var)` property which +should evaluate to a comparable value for each element. The `by(var)` property +defaults to the identity function. + +```yaml +template: + $sort: [{a: 2}, {a: 1, b: []}, {a: 3}] + by(x): 'x.a' +context: {} +result: [{a: 1, b: []}, {a: 2}, {a: 3}] +``` + +### `$reverse` + +The `$reverse` operator simply reverses the given array. + +```yaml +template: {$reverse: [3, 4, 1, 2]} +context: {} +result: [2, 1, 4, 3] +``` + +### Escaping operators + +All property names starting with `$` are reserved for JSON-e. +You can use `$$` to escape such properties: + +```yaml +template: {$$reverse: [3, 2, {$$eval: '2 - 1'}, 0]} +context: {} +result: {$reverse: [3, 2, {$eval: '2 - 1'}, 0]} +``` + +## Truthiness + +Many values can be evaluated in context where booleans are required, +not just booleans themselves. JSON-e defines the following values as false. +Anything else will be true. + +```yaml +template: {$if: 'a || b || c || d || e || f', then: "uh oh", else: "falsy" } +context: {a: null, b: [], c: {}, d: "", e: 0, f: false} +result: "falsy" +``` + +## Expression Syntax + +Expression are given in a simple Python- or JavaScript-like expression +language. Its data types are limited to JSON types plus function objects. + +### Literals + +Literals are similar to those for JSON. Numeric literals only accept integer +and decimal notation. Strings do not support any kind of escaping. The use of +`\n` and `\t` in the example below depends on the YAML parser to expand the +escapes. + +```yaml +template: + - {$eval: "1.3"} + - {$eval: "'abc'"} + - {$eval: '"abc"'} + - {$eval: "'\n\t'"} +context: {} +result: + - 1.3 + - "abc" + - "abc" + - "\n\t" +``` + +Array and object literals also look much like JSON, with bare identifiers +allowed as keys like in Javascript: + +```yaml +template: + - {$eval: '[1, 2, "three"]'} + - {$eval: '{foo: 1, "bar": 2}'} +context: {} +result: + - [1, 2, "three"] + - {"foo": 1, "bar": 2} +``` + +### Context References + +Bare identifiers refer to items from the context or to built-ins (described below). + +```yaml +template: {$eval: '[x, z, x+z]'} +context: {x: 'quick', z: 'sort'} +reslut: ['quick', 'sort', 'quicksort'] +``` + +### Arithmetic Operations + +The usual arithmetic operators are all defined, with typical associativity and +precedence: + +```yaml +template: + - {$eval: 'x + z'} + - {$eval: 's + t'} + - {$eval: 'z - x'} + - {$eval: 'x * z'} + - {$eval: 'z / x'} + - {$eval: 'z ** 2'} + - {$eval: '(z / x) ** 2'} +context: {x: 10, z: 20, s: "face", t: "plant"} +result: + - 30 + - "faceplant" + - 10 + - 200 + - 2 + - 400 + - 4 +``` + +Note that strings can be concatenated with `+`, but none of the other operators +apply. + +### Comparison Operations + +Comparisons work as expected. Equality is "deep" in the sense of doing +comparisons of the contents of data structures. + +```yaml +template: + - {$eval: 'x < z'} + - {$eval: 'x <= z'} + - {$eval: 'x > z'} + - {$eval: 'x >= z'} + - {$eval: 'deep == [1, [3, {a: 5}]]'} + - {$eval: 'deep != [1, [3, {a: 5}]]'} +context: {x: -10, z: 10, deep: [1, [3, {a: 5}]]} +result: [true, true, false, false, true, false] +``` + +### Boolean Operations + +Boolean operations use C- and Javascript-style symbls `||`, `&&`, and `!`: + +```yaml +template: {$eval: '!(false || false) && true'} +context: {} +result: true +``` + +### Object Property Access + +Like Javascript, object properties can be accessed either with array-index +syntax or with dot syntax. Unlike Javascript, `obj.prop` is an error if `obj` +does not have `prop`, while `obj['prop']` will evaulate to `null`. + +```yaml +template: {$eval: 'v.a + v["b"]'} +context: {v: {a: 'apple', b: 'bananna', c: 'carrot'}} +result: 'applebananna' +```` + +### Indexing and Slicing + +Strings and arrays can be indexed and sliced using a Python-like indexing +scheme. Negative indexes are counted from the end of the value. Slices are +treated as "half-open", meaning that the result contains the first index and +does not contain the second index. A "backward" slice with the start index +greater than the end index is treated as empty. + +```yaml +template: + - {$eval: '[array[1], string[1]]'} + - {$eval: '[array[1:4], string[1:4]]'} + - {$eval: '[array[2:], string[2:]]'} + - {$eval: '[array[:2], string[:2]]'} + - {$eval: '[array[4:2], string[4:2]]'} + - {$eval: '[array[-2], string[-2]]'} + - {$eval: '[array[-2:], string[-2:]]'} + - {$eval: '[array[:-3], string[:-3]]'} +context: {array: ['a', 'b', 'c', 'd', 'e'], string: 'abcde'} +result: + - ['b', 'b'] + - [['b', 'c', 'd'], 'bcd'] + - [['c', 'd', 'e'], 'cde'] + - [['a', 'b'], 'ab'] + - [[], ''] + - ['d', 'd'] + - [['d', 'e'], 'de'] + - [['a', 'b'], 'ab'] +``` + +### Containment Operation + +The `in` keyword can be used to check for containment: a property in an object, +an element in an array, or a substring in a string. + +```yaml +template: + - {$eval: '"foo" in {foo: 1, bar: 2}'} + - {$eval: '"foo" in ["foo", "bar"]'} + - {$eval: '"foo" in "foobar"'} +context: {} +result: [true, true, true] +``` + +### Function Invocation + +Function calls are made with the usual `fn(arg1, arg2)` syntax. Functions are +not JSON data, so they cannot be created in JSON-e, but they can be provided as +built-ins or supplied in the context and called from JSON-e. + +### Built-In Functions and Variables + +The expression language provides a laundry-list of built-in functions/variables. Library +users can easily add additional functions/variables, or override the built-ins, as part +of the context. + +#### Time + +The built-in context value `now` is set to the current time at the start of +evaluation of the template, and used as the default "from" value for `$fromNow` +and the built-in `fromNow()`. + +```yaml +template: + - {$eval: 'now'} + - {$eval: 'fromNow("1 minute")'} + - {$eval: 'fromNow("1 minute", "2017-01-19T16:27:20.974Z")'} +context: {} +result: + - '2017-01-19T16:27:20.974Z', + - '2017-01-19T16:28:20.974Z', + - '2017-01-19T16:28:20.974Z', +``` + +#### Math + +```yaml +template: + # the smallest of the arguments + - {$eval: 'min(1, 3, 5)'} + # the largest of the arguments + - {$eval: 'max(2, 4, 6)'} + # mathematical functions + - {$eval: 'sqrt(16)'} + - {$eval: 'ceil(0.3)'} + - {$eval: 'floor(0.3)'} + - {$eval: 'abs(-0.3)'} +context: {} +result: + - 1 + - 6 + - 4 + - 1 + - 0 + - 0.3 +``` + +#### Strings + +```yaml +template: + # convert string case + - {$eval: 'lowercase("Fools!")'} + - {$eval: 'uppercase("Fools!")'} + # convert string, number, boolean, or array to string + - {$eval: 'str(130)'} + # strip whitespace from left, right, or both ends of a string + - {$eval: 'lstrip(" room ")'} + - {$eval: 'rstrip(" room ")'} + - {$eval: 'strip(" room ")'} +context: {} +result: + - "fools!" + - "FOOLS!" + - "130" + - "room " + - " room" + - room +``` + +#### Type + +The `typeof()` built-in returns the type of an object. Its behavior around +`null` is reminiscent of JavaScript. + +```yaml +template: + - "${typeof('abc')}" + - "${typeof(42)}" + - "${typeof(42.0)}" + - "${typeof(true)}" + - "${typeof([])}" + - "${typeof({})}" + - "${typeof(typeof)}" + - {$eval: "typeof(null)"} + - "${typeof(null)}" +context: {} +result: + - string + - number + - number + - boolean + - array + - object + - function + - null # note: the value null, not the string "null" + - '' # .. which interpolates to an empty string +``` + +#### Length + +The `len()` built-in returns the length of a string or array. + +```yaml +template: {$eval: 'len([1, 2, 3])'} +context: {} +result: 3 +``` + diff --git a/third_party/python/json-e/jsone/__init__.py b/third_party/python/json-e/jsone/__init__.py new file mode 100644 index 0000000000..943674e672 --- /dev/null +++ b/third_party/python/json-e/jsone/__init__.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import, print_function, unicode_literals + +import re +from .render import renderValue +from .shared import JSONTemplateError, DeleteMarker, TemplateError, fromNow +from . import builtins + +_context_re = re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*$') + + +def render(template, context): + if not all(_context_re.match(c) for c in context): + raise TemplateError('top level keys of context must follow ' + '/[a-zA-Z_][a-zA-Z0-9_]*/') + full_context = {'now': fromNow('0 seconds', None)} + full_context.update(builtins.build(full_context)) + full_context.update(context) + rv = renderValue(template, full_context) + if rv is DeleteMarker: + return None + return rv diff --git a/third_party/python/json-e/jsone/builtins.py b/third_party/python/json-e/jsone/builtins.py new file mode 100644 index 0000000000..751ee2dc04 --- /dev/null +++ b/third_party/python/json-e/jsone/builtins.py @@ -0,0 +1,121 @@ +from __future__ import absolute_import, print_function, unicode_literals + +import math +from .shared import string, to_str, fromNow, JSONTemplateError + + +class BuiltinError(JSONTemplateError): + pass + + +def build(context): + builtins = {} + + def builtin(name, variadic=None, argument_tests=None, minArgs=None): + def wrap(fn): + def bad(reason=None): + raise BuiltinError( + (reason or 'invalid arguments to builtin: {}').format(name)) + if variadic: + def invoke(*args): + if minArgs: + if len(args) < minArgs: + bad("too few arguments to {}") + for arg in args: + if not variadic(arg): + bad() + return fn(*args) + + elif argument_tests: + def invoke(*args): + if len(args) != len(argument_tests): + bad() + for t, arg in zip(argument_tests, args): + if not t(arg): + bad() + return fn(*args) + + else: + def invoke(*args): + return fn(*args) + + builtins[name] = invoke + return fn + return wrap + + def is_number(v): + return isinstance(v, (int, float)) and not isinstance(v, bool) + + def is_string(v): + return isinstance(v, string) + + def is_string_or_array(v): + return isinstance(v, (string, list)) + + def anything_except_array(v): + return isinstance(v, (string, int, float, bool)) or v is None + + def anything(v): + return isinstance(v, (string, int, float, list, dict)) or v is None or callable(v) + + # --- + + builtin('min', variadic=is_number, minArgs=1)(min) + builtin('max', variadic=is_number, minArgs=1)(max) + builtin('sqrt', argument_tests=[is_number])(math.sqrt) + builtin('abs', argument_tests=[is_number])(abs) + + @builtin('ceil', argument_tests=[is_number]) + def ceil(v): + return int(math.ceil(v)) + + @builtin('floor', argument_tests=[is_number]) + def floor(v): + return int(math.floor(v)) + + @builtin('lowercase', argument_tests=[is_string]) + def lowercase(v): + return v.lower() + + @builtin('uppercase', argument_tests=[is_string]) + def lowercase(v): + return v.upper() + + builtin('len', argument_tests=[is_string_or_array])(len) + builtin('str', argument_tests=[anything_except_array])(to_str) + builtin('number', variadic=is_string, minArgs=1)(float) + + @builtin('strip', argument_tests=[is_string]) + def strip(s): + return s.strip() + + @builtin('rstrip', argument_tests=[is_string]) + def rstrip(s): + return s.rstrip() + + @builtin('lstrip', argument_tests=[is_string]) + def lstrip(s): + return s.lstrip() + + @builtin('fromNow', variadic=is_string, minArgs=1) + def fromNow_builtin(offset, reference=None): + return fromNow(offset, reference or context.get('now')) + + @builtin('typeof', argument_tests=[anything]) + def typeof(v): + if isinstance(v, bool): + return 'boolean' + elif isinstance(v, string): + return 'string' + elif isinstance(v, (int, float)): + return 'number' + elif isinstance(v, list): + return 'array' + elif isinstance(v, dict): + return 'object' + elif v is None: + return None + elif callable(v): + return 'function' + + return builtins diff --git a/third_party/python/json-e/jsone/interpreter.py b/third_party/python/json-e/jsone/interpreter.py new file mode 100644 index 0000000000..eb38a9c85b --- /dev/null +++ b/third_party/python/json-e/jsone/interpreter.py @@ -0,0 +1,289 @@ +from __future__ import absolute_import, print_function, unicode_literals + +from .prattparser import PrattParser, infix, prefix +from .shared import TemplateError, InterpreterError, string +import operator +import json + +OPERATORS = { + '-': operator.sub, + '*': operator.mul, + '/': operator.truediv, + '**': operator.pow, + '==': operator.eq, + '!=': operator.ne, + '<=': operator.le, + '<': operator.lt, + '>': operator.gt, + '>=': operator.ge, + '&&': lambda a, b: bool(a and b), + '||': lambda a, b: bool(a or b), +} + + +def infixExpectationError(operator, expected): + return InterpreterError('infix: {} expects {} {} {}'. + format(operator, expected, operator, expected)) + + +class ExpressionEvaluator(PrattParser): + + ignore = '\\s+' + patterns = { + 'number': '[0-9]+(?:\\.[0-9]+)?', + 'identifier': '[a-zA-Z_][a-zA-Z_0-9]*', + 'string': '\'[^\']*\'|"[^"]*"', + # avoid matching these as prefixes of identifiers e.g., `insinutations` + 'true': 'true(?![a-zA-Z_0-9])', + 'false': 'false(?![a-zA-Z_0-9])', + 'in': 'in(?![a-zA-Z_0-9])', + 'null': 'null(?![a-zA-Z_0-9])', + } + tokens = [ + '**', '+', '-', '*', '/', '[', ']', '.', '(', ')', '{', '}', ':', ',', + '>=', '<=', '<', '>', '==', '!=', '!', '&&', '||', 'true', 'false', 'in', + 'null', 'number', 'identifier', 'string', + ] + precedence = [ + ['||'], + ['&&'], + ['in'], + ['==', '!='], + ['>=', '<=', '<', '>'], + ['+', '-'], + ['*', '/'], + ['**-right-associative'], + ['**'], + ['[', '.'], + ['('], + ['unary'], + ] + + def __init__(self, context): + super(ExpressionEvaluator, self).__init__() + self.context = context + + def parse(self, expression): + if not isinstance(expression, string): + raise TemplateError('expression to be evaluated must be a string') + return super(ExpressionEvaluator, self).parse(expression) + + @prefix('number') + def number(self, token, pc): + v = token.value + return float(v) if '.' in v else int(v) + + @prefix("!") + def bang(self, token, pc): + return not pc.parse('unary') + + @prefix("-") + def uminus(self, token, pc): + v = pc.parse('unary') + if not isNumber(v): + raise InterpreterError('{} expects {}'.format('unary -', 'number')) + return -v + + @prefix("+") + def uplus(self, token, pc): + v = pc.parse('unary') + if not isNumber(v): + raise InterpreterError('{} expects {}'.format('unary +', 'number')) + return v + + @prefix("identifier") + def identifier(self, token, pc): + try: + return self.context[token.value] + except KeyError: + raise InterpreterError( + 'unknown context value {}'.format(token.value)) + + @prefix("null") + def null(self, token, pc): + return None + + @prefix("[") + def array_bracket(self, token, pc): + return parseList(pc, ',', ']') + + @prefix("(") + def grouping_paren(self, token, pc): + rv = pc.parse() + pc.require(')') + return rv + + @prefix("{") + def object_brace(self, token, pc): + return parseObject(pc) + + @prefix("string") + def string(self, token, pc): + return parseString(token.value) + + @prefix("true") + def true(self, token, pc): + return True + + @prefix("false") + def false(self, token, ps): + return False + + @infix("+") + def plus(self, left, token, pc): + if not isinstance(left, (string, int, float)) or isinstance(left, bool): + raise infixExpectationError('+', 'number/string') + right = pc.parse(token.kind) + if not isinstance(right, (string, int, float)) or isinstance(right, bool): + raise infixExpectationError('+', 'number/string') + if type(right) != type(left) and \ + (isinstance(left, string) or isinstance(right, string)): + raise infixExpectationError('+', 'numbers/strings') + return left + right + + @infix('-', '*', '/', '**') + def arith(self, left, token, pc): + op = token.kind + if not isNumber(left): + raise infixExpectationError(op, 'number') + right = pc.parse({'**': '**-right-associative'}.get(op)) + if not isNumber(right): + raise infixExpectationError(op, 'number') + return OPERATORS[op](left, right) + + @infix("[") + def index_slice(self, left, token, pc): + a = None + b = None + is_interval = False + if pc.attempt(':'): + a = 0 + is_interval = True + else: + a = pc.parse() + if pc.attempt(':'): + is_interval = True + + if is_interval and not pc.attempt(']'): + b = pc.parse() + pc.require(']') + + if not is_interval: + pc.require(']') + + return accessProperty(left, a, b, is_interval) + + @infix(".") + def property_dot(self, left, token, pc): + if not isinstance(left, dict): + raise infixExpectationError('.', 'object') + k = pc.require('identifier').value + try: + return left[k] + except KeyError: + raise TemplateError( + '{} not found in {}'.format(k, json.dumps(left))) + + @infix("(") + def function_call(self, left, token, pc): + if not callable(left): + raise TemplateError('function call', 'callable') + args = parseList(pc, ',', ')') + return left(*args) + + @infix('==', '!=', '||', '&&') + def equality_and_logic(self, left, token, pc): + op = token.kind + right = pc.parse(op) + return OPERATORS[op](left, right) + + @infix('<=', '<', '>', '>=') + def inequality(self, left, token, pc): + op = token.kind + right = pc.parse(op) + if type(left) != type(right) or \ + not (isinstance(left, (int, float, string)) and not isinstance(left, bool)): + raise infixExpectationError(op, 'numbers/strings') + return OPERATORS[op](left, right) + + @infix("in") + def contains(self, left, token, pc): + right = pc.parse(token.kind) + if isinstance(right, dict): + if not isinstance(left, string): + raise infixExpectationError('in-object', 'string on left side') + elif isinstance(right, string): + if not isinstance(left, string): + raise infixExpectationError('in-string', 'string on left side') + elif not isinstance(right, list): + raise infixExpectationError( + 'in', 'Array, string, or object on right side') + try: + return left in right + except TypeError: + raise infixExpectationError('in', 'scalar value, collection') + + +def isNumber(v): + return isinstance(v, (int, float)) and not isinstance(v, bool) + + +def parseString(v): + return v[1:-1] + + +def parseList(pc, separator, terminator): + rv = [] + if not pc.attempt(terminator): + while True: + rv.append(pc.parse()) + if not pc.attempt(separator): + break + pc.require(terminator) + return rv + + +def parseObject(pc): + rv = {} + if not pc.attempt('}'): + while True: + k = pc.require('identifier', 'string') + if k.kind == 'string': + k = parseString(k.value) + else: + k = k.value + pc.require(':') + v = pc.parse() + rv[k] = v + if not pc.attempt(','): + break + pc.require('}') + return rv + + +def accessProperty(value, a, b, is_interval): + if isinstance(value, (list, string)): + if is_interval: + if b is None: + b = len(value) + try: + return value[a:b] + except TypeError: + raise infixExpectationError('[..]', 'integer') + else: + try: + return value[a] + except IndexError: + raise TemplateError('index out of bounds') + except TypeError: + raise infixExpectationError('[..]', 'integer') + + if not isinstance(value, dict): + raise infixExpectationError('[..]', 'object, array, or string') + if not isinstance(a, string): + raise infixExpectationError('[..]', 'string index') + + try: + return value[a] + except KeyError: + return None diff --git a/third_party/python/json-e/jsone/prattparser.py b/third_party/python/json-e/jsone/prattparser.py new file mode 100644 index 0000000000..5bf250a816 --- /dev/null +++ b/third_party/python/json-e/jsone/prattparser.py @@ -0,0 +1,191 @@ +from __future__ import absolute_import, print_function, unicode_literals + +import re +from collections import namedtuple +from .shared import TemplateError +from .six import with_metaclass, viewitems + + +class SyntaxError(TemplateError): + + @classmethod + def unexpected(cls, got, exp): + exp = ', '.join(sorted(exp)) + return cls('Found {}, expected {}'.format(got.value, exp)) + + +Token = namedtuple('Token', ['kind', 'value', 'start', 'end']) + + +def prefix(*kinds): + """Decorate a method as handling prefix tokens of the given kinds""" + def wrap(fn): + try: + fn.prefix_kinds.extend(kinds) + except AttributeError: + fn.prefix_kinds = list(kinds) + return fn + return wrap + + +def infix(*kinds): + """Decorate a method as handling infix tokens of the given kinds""" + def wrap(fn): + try: + fn.infix_kinds.extend(kinds) + except AttributeError: + fn.infix_kinds = list(kinds) + return fn + return wrap + + +class PrattParserMeta(type): + + def __init__(cls, name, bases, body): + # set up rules based on decorated methods + infix_rules = cls.infix_rules = {} + prefix_rules = cls.prefix_rules = {} + for prop, value in viewitems(body): + if hasattr(value, 'prefix_kinds'): + for kind in value.prefix_kinds: + prefix_rules[kind] = value + delattr(cls, prop) + if hasattr(value, 'infix_kinds'): + for kind in value.infix_kinds: + infix_rules[kind] = value + delattr(cls, prop) + + # build a regular expression to generate a sequence of tokens + token_patterns = [ + '({})'.format(cls.patterns.get(t, re.escape(t))) + for t in cls.tokens] + if cls.ignore: + token_patterns.append('(?:{})'.format(cls.ignore)) + cls.token_re = re.compile('^(?:' + '|'.join(token_patterns) + ')') + + # build a map from token kind to precedence level + cls.precedence_map = { + kind: prec + 1 + for (prec, row) in enumerate(cls.precedence) + for kind in row + } + + +class PrattParser(with_metaclass(PrattParserMeta, object)): + + # regular expression for ignored input (e.g., whitespace) + ignore = None + + # regular expressions for tokens that do not match themselves + patterns = {} + + # all token kinds (note that order matters - the first matching token + # will be returned) + tokens = [] + + # precedence of tokens, as a list of lists, from lowest to highest + precedence = [] + + def parse(self, source): + pc = ParseContext(self, source, self._generate_tokens(source)) + result = pc.parse() + # if there are any tokens remaining, that's an error.. + token = pc.attempt() + if token: + raise SyntaxError.unexpected(token, self.infix_rules) + return result + + def parseUntilTerminator(self, source, terminator): + pc = ParseContext(self, source, self._generate_tokens(source)) + result = pc.parse() + token = pc.attempt() + if token.kind != terminator: + raise SyntaxError.unexpected(token, [terminator]) + return (result, token.start) + + def _generate_tokens(self, source): + offset = 0 + while True: + start = offset + remainder = source[offset:] + mo = self.token_re.match(remainder) + if not mo: + if remainder: + raise SyntaxError( + "Unexpected input: '{}'".format(remainder)) + break + offset += mo.end() + + # figure out which token matched (note that idx is 0-based) + indexes = list( + filter(lambda x: x[1] is not None, enumerate(mo.groups()))) + if indexes: + idx = indexes[0][0] + yield Token( + kind=self.tokens[idx], + value=mo.group(idx + 1), # (mo.group is 1-based) + start=start, + end=offset) + + +class ParseContext(object): + + def __init__(self, parser, source, token_generator): + self.parser = parser + self.source = source + + self._tokens = token_generator + self._error = None + + self._advance() + + def _advance(self): + try: + self.next_token = next(self._tokens) + except StopIteration: + self.next_token = None + except SyntaxError as exc: + self._error = exc + + def attempt(self, *kinds): + """Try to get the next token if it matches one of the kinds given, + otherwise returning None. If no kinds are given, any kind is + accepted.""" + if self._error: + raise self._error + token = self.next_token + if not token: + return None + if kinds and token.kind not in kinds: + return None + self._advance() + return token + + def require(self, *kinds): + """Get the next token, raising an exception if it doesn't match one of + the given kinds, or the input ends. If no kinds are given, returns the + next token of any kind.""" + token = self.attempt() + if not token: + raise SyntaxError('Unexpected end of input') + if kinds and token.kind not in kinds: + raise SyntaxError.unexpected(token, kinds) + return token + + def parse(self, precedence=None): + parser = self.parser + precedence = parser.precedence_map[precedence] if precedence else 0 + token = self.require() + prefix_rule = parser.prefix_rules.get(token.kind) + if not prefix_rule: + raise SyntaxError.unexpected(token, parser.prefix_rules) + left = prefix_rule(parser, token, self) + while self.next_token: + kind = self.next_token.kind + if kind not in parser.infix_rules: + break + if precedence >= parser.precedence_map[kind]: + break + token = self.require() + left = parser.infix_rules[kind](parser, left, token, self) + return left diff --git a/third_party/python/json-e/jsone/render.py b/third_party/python/json-e/jsone/render.py new file mode 100644 index 0000000000..e820da1ec2 --- /dev/null +++ b/third_party/python/json-e/jsone/render.py @@ -0,0 +1,354 @@ +from __future__ import absolute_import, print_function, unicode_literals + +import re +import json as json +from .shared import JSONTemplateError, TemplateError, DeleteMarker, string, to_str +from . import shared +from .interpreter import ExpressionEvaluator +from .six import viewitems +import functools + +operators = {} +IDENTIFIER_RE = re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*$') + + +def operator(name): + def wrap(fn): + operators[name] = fn + return fn + return wrap + + +def evaluateExpression(expr, context): + evaluator = ExpressionEvaluator(context) + return evaluator.parse(expr) + + +_interpolation_start_re = re.compile(r'\$?\${') + + +def interpolate(string, context): + mo = _interpolation_start_re.search(string) + if not mo: + return string + + result = [] + evaluator = ExpressionEvaluator(context) + + while True: + result.append(string[:mo.start()]) + if mo.group() != '$${': + string = string[mo.end():] + parsed, offset = evaluator.parseUntilTerminator(string, '}') + if isinstance(parsed, (list, dict)): + raise TemplateError( + "interpolation of '{}' produced an array or object".format(string[:offset])) + if to_str(parsed) == "null": + result.append("") + else: + result.append(to_str(parsed)) + string = string[offset + 1:] + else: # found `$${` + result.append('${') + string = string[mo.end():] + + mo = _interpolation_start_re.search(string) + if not mo: + result.append(string) + break + + return ''.join(result) + + +def checkUndefinedProperties(template, allowed): + unknownKeys = [] + combined = "|".join(allowed) + "$" + unknownKeys = [key for key in sorted(template) + if not re.match(combined, key)] + if unknownKeys: + raise TemplateError(allowed[0].replace('\\', '') + + " has undefined properties: " + " ".join(unknownKeys)) + + +@operator('$eval') +def eval(template, context): + checkUndefinedProperties(template, ['\$eval']) + if not isinstance(template['$eval'], string): + raise TemplateError("$eval must be given a string expression") + return evaluateExpression(template['$eval'], context) + + +@operator('$flatten') +def flatten(template, context): + checkUndefinedProperties(template, ['\$flatten']) + value = renderValue(template['$flatten'], context) + if not isinstance(value, list): + raise TemplateError('$flatten value must evaluate to an array') + + def gen(): + for e in value: + if isinstance(e, list): + for e2 in e: + yield e2 + else: + yield e + return list(gen()) + + +@operator('$flattenDeep') +def flattenDeep(template, context): + checkUndefinedProperties(template, ['\$flattenDeep']) + value = renderValue(template['$flattenDeep'], context) + if not isinstance(value, list): + raise TemplateError('$flattenDeep value must evaluate to an array') + + def gen(value): + if isinstance(value, list): + for e in value: + for sub in gen(e): + yield sub + else: + yield value + + return list(gen(value)) + + +@operator('$fromNow') +def fromNow(template, context): + checkUndefinedProperties(template, ['\$fromNow', 'from']) + offset = renderValue(template['$fromNow'], context) + reference = renderValue( + template['from'], context) if 'from' in template else context.get('now') + + if not isinstance(offset, string): + raise TemplateError("$fromNow expects a string") + return shared.fromNow(offset, reference) + + +@operator('$if') +def ifConstruct(template, context): + checkUndefinedProperties(template, ['\$if', 'then', 'else']) + condition = evaluateExpression(template['$if'], context) + try: + if condition: + rv = template['then'] + else: + rv = template['else'] + except KeyError: + return DeleteMarker + return renderValue(rv, context) + + +@operator('$json') +def jsonConstruct(template, context): + checkUndefinedProperties(template, ['\$json']) + value = renderValue(template['$json'], context) + return json.dumps(value, separators=(',', ':'), sort_keys=True, ensure_ascii=False) + + +@operator('$let') +def let(template, context): + checkUndefinedProperties(template, ['\$let', 'in']) + if not isinstance(template['$let'], dict): + raise TemplateError("$let value must be an object") + + subcontext = context.copy() + for k, v in template['$let'].items(): + if not IDENTIFIER_RE.match(k): + raise TemplateError('top level keys of $let must follow /[a-zA-Z_][a-zA-Z0-9_]*/') + subcontext[k] = renderValue(v, context) + + try: + in_expression = template['in'] + except KeyError: + raise TemplateError("$let operator requires an `in` clause") + return renderValue(in_expression, subcontext) + + +@operator('$map') +def map(template, context): + EACH_RE = 'each\([a-zA-Z_][a-zA-Z0-9_]*\)' + checkUndefinedProperties(template, ['\$map', EACH_RE]) + value = renderValue(template['$map'], context) + if not isinstance(value, list) and not isinstance(value, dict): + raise TemplateError("$map value must evaluate to an array or object") + + is_obj = isinstance(value, dict) + + each_keys = [k for k in template if k.startswith('each(')] + if len(each_keys) != 1: + raise TemplateError( + "$map requires exactly one other property, each(..)") + each_key = each_keys[0] + each_var = each_key[5:-1] + each_template = template[each_key] + + def gen(val): + subcontext = context.copy() + for elt in val: + subcontext[each_var] = elt + elt = renderValue(each_template, subcontext) + if elt is not DeleteMarker: + yield elt + if is_obj: + value = [{'key': v[0], 'val': v[1]} for v in value.items()] + v = dict() + for e in gen(value): + if not isinstance(e, dict): + raise TemplateError( + "$map on objects expects {0} to evaluate to an object".format(each_key)) + v.update(e) + return v + else: + return list(gen(value)) + + +@operator('$match') +def matchConstruct(template, context): + checkUndefinedProperties(template, ['\$match']) + + if not isinstance(template['$match'], dict): + raise TemplateError("$match can evaluate objects only") + + result = [] + for condition in template['$match']: + if evaluateExpression(condition, context): + result.append(renderValue(template['$match'][condition], context)) + + return result + + +@operator('$merge') +def merge(template, context): + checkUndefinedProperties(template, ['\$merge']) + value = renderValue(template['$merge'], context) + if not isinstance(value, list) or not all(isinstance(e, dict) for e in value): + raise TemplateError( + "$merge value must evaluate to an array of objects") + v = dict() + for e in value: + v.update(e) + return v + + +@operator('$mergeDeep') +def merge(template, context): + checkUndefinedProperties(template, ['\$mergeDeep']) + value = renderValue(template['$mergeDeep'], context) + if not isinstance(value, list) or not all(isinstance(e, dict) for e in value): + raise TemplateError( + "$mergeDeep value must evaluate to an array of objects") + + def merge(l, r): + if isinstance(l, list) and isinstance(r, list): + return l + r + if isinstance(l, dict) and isinstance(r, dict): + res = l.copy() + for k, v in viewitems(r): + if k in l: + res[k] = merge(l[k], v) + else: + res[k] = v + return res + return r + if len(value) == 0: + return {} + return functools.reduce(merge, value[1:], value[0]) + + +@operator('$reverse') +def reverse(template, context): + checkUndefinedProperties(template, ['\$reverse']) + value = renderValue(template['$reverse'], context) + if not isinstance(value, list): + raise TemplateError("$reverse value must evaluate to an array of objects") + return list(reversed(value)) + + +@operator('$sort') +def sort(template, context): + BY_RE = 'by\([a-zA-Z_][a-zA-Z0-9_]*\)' + checkUndefinedProperties(template, ['\$sort', BY_RE]) + value = renderValue(template['$sort'], context) + if not isinstance(value, list): + raise TemplateError('$sorted values to be sorted must have the same type') + + # handle by(..) if given, applying the schwartzian transform + by_keys = [k for k in template if k.startswith('by(')] + if len(by_keys) == 1: + by_key = by_keys[0] + by_var = by_key[3:-1] + by_expr = template[by_key] + + def xform(): + subcontext = context.copy() + for e in value: + subcontext[by_var] = e + yield evaluateExpression(by_expr, subcontext), e + to_sort = list(xform()) + elif len(by_keys) == 0: + to_sort = [(e, e) for e in value] + else: + raise TemplateError('only one by(..) is allowed') + + # check types + try: + eltype = type(to_sort[0][0]) + except IndexError: + return [] + if eltype in (list, dict, bool, type(None)): + raise TemplateError('$sorted values to be sorted must have the same type') + if not all(isinstance(e[0], eltype) for e in to_sort): + raise TemplateError('$sorted values to be sorted must have the same type') + + # unzip the schwartzian transform + return list(e[1] for e in sorted(to_sort)) + + +def renderValue(template, context): + if isinstance(template, string): + return interpolate(template, context) + + elif isinstance(template, dict): + matches = [k for k in template if k in operators] + if matches: + if len(matches) > 1: + raise TemplateError("only one operator allowed") + return operators[matches[0]](template, context) + + def updated(): + for k, v in viewitems(template): + if k.startswith('$$'): + k = k[1:] + elif k.startswith('$') and IDENTIFIER_RE.match(k[1:]): + raise TemplateError( + '$<identifier> is reserved; ues $$<identifier>') + else: + k = interpolate(k, context) + + try: + v = renderValue(v, context) + except JSONTemplateError as e: + if IDENTIFIER_RE.match(k): + e.add_location('.{}'.format(k)) + else: + e.add_location('[{}]'.format(json.dumps(k))) + raise + if v is not DeleteMarker: + yield k, v + return dict(updated()) + + elif isinstance(template, list): + def updated(): + for i, e in enumerate(template): + try: + v = renderValue(e, context) + if v is not DeleteMarker: + yield v + except JSONTemplateError as e: + e.add_location('[{}]'.format(i)) + raise + return list(updated()) + + else: + return template diff --git a/third_party/python/json-e/jsone/shared.py b/third_party/python/json-e/jsone/shared.py new file mode 100644 index 0000000000..0e70e21f81 --- /dev/null +++ b/third_party/python/json-e/jsone/shared.py @@ -0,0 +1,131 @@ +from __future__ import absolute_import, print_function, unicode_literals + +import re +import datetime + + +class DeleteMarker: + pass + + +class JSONTemplateError(Exception): + def __init__(self, message): + super(JSONTemplateError, self).__init__(message) + self.location = [] + + def add_location(self, loc): + self.location.insert(0, loc) + + def __str__(self): + location = ' at template' + ''.join(self.location) + return "{}{}: {}".format( + self.__class__.__name__, + location if self.location else '', + self.args[0]) + + +class TemplateError(JSONTemplateError): + pass + + +class InterpreterError(JSONTemplateError): + pass + + +# Regular expression matching: X days Y hours Z minutes +# todo: support hr, wk, yr +FROMNOW_RE = re.compile(''.join([ + '^(\s*(?P<years>\d+)\s*y(ears?)?)?', + '(\s*(?P<months>\d+)\s*mo(nths?)?)?', + '(\s*(?P<weeks>\d+)\s*w(eeks?)?)?', + '(\s*(?P<days>\d+)\s*d(ays?)?)?', + '(\s*(?P<hours>\d+)\s*h(ours?)?)?', + '(\s*(?P<minutes>\d+)\s*m(in(utes?)?)?)?\s*', + '(\s*(?P<seconds>\d+)\s*s(ec(onds?)?)?)?\s*$', +])) + + +def fromNow(offset, reference): + # copied from taskcluster-client.py + # We want to handle past dates as well as future + future = True + offset = offset.lstrip() + if offset.startswith('-'): + future = False + offset = offset[1:].lstrip() + if offset.startswith('+'): + offset = offset[1:].lstrip() + + # Parse offset + m = FROMNOW_RE.match(offset) + if m is None: + raise ValueError("offset string: '%s' does not parse" % offset) + + # In order to calculate years and months we need to calculate how many days + # to offset the offset by, since timedelta only goes as high as weeks + days = 0 + hours = 0 + minutes = 0 + seconds = 0 + if m.group('years'): + # forget leap years, a year is 365 days + years = int(m.group('years')) + days += 365 * years + if m.group('months'): + # assume "month" means 30 days + months = int(m.group('months')) + days += 30 * months + days += int(m.group('days') or 0) + hours += int(m.group('hours') or 0) + minutes += int(m.group('minutes') or 0) + seconds += int(m.group('seconds') or 0) + + # Offset datetime from utc + delta = datetime.timedelta( + weeks=int(m.group('weeks') or 0), + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + ) + + if isinstance(reference, string): + reference = datetime.datetime.strptime( + reference, '%Y-%m-%dT%H:%M:%S.%fZ') + elif reference is None: + reference = datetime.datetime.utcnow() + return stringDate(reference + delta if future else reference - delta) + + +datefmt_re = re.compile(r'(\.[0-9]{3})[0-9]*(\+00:00)?') + + +def to_str(v): + if isinstance(v, bool): + return {True: 'true', False: 'false'}[v] + elif isinstance(v, list): + return ','.join(to_str(e) for e in v) + elif v is None: + return 'null' + else: + return str(v) + + +def stringDate(date): + # Convert to isoFormat + try: + string = date.isoformat(timespec='microseconds') + # py2.7 to py3.5 does not have timespec + except TypeError as e: + string = date.isoformat() + if string.find('.') == -1: + string += '.000' + string = datefmt_re.sub(r'\1Z', string) + return string + + +# the base class for strings, regardless of python version +try: + string = basestring +except NameError: + string = str diff --git a/third_party/python/json-e/jsone/six.py b/third_party/python/json-e/jsone/six.py new file mode 100644 index 0000000000..1ab9cd2d7d --- /dev/null +++ b/third_party/python/json-e/jsone/six.py @@ -0,0 +1,23 @@ +import sys +import operator + +# https://github.com/benjaminp/six/blob/2c3492a9f16d294cd5e6b43d6110c5a3a2e58b4c/six.py#L818 + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(meta): + + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +# https://github.com/benjaminp/six/blob/2c3492a9f16d294cd5e6b43d6110c5a3a2e58b4c/six.py#L578 +if sys.version_info[0] == 3: + viewitems = operator.methodcaller("items") +else: + viewitems = operator.methodcaller("viewitems") diff --git a/third_party/python/json-e/package.json b/third_party/python/json-e/package.json new file mode 100644 index 0000000000..0c388d57db --- /dev/null +++ b/third_party/python/json-e/package.json @@ -0,0 +1,35 @@ +{ + "name": "json-e", + "version": "2.7.0", + "description": "json parameterization module inspired from json-parameterization", + "main": "./src/index.js", + "scripts": { + "lint": "eslint src/*.js test/*.js", + "test": "yarn lint && mocha test/*_test.js", + "build-demo": "cd demo && yarn && yarn build", + "start-demo": "cd demo && yarn && yarn start" + }, + "files": [ + "src" + ], + "repository": { + "type": "git", + "url": "https://github.com/taskcluster/json-e" + }, + "author": "", + "license": "MPL-2.0", + "dependencies": { + "json-stable-stringify": "^1.0.1" + }, + "devDependencies": { + "assume": "^1.5.2", + "browserify": "^14.5.0", + "eslint-config-taskcluster": "^3.0.0", + "mocha": "^4.0.1", + "source-map-support": "^0.5.0", + "timekeeper": "^2.0.0" + }, + "engines": { + "node": ">=6.4.0" + } +} diff --git a/third_party/python/json-e/setup.cfg b/third_party/python/json-e/setup.cfg new file mode 100644 index 0000000000..6410597b69 --- /dev/null +++ b/third_party/python/json-e/setup.cfg @@ -0,0 +1,8 @@ +[pep8] +max-line-length = 100 +select = E,W + +[egg_info] +tag_build = +tag_date = 0 + diff --git a/third_party/python/json-e/setup.py b/third_party/python/json-e/setup.py new file mode 100644 index 0000000000..76299df9be --- /dev/null +++ b/third_party/python/json-e/setup.py @@ -0,0 +1,31 @@ +import json +import os +from setuptools import setup, find_packages + +package_json = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'package.json') +with open(package_json) as f: + version = json.load(f)['version'] + +setup(name='json-e', + version=version, + description='A data-structure parameterization system written for embedding context in JSON objects', + author='Dustin J. Mitchell', + url='https://taskcluster.github.io/json-e/', + author_email='dustin@mozilla.com', + packages=['jsone'], + test_suite='nose.collector', + license='MPL2', + extras_require={ + 'release': [ + 'towncrier', + ], + }, + tests_require=[ + "freezegun", + "hypothesis", + "nose", + "PyYAML", + "python-dateutil", + 'pep8', + ] +) |