The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .gitignore
├── .gitmodules
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── NEWS
├── PATENTS
├── README.md
├── alpha
    └── ruby
    │   ├── Gemfile
    │   ├── LICENSE
    │   ├── README.md
    │   ├── Rakefile
    │   ├── examples
    │       └── plan_out
    │       │   └── voting_experiment.rb
    │   ├── lib
    │       ├── plan_out.rb
    │       └── plan_out
    │       │   ├── assignment.rb
    │       │   ├── experiment.rb
    │       │   ├── op_random.rb
    │       │   ├── operator.rb
    │       │   ├── simple_experiment.rb
    │       │   └── version.rb
    │   ├── planout.gemspec
    │   └── test
    │       ├── plan_out
    │           ├── assignment_test.rb
    │           ├── experiment_test.rb
    │           └── operator_test.rb
    │       └── test_helper.rb
├── compiler
    ├── planout.jison
    ├── planout.js
    └── readme.md
├── contrib
    ├── postgres_logger.py
    └── pydata14_tutorial
    │   ├── 0-getting-started.ipynb
    │   ├── 1-logging.ipynb
    │   ├── 2-interpreter.ipynb
    │   ├── 3-namespaces.ipynb
    │   └── README.md
├── demos
    ├── anchoring_demo.py
    ├── demo_experiments.py
    ├── demo_namespaces.py
    ├── interpreter_experiment_examples.py
    ├── sample_scripts
    │   ├── exp1.json
    │   ├── exp1.planout
    │   ├── exp2.json
    │   ├── exp2.planout
    │   ├── exp3.json
    │   ├── exp3.planout
    │   ├── exp4.json
    │   └── exp4.planout
    └── simple_experiment_examples.py
├── planout-editor
    ├── .gitignore
    ├── Procfile
    ├── README.md
    ├── js
    │   ├── actions
    │   │   ├── PlanOutExperimentActions.js
    │   │   └── PlanOutTesterActions.js
    │   ├── app.js
    │   ├── components
    │   │   ├── PlanOutEditorButtons.react.js
    │   │   ├── PlanOutScriptPanel.react.js
    │   │   ├── PlanOutTesterBox.react.js
    │   │   ├── PlanOutTesterBoxForm.react.js
    │   │   ├── PlanOutTesterBoxFormInput.react.js
    │   │   ├── PlanOutTesterBoxOutput.react.js
    │   │   └── PlanOutTesterPanel.react.js
    │   ├── constants
    │   │   └── PlanOutEditorConstants.js
    │   ├── dispatcher
    │   │   └── PlanOutEditorDispatcher.js
    │   ├── stores
    │   │   ├── PlanOutExperimentStore.js
    │   │   └── PlanOutTesterStore.js
    │   └── utils
    │   │   ├── DemoData.js
    │   │   ├── FileSaver.js
    │   │   ├── PlanOutAsyncRequests.js
    │   │   ├── PlanOutStaticAnalyzer.js
    │   │   └── planout_compiler.js
    ├── package.json
    ├── planout-editor-kernel.py
    ├── requirements.txt
    ├── static
    │   ├── bootstrap-theme.min.css
    │   ├── bootstrap.min.css
    │   ├── codemirror.css
    │   ├── fonts
    │   │   ├── glyphicons-halflings-regular.eot
    │   │   ├── glyphicons-halflings-regular.svg
    │   │   ├── glyphicons-halflings-regular.ttf
    │   │   └── glyphicons-halflings-regular.woff
    │   ├── js
    │   │   ├── JSXTransformer-0.11.0.js
    │   │   ├── bootstrap.js
    │   │   ├── bootstrap.min.js
    │   │   ├── bundle.js
    │   │   ├── jquery-1.11.1.min.js
    │   │   ├── jquery-1.11.1.min.map
    │   │   ├── lib
    │   │   │   └── codemirror.js
    │   │   ├── mode
    │   │   │   ├── javascript
    │   │   │   │   ├── index.html
    │   │   │   │   ├── javascript.js
    │   │   │   │   ├── json-ld.html
    │   │   │   │   ├── test.js
    │   │   │   │   └── typescript.html
    │   │   │   └── planout
    │   │   │   │   ├── index.html
    │   │   │   │   ├── json-ld.html
    │   │   │   │   ├── planout.js
    │   │   │   │   ├── test.js
    │   │   │   │   └── typescript.html
    │   │   ├── react-0.11.0.js
    │   │   └── react-bootstrap.min.js
    │   └── planoutstyle.css
    └── templates
    │   ├── index.html
    │   └── layout.html
└── python
    ├── docs
        ├── 00-getting-started.md
        ├── 01-why-planout.md
        ├── 02-operators.md
        ├── 03-how-planout-works.md
        ├── 04-logging.md
        ├── 04.1-extending-logging.md
        ├── 05-namespaces.md
        ├── 05.1-simple-namespaces.md
        ├── 06-best-practices.md
        ├── 07-language.md
        └── 08-about-planout.md
    ├── planout
        ├── __init__.py
        ├── assignment.py
        ├── experiment.py
        ├── interpreter.py
        ├── namespace.py
        ├── ops
        │   ├── __init__.py
        │   ├── base.py
        │   ├── core.py
        │   ├── random.py
        │   └── utils.py
        └── test
        │   ├── __init__.py
        │   ├── test_assignment.py
        │   ├── test_core_ops.py
        │   ├── test_experiment.py
        │   ├── test_interpreter.py
        │   ├── test_namespace.py
        │   └── test_random_ops.py
    └── setup.py


/.gitignore:
--------------------------------------------------------------------------------
 1 | *.pyc
 2 | *.DS_store
 3 | *.log
 4 | *.swp
 5 | build/*
 6 | python/build/*
 7 | _site/*
 8 | *node_modules/*
 9 | *.tar.gz
10 | .ipynb_checkpoint*
11 | Gemfile.lock
12 | MANIFEST
13 | 


--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
 1 | [submodule "java"]
 2 | 	path = java
 3 | 	url = https://github.com/Glassdoor/planout4j
 4 | [submodule "alpha/js"]
 5 | 	path = js
 6 | 	url = https://github.com/HubSpot/PlanOut.js
 7 | [submodule "php"]
 8 | 	path = php
 9 | 	url = https://github.com/vimeo/ABLincoln
10 | [submodule "alpha/julia"]
11 | 	path = alpha/julia
12 | 	url = https://github.com/rawls238/PlanOut.jl
13 | [submodule "alpha/golang"]
14 | 	path = alpha/golang
15 | 	url = https://github.com/biased-unit/planout-golang
16 | 


--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
 1 | language: python
 2 | python:
 3 |   - 2.7
 4 |   - 3.4
 5 |   - 3.5
 6 |   - 3.6
 7 |   - nightly
 8 | 
 9 | install:
10 |   - pip install -e python
11 | 
12 | script: 
13 |   - python -m unittest discover python/planout/test
14 | 


--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.fb.com/codeofconduct) so that you can understand what actions will and will not be tolerated.


--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | # Contributing to PlanOut
 2 | We want to make contributing to this project as easy and transparent as
 3 | possible.  If you identify and bugs, have improvements to the code or documentation,
 4 | we will be happy to review your code and integrate the changes into the main
 5 | branch.
 6 | 
 7 | If you are planning on porting PlanOut to other languages or platforms,
 8 | please let us know! We are happy to review ports in detail, and either post
 9 | them on our own repository, or link to your own repository as a submodule.
10 | 
11 | ## Code of Conduct
12 | The code of conduct is described in [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md).
13 | 
14 | ## Our Development Process
15 | The Python reference implementation mirrors the functionality of PlanOut and
16 | core parts of Facebook's experimentation stack, which is written in Hack (we 
17 | hope to open source parts of this implementation in the near future). As
18 | we improve PlanOut internally, we will continue to maintain the Python
19 | implementation as to maintain feature parity.
20 | 
21 | ## Pull Requests
22 | We actively welcome your pull requests.
23 | 1. Fork the repo and create your branch from `master`.
24 | 2. If you've added code that should be tested (most code should be!), add unit tests.
25 | 3. If you've changed APIs, update the documentation.
26 | 4. Ensure the test suite passes.
27 | 5. Make sure your code lints.
28 | 6. If you haven't already, complete the Contributor License Agreement ("CLA").
29 | 
30 | ## Contributor License Agreement ("CLA")
31 | In order to accept your pull request, we need you to submit a CLA. You only need
32 | to do this once to work on any of Facebook's open source projects.
33 | 
34 | Complete your CLA here: <https://developers.facebook.com/opensource/cla>
35 | 
36 | ## Issues  
37 | We use GitHub issues to track public bugs. Please ensure your description is
38 | clear and has sufficient instructions to be able to reproduce the issue.
39 | 
40 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe
41 | disclosure of security bugs. In those cases, please go through the process
42 | outlined on that page and do not file a public issue.
43 | 
44 | ## Coding Style  
45 | * 80 character line length
46 | * PEP-8 (if contributing Python code)
47 | 
48 | 
49 | ## License
50 | By contributing to PlanOut, you agree that your contributions will be licensed
51 | under its BSD license.
52 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | BSD License
 2 | 
 3 | For PlanOut software
 4 | 
 5 | Copyright (c) 2014, Facebook, Inc. All rights reserved.
 6 | 
 7 | Redistribution and use in source and binary forms, with or without modification,
 8 | are permitted provided that the following conditions are met:
 9 | 
10 |  * Redistributions of source code must retain the above copyright notice, this
11 |    list of conditions and the following disclaimer.
12 | 
13 |  * Redistributions in binary form must reproduce the above copyright notice,
14 |    this list of conditions and the following disclaimer in the documentation
15 |    and/or other materials provided with the distribution.
16 | 
17 |  * Neither the name Facebook nor the names of its contributors may be used to
18 |    endorse or promote products derived from this software without specific
19 |    prior written permission.
20 | 
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 | 


--------------------------------------------------------------------------------
/NEWS:
--------------------------------------------------------------------------------
 1 | * Noteworthy changes in release 0.6.0
 2 | - Python 3 compatibility
 3 | - Language-level control of logging: The return value of assign now determines
 4 |   whether or not an exposure event will be automatically logged. If either a
 5 |   truth-y value or nothing is returned from assign, then an exposure will be
 6 |   logged. If a false-y value (excluding None) is returned then exposure will not
 7 |   be logged.
 8 | - SimpleInterpretedExperiment has been updated to take utilize the return op
 9 | - Ability to register custom operators with the PlanOut interpreter.  This
10 |   allows developers to more easily integrate PlanOut with other aspects of their
11 |   production stack, including gating / eligibility checks.
12 | - Fast sampling: There is now an alternative Sample Op called FastSample that is
13 |   more efficient than the existing Sample Op especially when sampling k elements
14 |   from a population of n when k << n. If you want to enable this op to speed up
15 |   namespace segment allocation then when instantiating your Namespace you can
16 |   use the optional keyword argument, use_fast_sample=True in the Namespace
17 |   constructor.  Thanks to Guy Aridor for this excellent speed up!
18 | - ProductionExperiment class which enforces runtime checks on what PlanOut
19 |   variables are available.  If undefined variables are requested, exposure log
20 |   events do not occur.
21 | 
22 | * Noteworthy changes in release 0.5 (2014-11-23)
23 | - A SimpleInterpretedExperiment to make it easier to get up and running with
24 |   the PlanOut language and interpreter.
25 | 
26 | - PlanOut language:
27 |   - Standardized handling of undefined keys: indexing into a list or dictionary
28 |     for an invalid index or key always yields null; nulls may be coalesced via
29 |     the null coalescing operator, coalesce()
30 |   - A return() operator for determining whether the input units are
31 |     "in the experiment" (and therefore logged). Calling "return;" or
32 |     "return <value with positive truthiness>" sets in_experiment to True.
33 |     Returning a value with negative truthiness sets in_experiment to False.
34 | 
35 | - PlanOut interpreter:
36 |   - Convenience methods for extracting and checking typed operator arguments,
37 |     e.g., self.getArgInt('x') will retreive an argument named 'x'. If 'x' is
38 |     not given, or 'x' is not an integer, an error will be thrown. This improves
39 |     ease of debugging, and also provides a clear path for those who are
40 |     implementing the interpreter in strongly typed language.
41 |   - Improved argument checking for built-in operators.
42 | 
43 | - PlanOut editor: the PlanOut editor is a reactive development environment and
44 | testing system for PlanOut-language experiments.
45 | 
46 | - Breaking changes:
47 |   - Namespaces: when a unit is assigned to an experiment and in_experiment is
48 |     set to False, the default experiment is executed.
49 |   - PlanOut interpreter:
50 |     - The length() operator's argument is now 'value', as to allow for unnamed
51 |       arguments to be passed in.
52 |     - Min and max are now required arguments for randomInteger and randomFloat
53 |     - Deprecated validate() and options() methods
54 |     - Deprecated self.parameters for operators: self.args refers to arguments
55 |       passed into an operator. If an object is a subclass of SimpleOp, then
56 |       the arguments are pre-evaluated (and act just like self.parameters).
57 | 
58 | 
59 | * Noteworthy changes in release 0.4 (2014-10-29)
60 | - Full support for parameter overrides, which allow for testing of PlanOut
61 |   experiments
62 | - Alpha version of PlanOut editor
63 | - Bugfixes to how the PlanOut compiler handles indexing into arrays multiple
64 | times (e.g., a['b'][1][0])
65 | 
66 | 
67 | * Noteworthy changes in release 0.3 (2014-06-18) [alpha]
68 | - IMPORTANT UPDATE: The hash function used in prior versions did not match the expected behavior in the documentation (caught by @akalicki). We have updated PlanOut's hashing function, which will likely cause units to be randomly assigned to different parameters. If you are already using PlanOut in a production environment, please upgrade with care. If you are not currently using PlanOut in a production environment, please upgrade immediately:
69 | 
70 |   pip install -U planout
71 | 
72 | - Other updates:
73 |   -The assign() method is now only called once. Thanks to @akalicki for catching this.
74 | 


--------------------------------------------------------------------------------
/PATENTS:
--------------------------------------------------------------------------------
 1 | Additional Grant of Patent Rights
 2 | 
 3 | "Software" means the PlanOut software distributed by Facebook, Inc.
 4 | 
 5 | Facebook hereby grants you a perpetual, worldwide, royalty-free, non-exclusive,
 6 | irrevocable (subject to the termination provision below) license under any
 7 | rights in any patent claims owned by Facebook, to make, have made, use, sell,
 8 | offer to sell, import, and otherwise transfer the Software. For avoidance of
 9 | doubt, no license is granted under Facebook’s rights in any patent claims that
10 | are infringed by (i) modifications to the Software made by you or a third party,
11 | or (ii) the Software in combination with any software or other technology
12 | provided by you or a third party.
13 | 
14 | The license granted hereunder will terminate, automatically and without notice,
15 | for anyone that makes any claim (including by filing any lawsuit, assertion or
16 | other action) alleging (a) direct, indirect, or contributory infringement or
17 | inducement to infringe any patent: (i) by Facebook or any of its subsidiaries or
18 | affiliates, whether or not such claim is related to the Software, (ii) by any
19 | party if such claim arises in whole or in part from any software, product or
20 | service of Facebook or any of its subsidiaries or affiliates, whether or not
21 | such claim is related to the Software, or (iii) by any party relating to the
22 | Software; or (b) that any right in any patent claim of Facebook is invalid or
23 | unenforceable.
24 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # PlanOut
  2 | 
  3 | [![Build Status](https://img.shields.io/pypi/v/planout.svg)](https://pypi.org/project/planout/)
  4 | [![Build Status](https://img.shields.io/pypi/pyversions/planout.svg)](https://pypi.org/project/planout/)
  5 | [![Build Status](https://travis-ci.com/facebook/planout.svg?branch=master)](https://travis-ci.com/facebook/planout)
  6 | 
  7 | PlanOut is a multi-platform framework and programming language for online field experimentation. PlanOut was created to make it easy to run and iterate on sophisticated experiments, while satisfying the constraints of deployed Internet services with many users.
  8 | 
  9 | Developers integrate PlanOut by defining experiments that detail how _units_ (e.g., users, cookie IDs) should get mapped onto conditions. For example, to create a 2x2 experiment randomizing both the color and the text on a button, you create a class like this in Python:
 10 | 
 11 | ```python
 12 | class MyExperiment(SimpleExperiment):
 13 |   def assign(self, params, userid):
 14 |     params.button_color = UniformChoice(choices=['#ff0000', '#00ff00'], unit=userid)
 15 |     params.button_text = UniformChoice(choices=['I voted', 'I am a voter'], unit=userid)
 16 | ```
 17 | 
 18 | Then, in the application code, you query the Experiment object to find out what values the current user should be mapped onto:
 19 | ```python
 20 | my_exp = MyExperiment(userid=101)
 21 | color = my_exp.get('button_color')
 22 | text = my_exp.get('button_text')
 23 | ```
 24 | 
 25 | PlanOut takes care of randomizing each ``userid`` into the right bucket. It does so by hashing the input, so each ``userid`` will always map onto the same values for that experiment.
 26 | 
 27 | ### What does the PlanOut distribution include?
 28 | 
 29 | The reference implementation for PlanOut is written in Python.  It includes:
 30 |   * Extensible classes for defining experiments. These classes make it easy to implement reliable, deterministic random assignment procedures, and automatically log key data.
 31 |   * A basic implementation of namespaces, which can be used to manage multiple mutually exclusive experiments.
 32 |   * The PlanOut interpreter, which executes serialized code generated by the PlanOut domain specific language.
 33 |   * An interactive Web-based editor and compiler for developing and testing
 34 |   PlanOut-language scripts.
 35 | 
 36 | Other production-ready versions of PlanOut are available for Java, JavaScript, and PHP, and can found in the `java/`, `js/`, and `php/` directories, respectively.
 37 | 
 38 | The `alpha/` directory contains implementations of PlanOut to other languages that are currently under development, including Go, Julia, and Ruby.
 39 | 
 40 | ### Who is PlanOut for?
 41 | PlanOut designed for researchers, students, and small businesses wanting to run experiments. It is built to be extensible, so that it may be adapted for use with large production environments.  The implementation here mirrors many of the key components of Facebook's Hack-based implementation of PlanOut which is used to conduct experiments with hundreds of millions of users.
 42 | 
 43 | ### Full Example
 44 | 
 45 | To create a basic PlanOut experiment in Python, you subclass ``SimpleExperiment`` object, and implement an assignment method. You can use PlanOut's random assignment operators by setting ``e.varname``, where ``params`` is the first argument passed to the ``assign()`` method, and ``varname`` is the name of the variable you are setting.
 46 | ```python
 47 | 
 48 | from planout.experiment import SimpleExperiment
 49 | from planout.ops.random import *
 50 | 
 51 | class FirstExperiment(SimpleExperiment):
 52 |   def assign(self, params, userid):
 53 |     params.button_color = UniformChoice(choices=['#ff0000', '#00ff00'], unit=userid)
 54 |     params.button_text = WeightedChoice(
 55 |         choices=['Join now!', 'Sign up.'],
 56 |         weights=[0.3, 0.7], unit=userid)
 57 | 
 58 | my_exp = FirstExperiment(userid=12)
 59 | # parameters may be accessed via the . operator
 60 | print my_exp.get('button_text'), my_exp.get('button_color')
 61 | 
 62 | # experiment objects include all input data
 63 | for i in xrange(6):
 64 |   print FirstExperiment(userid=i)
 65 | ```
 66 | 
 67 | which outputs:
 68 | ```
 69 | Join now! #ff0000
 70 | {'inputs': {'userid': 0}, 'checksum': '22c13b16', 'salt': 'FirstExperiment', 'name': 'FirstExperiment', 'params': {'button_color': '#ff0000', 'button_text': 'Sign up.'}}
 71 | {'inputs': {'userid': 1}, 'checksum': '22c13b16', 'salt': 'FirstExperiment', 'name': 'FirstExperiment', 'params': {'button_color': '#ff0000', 'button_text': 'Sign up.'}}
 72 | {'inputs': {'userid': 2}, 'checksum': '22c13b16', 'salt': 'FirstExperiment', 'name': 'FirstExperiment', 'params': {'button_color': '#00ff00', 'button_text': 'Sign up.'}}
 73 | {'inputs': {'userid': 3}, 'checksum': '22c13b16', 'salt': 'FirstExperiment', 'name': 'FirstExperiment', 'params': {'button_color': '#ff0000', 'button_text': 'Sign up.'}}
 74 | {'inputs': {'userid': 4}, 'checksum': '22c13b16', 'salt': 'FirstExperiment', 'name': 'FirstExperiment', 'params': {'button_color': '#00ff00', 'button_text': 'Join now!'}}
 75 | {'inputs': {'userid': 5}, 'checksum': '22c13b16', 'salt': 'FirstExperiment', 'name': 'FirstExperiment', 'params': {'button_color': '#00ff00', 'button_text': 'Sign up.'}}
 76 | ```
 77 | 
 78 | The ``SimpleExperiment`` class will automatically concatenate the name of the experiment, ``FirstExperiment``, the variable name, and the input data (``userid``) and hash that string to perform the random assignment. Parameter assignments and inputs are automatically logged into a file called ``firstexperiment.log'``.
 79 | 
 80 | 
 81 | ### Installation
 82 | You can immediately install the reference implementation of PlanOut for Python using `pip` with:
 83 | ```
 84 | pip install planout
 85 | ```
 86 | 
 87 | See the `java/`, `php/`, `js/`, `alpha/golang`, `alpha/ruby`, `alpha/julia` directories for instructions on installing PlanOut for other languages.
 88 | 
 89 | ### Learn more
 90 | Learn more about PlanOut visiting the [PlanOut website](http://facebook.github.io/planout/) or by [reading the PlanOut paper](http://arxiv.org/pdf/1409.3174v1.pdf). You can cite PlanOut as "Designing and Deploying Online Field Experiments". Eytan Bakshy, Dean Eckles, Michael S. Bernstein. Proceedings of the 23rd ACM conference on the World Wide Web. April 7–11, 2014, Seoul, Korea, or by copying and pasting the bibtex below:
 91 | ``` bibtex
 92 | @inproceedings{bakshy2014www,
 93 | 	Author = {Bakshy, E. and Eckles, D. and Bernstein, M.S.},
 94 | 	Booktitle = {Proceedings of the 23rd ACM conference on the World Wide Web},
 95 | 	Organization = {ACM},
 96 | 	Title = {Designing and Deploying Online Field Experiments},
 97 | 	Year = {2014}
 98 | }
 99 | ```
100 | 


--------------------------------------------------------------------------------
/alpha/ruby/Gemfile:
--------------------------------------------------------------------------------
 1 | source 'https://rubygems.org'
 2 | 
 3 | # Specify your gem's dependencies in planout.gemspec
 4 | gemspec
 5 | 
 6 | group :test do
 7 |   gem 'rake'
 8 |   gem 'minitest', '~> 5.5'
 9 | end
10 | 


--------------------------------------------------------------------------------
/alpha/ruby/LICENSE:
--------------------------------------------------------------------------------
 1 | BSD License
 2 | 
 3 | For PlanOut software
 4 | 
 5 | Copyright (c) 2014, Facebook, Inc. All rights reserved.
 6 | 
 7 | Redistribution and use in source and binary forms, with or without modification,
 8 | are permitted provided that the following conditions are met:
 9 | 
10 |  * Redistributions of source code must retain the above copyright notice, this
11 |    list of conditions and the following disclaimer.
12 | 
13 |  * Redistributions in binary form must reproduce the above copyright notice,
14 |    this list of conditions and the following disclaimer in the documentation
15 |    and/or other materials provided with the distribution.
16 | 
17 |  * Neither the name Facebook nor the names of its contributors may be used to
18 |    endorse or promote products derived from this software without specific
19 |    prior written permission.
20 | 
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 | 


--------------------------------------------------------------------------------
/alpha/ruby/README.md:
--------------------------------------------------------------------------------
 1 | # PlanOut
 2 | [![Gem Version](https://badge.fury.io/rb/planout.svg)](http://badge.fury.io/rb/planout)
 3 | [![Build Status](https://travis-ci.org/facebook/planout.svg)](https://travis-ci.org/facebook/planout)
 4 | 
 5 | ## Overview
 6 | This is a rough implementation of the Experiment / logging infrastructure for running PlanOut experiments, with all the random assignment operators available in Python. This port is nearly a line-by-line port, and produces assignments that are completely consistent with those produced by the Python reference implementation.
 7 | 
 8 | ## Installation
 9 | Add this line to your application's Gemfile:
10 | 
11 | ```ruby
12 | gem 'planout'
13 | ```
14 | 
15 | And then execute:
16 | ```bash
17 | $ bundle
18 | ```
19 | 
20 | Or install it yourself as:
21 | ```bash
22 | $ gem install planout
23 | ```
24 | 
25 | ## How it works
26 | 
27 | This defines a simple experiment that randomly assigns three variables, foo, bar, and baz.
28 | `foo` and `baz` use `userid` as input, while `bar` uses a pair, namely `userid` combined with the value of `foo` from the prior step.
29 | 
30 | ```ruby
31 | module PlanOut
32 |   class VotingExperiment < SimpleExperiment
33 |     # Experiment#assign takes params and an input array
34 |     def assign(params, **inputs)
35 |       userid = inputs[:userid]
36 | 
37 |       params[:button_color] = UniformChoice.new({
38 |         choices: ['ff0000', '#00ff00'],
39 |         unit: userid
40 |       })
41 | 
42 |       params[:button_text] = UniformChoice.new({
43 |         choices: ["I'm voting", "I'm a voter"],
44 |         unit: userid,
45 |         salt:'x'
46 |       })
47 |     end
48 |   end
49 | end
50 | ```
51 | 
52 | Then, we can examine the assignments produced for a few input userids. Note that since exposure logging is enabled by default, all of the experiments' inputs, configuration information, timestamp, and parameter assignments are pooped out via the Logger class.
53 | 
54 | ```ruby
55 | my_exp = PlanOut::VotingExperiment.new(userid: 14)
56 | my_button_color = my_exp.get(:button_color)
57 | button_text = my_exp.get(:button_text)
58 | puts "button color is #{my_button_color} and button text is #{button_text}."
59 | ```
60 | 
61 | The output of the Ruby script looks something like this:
62 | 
63 | ```ruby
64 | logged data: {"name":"PlanOut::VotingExperiment","time":1404944726,"salt":"PlanOut::VotingExperiment","inputs":{"userid":14},"params":{"button_color":"ff0000","button_text":"I'm a voter"},"event":"exposure"}
65 | 
66 | button color is ff0000 and button text is I'm a voter.
67 | ```
68 | ## Examples
69 | 
70 | For examples please refer to the `examples` directory.
71 | 
72 | ## Running the tests
73 | Make sure you're in the ruby implementation directory of PlanOut and run
74 | 
75 | `rake` or `rake test`
76 | 
77 | to run the entire test suite.
78 | 
79 | If you wish to run a specific test, run
80 | 
81 | `rake test TEST=test/testname.rb` or even better `ruby test/testname.rb`
82 | 


--------------------------------------------------------------------------------
/alpha/ruby/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rake/testtask'
2 | 
3 | Rake::TestTask.new do |t|
4 |   t.libs << "test"
5 |   t.test_files = FileList['test/**/*_test.rb']
6 | end
7 | 
8 | task default: :test
9 | 


--------------------------------------------------------------------------------
/alpha/ruby/examples/plan_out/voting_experiment.rb:
--------------------------------------------------------------------------------
 1 | require_relative '../../lib/plan_out/simple_experiment'
 2 | 
 3 | module PlanOut
 4 |   class VotingExperiment < SimpleExperiment
 5 |     def setup; end
 6 | 
 7 |     def assign(params, **inputs)
 8 |       userid = inputs[:userid]
 9 |       params[:button_color] = UniformChoice.new({
10 |         choices: ['ff0000', '00ff00'],
11 |         unit: userid
12 |       })
13 | 
14 |       params[:button_text] = UniformChoice.new({
15 |         choices: ["I'm voting", "I'm a voter"],
16 |         unit: userid,
17 |         salt:'x'
18 |       })
19 |     end
20 |   end
21 | 
22 |   if __FILE__ == $0
23 |     (14..16).each do |i|
24 |       my_exp = VotingExperiment.new(userid:i)
25 |       # toggling the above disables or re-enables auto-logging
26 |       #my_exp.auto_exposure_log = false
27 |       puts "\ngetting assignment for user #{i} note: first time triggers a log event"
28 |       puts "button color is #{my_exp.get(:button_color)} and button text is #{my_exp.get(:button_text)}"
29 |     end
30 |   end
31 | end
32 | 


--------------------------------------------------------------------------------
/alpha/ruby/lib/plan_out.rb:
--------------------------------------------------------------------------------
1 | require_relative 'plan_out/op_random'
2 | require_relative 'plan_out/assignment'
3 | require_relative 'plan_out/simple_experiment'
4 | require_relative 'plan_out/version'
5 | 
6 | module PlanOut
7 | 
8 | end
9 | 


--------------------------------------------------------------------------------
/alpha/ruby/lib/plan_out/assignment.rb:
--------------------------------------------------------------------------------
 1 | module PlanOut
 2 |   class Assignment
 3 |     attr_accessor :experiment_salt, :data
 4 | 
 5 |     def initialize(experiment_salt)
 6 |       @experiment_salt = experiment_salt
 7 |       @data = {}
 8 |     end
 9 | 
10 |     def evaluate(data)
11 |       data
12 |     end
13 | 
14 |     def get(var, default = nil)
15 |       @data[var.to_sym] || default
16 |     end
17 | 
18 |     # in python this would be defined as __setattr__ or __setitem__
19 |     # not sure how to do this in Ruby.
20 |     def set(name, value)
21 |       if value.is_a? Operator
22 |         value.args[:salt] = name if !value.args.has_key?(:salt)
23 |         @data[name.to_sym] = value.execute(self)
24 |       else
25 |         @data[name.to_sym] = value
26 |       end
27 |     end
28 | 
29 |     def [](x)
30 |       get(x)
31 |     end
32 | 
33 |     def []=(x,y)
34 |       set(x,y)
35 |     end
36 | 
37 |     def get_params
38 |       @data
39 |     end
40 |   end
41 | end
42 | 


--------------------------------------------------------------------------------
/alpha/ruby/lib/plan_out/experiment.rb:
--------------------------------------------------------------------------------
  1 | require 'logger'
  2 | require 'json'
  3 | 
  4 | module PlanOut
  5 |   class Experiment
  6 |     attr_accessor :auto_exposure_log
  7 | 
  8 |     def initialize(**inputs)
  9 |       @inputs = inputs
 10 |       @exposure_logged = false
 11 |       @_salt = nil
 12 |       @in_experiment = true
 13 |       @name = self.class.name
 14 |       @auto_exposure_log = true
 15 | 
 16 |       setup  # sets name, salt, etc.
 17 | 
 18 |       @assignment = Assignment.new(salt)
 19 |       @assigned = false
 20 | 
 21 |       @logger = nil
 22 |       setup
 23 |     end
 24 | 
 25 |     def _assign
 26 |       configure_logger
 27 |       assign(@assignment, **@inputs)
 28 |       @in_experiment = @assignment.get(:in_experiment, @in_experiment)
 29 |       @assigned = true
 30 |     end
 31 | 
 32 |     def setup
 33 |       nil
 34 |     end
 35 | 
 36 |     def salt=(value)
 37 |       @_salt = value
 38 |     end
 39 | 
 40 |     def salt
 41 |       @_salt || @name
 42 |     end
 43 | 
 44 |     def auto_exposure_log=(value)
 45 |       @auto_exposure_log = value
 46 |     end
 47 | 
 48 |     def configure_logger
 49 |       nil
 50 |     end
 51 | 
 52 |     def requires_assignment
 53 |       _assign if !@assigned
 54 |     end
 55 | 
 56 |     def is_logged?
 57 |       @logged
 58 |     end
 59 | 
 60 |     def requires_exposure_logging
 61 |       log_exposure if @auto_exposure_log && @in_experiment && !@exposure_logged
 62 |     end
 63 | 
 64 |     def get_params
 65 |       requires_assignment
 66 |       requires_exposure_logging
 67 |       @assignment.get_params
 68 |     end
 69 | 
 70 |     def get(name, default = nil)
 71 |       requires_assignment
 72 |       requires_exposure_logging
 73 |       @assignment.get(name, default)
 74 |     end
 75 | 
 76 |     def assign(params, *inputs)
 77 |       # up to child class to implement
 78 |       nil
 79 |     end
 80 | 
 81 |     def log_event(event_type, extras = nil)
 82 |       if extras.nil?
 83 |         extra_payload = {event: event_type}
 84 |       else
 85 |         extra_payload = {
 86 |           event: event_type,
 87 |           extra_data: extras.clone
 88 |         }
 89 |       end
 90 | 
 91 |       log(as_blob(extra_payload))
 92 |     end
 93 | 
 94 |     def log_exposure(extras = nil)
 95 |       @exposure_logged = true
 96 |       log_event(:exposure, extras)
 97 |     end
 98 | 
 99 |     def as_blob(extras = {})
100 |       d = {
101 |         name: @name,
102 |         time: Time.now.to_i,
103 |         salt: salt,
104 |         inputs: @inputs,
105 |         params: @assignment.data
106 |       }
107 | 
108 |       d.merge!(extras)
109 |     end
110 |   end
111 | 
112 | end
113 | 


--------------------------------------------------------------------------------
/alpha/ruby/lib/plan_out/op_random.rb:
--------------------------------------------------------------------------------
 1 | require_relative 'operator'
 2 | 
 3 | module PlanOut
 4 |   class OpRandom < OpSimple
 5 |     LONG_SCALE = Float(0xFFFFFFFFFFFFFFF)
 6 | 
 7 |     def get_unit(appended_unit = nil)
 8 |       unit = @parameters[:unit]
 9 |       unit = [unit] if !unit.is_a? Array
10 |       unit += appended_unit if appended_unit != nil
11 |       unit
12 |     end
13 | 
14 |     def get_hash(appended_unit = nil)
15 |       salt = @parameters[:salt]
16 |       salty = "#{@mapper.experiment_salt}.#{salt}"
17 |       unit_str = get_unit(appended_unit).join('.')
18 |       x = "#{salty}.#{unit_str}"
19 |       last_hex = (Digest::SHA1.hexdigest(x))[0..14]
20 |       last_hex.to_i(16)
21 |     end
22 | 
23 |     def get_uniform(min_val = 0.0, max_val = 1.0, appended_unit = nil)
24 |       zero_to_one = self.get_hash(appended_unit)/LONG_SCALE
25 |       min_val + (max_val-min_val) * zero_to_one
26 |     end
27 |   end
28 | 
29 |   class RandomFloat < OpRandom
30 |     def simple_execute
31 |       min_val = @parameters.fetch(:min, 0)
32 |       max_val = @parameters.fetch(:max, 1)
33 |       get_uniform(min_val, max_val)
34 |     end
35 |   end
36 | 
37 |   class RandomInteger < OpRandom
38 |     def simple_execute
39 |       min_val = @parameters.fetch(:min, 0)
40 |       max_val = @parameters.fetch(:max, 1)
41 |       min_val + get_hash() % (max_val - min_val + 1)
42 |     end
43 |   end
44 | 
45 |   class BernoulliTrial < OpRandom
46 |     def simple_execute
47 |       p = @parameters[:p]
48 |       rand_val = get_uniform(0.0, 1.0)
49 |       (rand_val <= p) ? 1 : 0
50 |     end
51 |   end
52 | 
53 |   class WeightedChoice < OpRandom
54 |     def simple_execute
55 |       choices = @parameters[:choices]
56 |       weights = @parameters[:weights]
57 | 
58 |       return [] if choices.length() == 0
59 | 
60 |       cum_weights = Array.new(weights.length)
61 |       cum_sum = 0.0
62 | 
63 |       weights.each_with_index do |weight, index|
64 |         cum_sum += weight
65 |         cum_weights[index] = cum_sum
66 |       end
67 | 
68 |       stop_value = get_uniform(0.0, cum_sum)
69 | 
70 |       i = 0
71 |       cum_weights.each_with_index do |cum_weight, index|
72 |         return choices[index] if stop_value <= cum_weight
73 |       end
74 |     end
75 |   end
76 | 
77 |   class UniformChoice < OpRandom
78 |     def simple_execute
79 |       choices = @parameters[:choices]
80 |       return [] if choices.length() == 0
81 |       rand_index = get_hash() % choices.length()
82 |       choices[rand_index]
83 |     end
84 |   end
85 | end
86 | 


--------------------------------------------------------------------------------
/alpha/ruby/lib/plan_out/operator.rb:
--------------------------------------------------------------------------------
 1 | require 'digest/sha1'
 2 | 
 3 | module PlanOut
 4 |   class Operator
 5 |     attr_accessor :args
 6 | 
 7 |     def initialize(parameters)
 8 |       @args = parameters
 9 |     end
10 | 
11 |     def execute(mapper)
12 |       mapper.experiment_salt
13 |     end
14 |   end
15 | 
16 |   class OpSimple < Operator
17 |     def execute(mapper)
18 |       @mapper = mapper
19 |       @parameters = {}
20 | 
21 |       @args.each do |key, value|
22 |         @parameters[key] = mapper.evaluate(value)
23 |       end
24 | 
25 |       simple_execute
26 |     end
27 | 
28 |     def simple_execute
29 |       -1
30 |     end
31 |   end
32 | end
33 | 


--------------------------------------------------------------------------------
/alpha/ruby/lib/plan_out/simple_experiment.rb:
--------------------------------------------------------------------------------
 1 | require_relative 'experiment'
 2 | 
 3 | module PlanOut
 4 |   class SimpleExperiment < Experiment
 5 |     def configure_logger
 6 |       @logger = Logger.new(STDOUT)
 7 |       #@loger.level = Logger::WARN
 8 |       @logger.formatter = proc do |severity, datetime, progname, msg|
 9 |         "logged data: #{msg}\n"
10 |       end
11 |     end
12 | 
13 |     def log(data)
14 |       @logger.info(JSON.dump(data))
15 |     end
16 |   end
17 | end
18 | 


--------------------------------------------------------------------------------
/alpha/ruby/lib/plan_out/version.rb:
--------------------------------------------------------------------------------
1 | module PlanOut
2 |   VERSION = '0.1.2'
3 | end
4 | 


--------------------------------------------------------------------------------
/alpha/ruby/planout.gemspec:
--------------------------------------------------------------------------------
 1 | # coding: utf-8
 2 | lib = File.expand_path('../lib', __FILE__)
 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
 4 | require 'plan_out/version'
 5 | 
 6 | Gem::Specification.new do |spec|
 7 |   spec.name          = "planout"
 8 |   spec.version       = PlanOut::VERSION
 9 |   spec.authors       = ["Eytan Bakshy", "Mohnish Thallavajhula"]
10 |   spec.email         = ["ebakshy@gmail.com", "i@mohni.sh"]
11 |   spec.summary       = %q{PlanOut is a framework and programming language for online field experimentation.}
12 |   spec.description   = %q{PlanOut is a framework and programming language for online field experimentation. PlanOut was created to make it easy to run and iterate on sophisticated experiments, while satisfying the constraints of deployed Internet services with many users.}
13 |   spec.homepage      = "https://facebook.github.io/planout"
14 |   spec.license       = "BSD"
15 | 
16 |   spec.files         = `git ls-files -z`.split("\x0")
17 |   spec.executables   = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18 |   spec.test_files    = spec.files.grep(%r{^(test|spec|features)/})
19 |   spec.require_paths = ["lib"]
20 | 
21 |   spec.add_development_dependency "bundler", "~> 1.7"
22 |   spec.add_development_dependency "minitest", "~> 5.5"
23 |   spec.add_development_dependency "rake", "~> 10.0"
24 | end
25 | 


--------------------------------------------------------------------------------
/alpha/ruby/test/plan_out/assignment_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative '../test_helper'
 2 | 
 3 | module PlanOut
 4 |   class AssignmentTest < Minitest::Test
 5 |     def setup
 6 |       @assignment = Assignment.new('mtsalt')
 7 |     end
 8 | 
 9 |     def test_salt
10 |       assert_equal('mtsalt', @assignment.experiment_salt)
11 |     end
12 | 
13 |     def test_evaluate
14 |       assert_equal(1, @assignment.evaluate(1))
15 |       assert_equal(2, @assignment.evaluate(2))
16 |     end
17 | 
18 |     def test_set
19 |       @assignment.set(:color, 'green')
20 |       @assignment.set('platform', 'ios')
21 |       @assignment.set('foo', UniformChoice.new({ unit: 1, choices: ['x', 'y'] }))
22 |       assert_equal('green', @assignment.data[:color])
23 |       assert_equal('ios', @assignment.data[:platform])
24 |       assert_equal('y', @assignment.data[:foo])
25 |     end
26 | 
27 |     def test_get
28 |       @assignment.set(:button_text, 'Click Me!')
29 |       @assignment.set(:gender, 'f')
30 |       assert_equal('Click Me!', @assignment.get('button_text'))
31 |       assert_equal('f', @assignment.get('gender'))
32 |       assert_equal(10, @assignment.get('missing_key', 10))
33 |       assert_nil(@assignment.get('missing_key'))
34 |     end
35 | 
36 |     def test_get_params
37 |       @assignment.set('foo', 'bar')
38 |       @assignment.set(:baz, 'qux')
39 |       assert_equal({ foo: 'bar', baz: 'qux' }, @assignment.get_params)
40 |     end
41 |   end
42 | end
43 | 


--------------------------------------------------------------------------------
/alpha/ruby/test/plan_out/experiment_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative '../test_helper'
 2 | require_relative '../../examples/plan_out/voting_experiment'
 3 | 
 4 | module PlanOut
 5 |   class ExperimentTest < Minitest::Test
 6 |     def setup
 7 |       @voting_experiment = VotingExperiment.new(userid: 14)
 8 |       @voting_experiment2 = VotingExperiment.new(userid: 15)
 9 |       @voting_experiment.auto_exposure_log = false
10 |       @voting_experiment2.auto_exposure_log = false
11 |     end
12 | 
13 |     def test_get_attributes
14 |       assert_equal('ff0000', @voting_experiment.get(:button_color))
15 |       assert_equal(1, @voting_experiment.get(:missing_key, 1))
16 |       assert_equal('ff0000', @voting_experiment2.get(:button_color))
17 |       assert_equal("I'm voting", @voting_experiment.get(:button_text))
18 |       assert_equal("I'm voting", @voting_experiment2.get(:button_text))
19 |     end
20 | 
21 |     def test_get_params
22 |       assert_equal({ button_color: 'ff0000', button_text: "I'm voting" }, @voting_experiment.get_params)
23 |       assert_equal({ button_color: 'ff0000', button_text: "I'm voting" }, @voting_experiment2.get_params)
24 |     end
25 | 
26 |     def test_as_blob
27 |       result = @voting_experiment.as_blob
28 |       assert_equal('PlanOut::VotingExperiment', result[:name])
29 |       assert_equal('PlanOut::VotingExperiment', result[:salt])
30 |       assert_equal({ userid: 14 }, result[:inputs])
31 |     end
32 |   end
33 | end
34 | 


--------------------------------------------------------------------------------
/alpha/ruby/test/plan_out/operator_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative '../test_helper'
 2 | 
 3 | module PlanOut
 4 |   class OperatorTest < Minitest::Test
 5 |     def setup
 6 |       @a = Assignment.new('mtsalt')
 7 |     end
 8 | 
 9 |     def test_execute
10 |       operator = Operator.new({ foo: 'bar' })
11 |       op_simple = OpSimple.new({ bar: 'qux' })
12 |       assert_equal('mtsalt', operator.execute(@a))
13 |       assert_equal(-1, op_simple.execute(@a))
14 |     end
15 | 
16 |     def test_weighted_choice
17 |       weighted = WeightedChoice.new({
18 |         choices: ["c1", "c2", "c1"],
19 |         weights: [20, 40, 60],
20 |         unit: 42,
21 |         salt:'x'
22 |       })
23 |       assert_equal("c2", weighted.execute(@a))
24 |     end
25 |   end
26 | end
27 | 


--------------------------------------------------------------------------------
/alpha/ruby/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require_relative '../lib/plan_out'
3 | 


--------------------------------------------------------------------------------
/compiler/planout.jison:
--------------------------------------------------------------------------------
  1 | %lex
  2 | 
  3 | %%
  4 | 
  5 | "#"(.)*(\n|$)                             /* skip comments */
  6 | \s+                                       /* skip whitespace */
  7 | 
  8 | "true"                                    return 'TRUE'
  9 | "false"                                   return 'FALSE'
 10 | "null"                                    return 'NULL'
 11 | "@"                                       return 'JSON_START'
 12 | 
 13 | "switch"                                  return 'SWITCH';
 14 | "if"                                      return 'IF';
 15 | "else"                                    return 'ELSE';
 16 | 
 17 | "return"                                  return 'RETURN';
 18 | 
 19 | [a-zA-Z_][a-zA-Z0-9_]*                    return 'IDENTIFIER'
 20 | 
 21 | [0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?         { yytext = Number(yytext); return 'CONST'; }
 22 | \"(\\.|[^\\"])*\"                         { yytext = yytext.substr(1, yyleng-2); return 'CONST'; }
 23 | \'[^\']*\'                                { yytext = yytext.substr(1, yyleng-2); return 'CONST'; }
 24 | 
 25 | "<-"                                      return 'ARROW_ASSIGN'
 26 | "||"                                      return 'OR'
 27 | "&&"                                      return 'AND'
 28 | "??"                                      return 'COALESCE'
 29 | "=="                                      return 'EQUALS'
 30 | ">="                                      return 'GTE'
 31 | "<="                                      return 'LTE'
 32 | "!="                                      return 'NEQ'
 33 | "=>"                                      return 'THEN'
 34 | ";"                                       return 'END_STATEMENT'
 35 | 
 36 | "="|":"|"["|"]"|"("|")"|","|"{"|"}"|"+"|"%"|"*"|"-"|"/"|"%"|">"|"<"|"!"
 37 |                                           return yytext
 38 | 
 39 | /lex
 40 | 
 41 | %token ARROW_ASSIGN
 42 | %token AND
 43 | %token CONST
 44 | %token DEFAULT
 45 | %token ELSE
 46 | %token END_STATEMENT
 47 | %token EQUALS
 48 | %token GTE
 49 | %token IDENTIFIER
 50 | %token IF
 51 | %token JSON_START
 52 | %token LTE
 53 | %token NEQ
 54 | %token OR
 55 | %token COALESCE
 56 | %token SWITCH
 57 | %token THEN
 58 | %token RETURN
 59 | 
 60 | %left '!'
 61 | %left OR AND COALESCE
 62 | %left EQUALS NEQ LTE GTE '>' '<'
 63 | %left '+' '-'
 64 | %left '*' '/' '%'
 65 | %left '(' '!'
 66 | %left '['
 67 | 
 68 | %%
 69 | 
 70 | start
 71 |   : rules_list
 72 |     { $ = {"op": "seq", "seq": $1}; return $; }
 73 |   ;
 74 | 
 75 | rules_list
 76 |   : /* empty */
 77 |     { $ = []; }
 78 |   | rules_list rule
 79 |     { $ = $1; $.push($2); }
 80 |   ;
 81 | 
 82 | rule
 83 |   : expression
 84 |     { $ = $1; }
 85 |   | expression END_STATEMENT
 86 |     { $ = $1; }
 87 |   | IDENTIFIER '=' simple_expression END_STATEMENT
 88 |     { $ = {"op": "set", "var": $1, "value": $3}; }
 89 |   | IDENTIFIER ARROW_ASSIGN simple_expression END_STATEMENT
 90 |     { $ = {"op": "set", "var": $1, "value": $3}; }
 91 |   ;
 92 | 
 93 | expression
 94 |   : switch_expression
 95 |     { $ = $1; }
 96 |   | if_expression
 97 |     { $ = $1; }
 98 |   | return_expression
 99 |     { $ = $1; }
100 |   ;
101 | 
102 | simple_expression
103 |   : IDENTIFIER
104 |     { $ = {"op": "get", "var": $1}; }
105 |   | TRUE
106 |    { $ = true; }
107 |   | FALSE
108 |    { $ = false; }
109 |   | NULL
110 |    { $ = null; }
111 |   | '[' array ']'
112 |     { $ = {"op": "array", "values": $2}; }
113 |   | IDENTIFIER '(' arguments ')'
114 |     { $ = $3; $["op"] = $1; }
115 |   | simple_expression '[' simple_expression ']'
116 |     { $ = {"op": "index", "base": $1, "index": $3}; }
117 |   | '{' rules_list '}'
118 |     { $ = {"op": "seq", "seq": $2}; }
119 |   | '(' simple_expression ')'
120 |     { $ = $2; }
121 |   | CONST
122 |     { $ = $1; }
123 |   | JSON_START json { $ = {"op": "literal", "value": $2}; }
124 |   | simple_expression '%' simple_expression
125 |     { $ = {"op": "%", "left": $1, "right": $3}; }
126 |   | simple_expression '/' simple_expression
127 |     { $ = {"op": "/", "left": $1, "right": $3}; }
128 |   | simple_expression '>' simple_expression
129 |     { $ = {"op": ">", "left": $1, "right": $3}; }
130 |   | simple_expression '<' simple_expression
131 |     { $ = {"op": "<", "left": $1, "right": $3}; }
132 |   | simple_expression EQUALS simple_expression
133 |     { $ = {"op": "equals", "left": $1, "right": $3}; }
134 |   | simple_expression NEQ simple_expression
135 |     { $ = {"op": "not", "value": {"op": "equals", "left": $1, "right": $3}}; }
136 |   | simple_expression LTE simple_expression
137 |     { $ = {"op": "<=", "left": $1, "right": $3}; }
138 |   | simple_expression GTE simple_expression
139 |     { $ = {"op": ">=", "left": $1, "right": $3}; }
140 |   | simple_expression '+' simple_expression
141 |     { $ = {"op": "sum", "values": [$1, $3]}; }
142 |   | simple_expression '-' simple_expression
143 |     { $ = {"op": "sum", "values": [$1, {"op": "negative", "value": $3}]}; }
144 |   | simple_expression '*' simple_expression
145 |     { $ = {"op": "product", "values": [$1, $3]}; }
146 |   | '-' simple_expression
147 |     { $ = {"op": "negative", "value": $2}; }
148 |   | '!' simple_expression
149 |     { $ = {"op": "not", "value": $2}; }
150 |   | simple_expression OR simple_expression
151 |     { $ = {"op": "or", "values": [$1, $3]}; }
152 |   | simple_expression COALESCE simple_expression
153 |     { $ = {"op": "coalesce", "values": [$1, $3]}; }
154 |   | simple_expression AND simple_expression
155 |     { $ = {"op": "and", "values": [$1, $3]}; }
156 |   ;
157 | 
158 | array
159 |   : /* empty */
160 |     { $ = []; }
161 |   | simple_expression
162 |     { $ = [$1]; }
163 |   | array ',' simple_expression
164 |     { $ = $1; $.push($3); }
165 |   ;
166 | 
167 | json: /* true, false, null, etc. */
168 |   IDENTIFIER { $ = JSON.parse($1); }
169 |   | CONST { $ = $1; }
170 |   | TRUE { $ = true; }
171 |   | FALSE { $ = false; }
172 |   | NULL { $ = null; }
173 |   | '-' json_neg_num {$ = $2; }
174 |   | '[' json_array ']' { $ = $2; }
175 |   | '{' json_map '}' { $ = $2; }
176 |   ;
177 | 
178 | json_array: /* empty */ { $ = []; }
179 |   | json { $ = []; $.push($1); }
180 |   | json_array ',' json { $ = $1; $.push($3); }
181 |   ;
182 | 
183 | json_map: /* empty */ { $ = {}; }
184 |   | json ':' json { $ = {}; $[$1] = $3; }
185 |   | json_map ',' json ':' json { $ = $1; $[$3] = $5; }
186 |   ;
187 | 
188 | json_neg_num: CONST { $ = -$1; };
189 | 
190 | arguments
191 |   : /* empty */
192 |     { $ = {}; }
193 |   | arguments_list
194 |     { $ = $1; }
195 |   | values_list
196 |     { if ($1["values"].length > 1) {
197 |         $ = $1;
198 |       } else {
199 |         $ = {"value": $1["values"][0]};
200 |       }
201 |     }
202 |   ;
203 | 
204 | arguments_list
205 |   : IDENTIFIER '=' simple_expression
206 |     { $ = {}; $[$1] = $3; }
207 |   | arguments_list ',' IDENTIFIER '=' simple_expression
208 |     { $ = $1; $[$3] = $5; }
209 |   ;
210 | 
211 | values_list
212 |   : simple_expression
213 |     { $ = {}; $["values"] = [$1]; }
214 |   | values_list ',' simple_expression
215 |     { $ = $1; $["values"].push($3); }
216 |   ;
217 | 
218 | switch_expression
219 |   : SWITCH '{' cases_list '}'
220 |     { $ = {"op": "switch", "cases": $3}; }
221 |   ;
222 | 
223 | if_expression
224 |   : IF '(' simple_expression ')' simple_expression optional_else_expression
225 |     { $ = {"op": "cond", "cond": [{"if": $3, "then": $5}]};
226 |       if ($6["cond"]) {
227 |         $["cond"] = $["cond"].concat($6["cond"]);
228 |       }
229 |     }
230 |   ;
231 | 
232 | return_expression
233 |   : RETURN simple_expression
234 |    { $ = {"op": "return", "value": $2} }
235 | ;
236 | 
237 | optional_else_expression
238 |   : /* empty */
239 |     { $ = {}; }
240 |   | ELSE if_expression
241 |     { $ = $2; }
242 |   | ELSE simple_expression
243 |     { $ = {"op": "cond", "cond": [{"if": true, "then": $2}]}; }
244 |   ;
245 | 
246 | cases_list
247 |   : /* empty */
248 |     { $ = []; }
249 |   | cases_list case END_STATEMENT
250 |     { $ = $1; $.push($2); }
251 |   ;
252 | 
253 | case
254 |   : simple_expression THEN expression
255 |     { $ = {"op": "case", "condidion": $1, "result": $3}; }
256 |   ;
257 | 


--------------------------------------------------------------------------------
/compiler/readme.md:
--------------------------------------------------------------------------------
 1 | # PlanOut Compiler
 2 | 
 3 | `planout.js` is a script that parses a PlanOut experiment and outputs a serialized experiment in JSON format.
 4 | 
 5 | ## Using the Compiler
 6 | The script can be run from the command line via [node.js](http://nodejs.org/) as follows.
 7 | ```
 8 | node planout.js ../demos/sample_scripts/exp1.planout
 9 | ```
10 | 
11 | Alternatively, the script may be run from a web page.
12 | ```
13 | <script src="planout.js"></script>
14 | <script>
15 |   var json = planout.parse(script);
16 | </script>
17 | ```
18 | 
19 | Here is a [demo](http://facebook.github.io/planout/demo/planout-compiler.html).
20 | 
21 | ## Extending the PlanOut language
22 | The PlanOut grammar is specified in `planout.jison`. If you wish to extend PlanOut to support custom operators, you can modify the grammar file and generate a new compiler script using [Jison](http://zaach.github.io/jison/) library.
23 | 


--------------------------------------------------------------------------------
/contrib/postgres_logger.py:
--------------------------------------------------------------------------------
 1 | from planout.experiment import SimpleExperiment
 2 | 
 3 | import psycopg2 as pg
 4 | from psycopg2.extras import Json as pJson
 5 | 
 6 | class PostgresLoggedExperiment(SimpleExperiment):
 7 | 
 8 |     def configure_logger(self):
 9 |         """ Sets up a logger to postgres.
10 | 
11 |         1. Modify the connection_parameters variable to be a dictionary of the
12 |         parameters to create a connection to your postgres database.
13 |         2. Modify the table variable to be the table to which you plan on
14 |         logging.
15 |         """
16 | 
17 |         connection_parameters = {'host': 'localhost',
18 |                                  'database': 'experiments'}
19 |         table = 'experiments'
20 | 
21 |         self.conn = pg.connect(**connection_parameters)
22 |         self.table = table
23 | 
24 |     def log(self, data):
25 |         """ Log exposure. """
26 | 
27 |         columns = ['inputs', 'name', 'checksum', 'params', 'time', 'salt',
28 |                    'event']
29 | 
30 |         names = ','.join(columns)
31 |         placeholders = ','.join(['%s']*len(columns))
32 |         ins_statement = ('insert into {} ({}) values ({})'
33 |                          .format(self.table, names, placeholders))
34 | 
35 |         row = []
36 |         for column in columns:
37 |             value = data[column]
38 |             row.append(pJson(value) if isinstance(value, dict) else value)
39 | 
40 |         with self.conn.cursor() as curr:
41 |             curr.execute(ins_statement, row)
42 | 
43 |         self.conn.commit()
44 | 


--------------------------------------------------------------------------------
/contrib/pydata14_tutorial/README.md:
--------------------------------------------------------------------------------
 1 | # PyData '14 PlanOut Tutorial
 2 | Welcome to the PyData '14 Silicon Valley PlanOut tutorial! You'll find a collection of IPython notebooks for Eytan Bakshy's tutorial on PlanOut.
 3 | 
 4 | If you want to follow along the live tutorial on your own computer (or are not at PyData!), you need to install some software and clone the PlanOut git repository.
 5 | 
 6 | ## Requirements
 7 | ### Software requirements
 8 | The tutorial requires IPython, Pandas, and PlanOut. The former two come with Anaconda. PlanOut has only been tested on Mac OS X and Linux.
 9 | 
10 |  * IPython ([installation instructions](http://ipython.org/install.html). We recommend Anaconda.)
11 |  * PlanOut v0.2 or greater (first timers `sudo easy_install planout`, older timers `sudo easy_install --upgrade planout`)
12 |  * Node.js - optional (installation link on the [node.js homepage](http://nodejs.org))
13 | 
14 | Note that you may need to re-install the `planout` package if you installed PlanOut before installing IPython.
15 | 
16 | ### Downloading the tutorial files
17 | If you have git, you can checkout PlanOut by typing:
18 | 
19 | ```
20 | git clone https://github.com/facebook/planout.git
21 | ```
22 | 
23 | or you can [click here](https://github.com/facebook/planout/archive/master.zip) to download a zip archive.
24 | 
25 | 
26 | ## Loading up the tutorial notebooks
27 | Navigate to your checked out version of PlanOut and type:
28 | 
29 | ```
30 | cd contrib/pydata14_tutorial/
31 | ipython notebook --pylab inline
32 | ```
33 | 
34 | Tutorial files include:
35 |  * `0-getting-started.ipynb`: This teaches you the basics of how to implement experiments in pure Python.
36 |  * `1-logging.ipynb`: How data is logged in PlanOut and examples of how to analyze PlanOut log data with Pandas.
37 |  * `2-interpreter.ipynb`: How to generate serialized experiment definitions using (1) the PlanOut domain-specific language (2) automatically, e.g., via configuration files or GUIs.
38 |  * `3-namespaces.ipynb`: How to manage simultaneous and follow-on experiments.
39 | 


--------------------------------------------------------------------------------
/demos/anchoring_demo.py:
--------------------------------------------------------------------------------
  1 | import random
  2 | from uuid import uuid4
  3 | from flask import (
  4 |   Flask,
  5 |   session,
  6 |   request,
  7 |   redirect,
  8 |   url_for,
  9 |   render_template_string
 10 | )
 11 | app = Flask(__name__)
 12 | 
 13 | app.config.update(dict(
 14 |   DEBUG=True,
 15 |   SECRET_KEY='3.14159', # shhhhh
 16 | ))
 17 | 
 18 | from planout.experiment import SimpleExperiment
 19 | from planout.ops.random import *
 20 | 
 21 | class AnchoringExperiment(SimpleExperiment):
 22 |   def setup(self):
 23 |     self.set_log_file('anchoring_webapp.log')
 24 | 
 25 |   def assign(self, params, userid):
 26 |     params.use_round_number = BernoulliTrial(p=0.5, unit=userid)
 27 |     if params.use_round_number:
 28 |       params.price = UniformChoice(choices=[240000, 250000, 260000],
 29 |         unit=userid)
 30 |     else:
 31 |       params.price = RandomInteger(min=240000, max=260000, unit=userid)
 32 | 
 33 | def money_format(number):
 34 |   return "${:,.2f}".format(number)
 35 | 
 36 | @app.route('/')
 37 | def main():
 38 |     # if no userid is defined make one up
 39 |     if 'userid' not in session:
 40 |         session['userid'] = str(uuid4())
 41 | 
 42 |     anchoring_exp = AnchoringExperiment(userid=session['userid'])
 43 |     price = anchoring_exp.get('price')
 44 | 
 45 |     return render_template_string("""
 46 |     <html>
 47 |       <head>
 48 |         <title>Let's buy a house!</title>
 49 |       </head>
 50 |       <body>
 51 |         <h3>
 52 |           A lovely new home is going on the market for {{ price }}. <br>
 53 |         </h3>
 54 |         <p>
 55 |           What will be your first offer?
 56 |         </p>
 57 |         <form action="/bid" method="GET">
 58 |           
lt;input type="text" length="10" name="bid"></input>
 59 |           <input type="submit"></input>
 60 |         </form>
 61 |       <br>
 62 |       <p><a href="/">Reload without resetting my session ID. I'll get the same offer when I come back.</a></p>
 63 |       <p><a href="/reset">Reset my session ID so I get re-randomized into a new treatment.</a></p>
 64 |       </body>
 65 |     </html>
 66 |     """, price=money_format(price))
 67 | 
 68 | @app.route('/reset')
 69 | def reset():
 70 |   session.clear()
 71 |   return redirect(url_for('main'))
 72 | 
 73 | @app.route('/bid')
 74 | def bid():
 75 |   bid_string = request.args.get('bid')
 76 |   bid_string = bid_string.replace(',', '') # get rid of commas
 77 |   try:
 78 |     bid_amount = int(bid_string)
 79 | 
 80 |     anchoring_exp = AnchoringExperiment(userid=session['userid'])
 81 |     anchoring_exp.log_event('bid', {'bid_amount': bid_amount})
 82 | 
 83 |     return render_template_string("""
 84 |       <html>
 85 |         <head>
 86 |           <title>Nice bid!</title>
 87 |         </head>
 88 |         <body>
 89 |           <p>You bid {{ bid }}. We'll be in touch if they accept your offer!</p>
 90 |           <p><a href="/">Back</a></p>
 91 |         </body>
 92 |       </html>
 93 |       """, bid=money_format(bid_amount))
 94 |   except ValueError:
 95 |     return render_template_string("""
 96 |       <html>
 97 |         <head>
 98 |           <title>Bad bid!</title>
 99 |         </head>
100 |         <body>
101 |           <p>You bid {{ bid }}. That's not a number, so we probably won't be accepting your bid.</p>
102 |           <p><a href="/">Back</a></p>
103 |         </body>
104 |       </html>
105 |       """, bid=bid_string)
106 | 
107 | 
108 | if __name__ == '__main__':
109 |     app.run()
110 | 


--------------------------------------------------------------------------------
/demos/demo_experiments.py:
--------------------------------------------------------------------------------
 1 | import interpreter_experiment_examples as interpreter
 2 | import simple_experiment_examples as simple_experiment
 3 | 
 4 | def demo_experiment1(module):
 5 |   print 'using %s...' % module.__name__
 6 |   exp1_runs = [module.Exp1(userid=i) for i in xrange(10)]
 7 |   print [(e.get('group_size'), e.get('ratings_goal')) for e in exp1_runs]
 8 | 
 9 | def demo_experiment2(module):
10 |   print 'using %s...' % module.__name__
11 |   # number of cues and selection of cues depends on userid and pageid
12 |   for u in xrange(1,4):
13 |     for p in xrange(1, 4):
14 |       print module.Exp2(userid=u, pageid=p, liking_friends=['a','b','c','d'])
15 | 
16 | def demo_experiment3(module):
17 |   print 'using %s...' % module.__name__
18 |   for i in xrange(5):
19 |     print module.Exp3(userid=i)
20 | 
21 | def demo_experiment4(module):
22 |   print 'using %s...' % module.__name__
23 |   for i in xrange(5):
24 |     # probability of collapsing is deterministic on sourceid
25 |     e = module.Exp4(sourceid=i, storyid=1, viewerid=1)
26 |     # whether or not the story is collapsed depends on the sourceid
27 |     exps = [module.Exp4(sourceid=i, storyid=1, viewerid=v) for v in xrange(10)]
28 |     print e.get('prob_collapse'), [exp.get('collapse') for exp in exps]
29 | 
30 | if __name__ == '__main__':
31 |   # run each experiment implemented using SimpleExperiment (simple_experiment)
32 |   # or using the interpreter
33 |   print '\nDemoing experiment 1...'
34 |   demo_experiment1(simple_experiment)
35 |   demo_experiment1(interpreter)
36 | 
37 |   print '\nDemoing experiment 2...'
38 |   demo_experiment2(simple_experiment)
39 |   demo_experiment2(interpreter)
40 | 
41 |   print '\nDemoing experiment 3...'
42 |   demo_experiment3(simple_experiment)
43 |   demo_experiment3(interpreter)
44 | 
45 |   print '\nDemoing experiment 4...'
46 |   demo_experiment4(simple_experiment)
47 |   demo_experiment4(interpreter)
48 | 


--------------------------------------------------------------------------------
/demos/demo_namespaces.py:
--------------------------------------------------------------------------------
 1 | from planout.namespace import SimpleNamespace
 2 | from planout.experiment import SimpleExperiment, DefaultExperiment
 3 | from planout.ops.random import *
 4 | 
 5 | 
 6 | class V1(SimpleExperiment):
 7 |   def assign(self, params, userid):
 8 |     params.banner_text = UniformChoice(
 9 |       choices=['Hello there!', 'Welcome!'],
10 |       unit=userid)
11 | 
12 | class V2(SimpleExperiment):
13 |   def assign(self, params, userid):
14 |     params.banner_text = WeightedChoice(
15 |       choices=['Hello there!', 'Welcome!'],
16 |       weights=[0.8, 0.2],
17 |       unit=userid)
18 | 
19 | class V3(SimpleExperiment):
20 |   def assign(self, params, userid):
21 |     params.banner_text = WeightedChoice(
22 |       choices=['Nice to see you!', 'Welcome back!'],
23 |       weights=[0.8, 0.2],
24 |       unit=userid)
25 | 
26 | 
27 | class DefaultButtonExperiment(DefaultExperiment):
28 |   def get_default_params(self):
29 |     return {'banner_text': 'Generic greetings!'}
30 | 
31 | class ButtonNamespace(SimpleNamespace):
32 |   def setup(self):
33 |     self.name = 'my_demo'
34 |     self.primary_unit = 'userid'
35 |     self.num_segments = 100
36 |     self.default_experiment_class = DefaultButtonExperiment
37 | 
38 |   def setup_experiments(self):
39 |     self.add_experiment('first version phase 1', V1, 10)
40 |     self.add_experiment('first version phase 2', V1, 30)
41 |     self.add_experiment('second version phase 1', V2, 40)
42 |     self.remove_experiment('second version phase 1')
43 |     self.add_experiment('third version phase 1', V3, 30)
44 | 
45 | if __name__ == '__main__':
46 |   for i in xrange(100):
47 |     e = ButtonNamespace(userid=i)
48 |     print 'user %s: %s' % (i, e.get('banner_text'))
49 | 


--------------------------------------------------------------------------------
/demos/interpreter_experiment_examples.py:
--------------------------------------------------------------------------------
 1 | from planout.experiment import SimpleInterpretedExperiment
 2 | import json
 3 | 
 4 | class Exp1(SimpleInterpretedExperiment):
 5 |   def loadScript(self):
 6 |     self.script = json.loads(open("sample_scripts/exp1.json").read())
 7 | 
 8 | class Exp2(SimpleInterpretedExperiment):
 9 |   def loadScript(self):
10 |     self.script = json.loads(open("sample_scripts/exp2.json").read())
11 | 
12 | class Exp3(SimpleInterpretedExperiment):
13 |   def loadScript(self):
14 |     self.script = json.loads(open("sample_scripts/exp3.json").read())
15 | 
16 | class Exp4(SimpleInterpretedExperiment):
17 |   def loadScript(self):
18 |     self.script = json.loads(open("sample_scripts/exp4.json").read())
19 | 


--------------------------------------------------------------------------------
/demos/sample_scripts/exp1.json:
--------------------------------------------------------------------------------
1 | {"op":"seq","seq":[{"op":"set","var":"group_size","value":{"choices":{"op":"array","values":[1,10]},"unit":{"op":"get","var":"userid"},"op":"uniformChoice"}},{"op":"set","var":"specific_goal","value":{"p":0.8,"unit":{"op":"get","var":"userid"},"op":"bernoulliTrial"}},{"op":"cond","cond":[{"if":{"op":"get","var":"specific_goal"},"then":{"op":"seq","seq":[{"op":"set","var":"ratings_per_user_goal","value":{"choices":{"op":"array","values":[8,16,32,64]},"unit":{"op":"get","var":"userid"},"op":"uniformChoice"}},{"op":"set","var":"ratings_goal","value":{"op":"product","values":[{"op":"get","var":"group_size"},{"op":"get","var":"ratings_per_user_goal"}]}}]}}]}]}


--------------------------------------------------------------------------------
/demos/sample_scripts/exp1.planout:
--------------------------------------------------------------------------------
1 | group_size = uniformChoice(choices=[1, 10], unit=userid);
2 | specific_goal = bernoulliTrial(p=0.8, unit=userid);
3 | 
4 | if (specific_goal) {
5 |   ratings_per_user_goal = uniformChoice(
6 |     choices=[8, 16, 32, 64], unit=userid);
7 |   ratings_goal = group_size * ratings_per_user_goal;
8 | }
9 | 


--------------------------------------------------------------------------------
/demos/sample_scripts/exp2.json:
--------------------------------------------------------------------------------
1 | {"op":"seq","seq":[{"op":"set","var":"num_cues","value":{"min":1,"max":{"values":[{"value":{"op":"get","var":"liking_friends"},"op":"length"},3],"op":"min"},"unit":{"op":"array","values":[{"op":"get","var":"userid"},{"op":"get","var":"pageid"}]},"op":"randomInteger"}},{"op":"set","var":"friends_shown","value":{"choices":{"op":"get","var":"liking_friends"},"draws":{"op":"get","var":"num_cues"},"unit":{"op":"array","values":[{"op":"get","var":"userid"},{"op":"get","var":"pageid"}]},"op":"sample"}}]}
2 | 


--------------------------------------------------------------------------------
/demos/sample_scripts/exp2.planout:
--------------------------------------------------------------------------------
1 | num_cues = randomInteger(
2 |   min=1, max=min(length(liking_friends), 3),
3 |   unit=[userid, pageid]);
4 | 
5 | friends_shown = sample(
6 |   choices=liking_friends, draws=num_cues,
7 |   unit=[userid, pageid]);
8 | 


--------------------------------------------------------------------------------
/demos/sample_scripts/exp3.json:
--------------------------------------------------------------------------------
1 | {"op":"seq","seq":[{"op":"set","var":"has_banner","value":{"p":0.97,"unit":{"op":"get","var":"userid"},"op":"bernoulliTrial"}},{"op":"set","var":"cond_probs","value":{"op":"array","values":[0.5,0.95]}},{"op":"set","var":"has_feed_stories","value":{"p":{"op":"index","base":{"op":"get","var":"cond_probs"},"index":{"op":"get","var":"has_banner"}},"unit":{"op":"get","var":"userid"},"op":"bernoulliTrial"}},{"op":"set","var":"button_text","value":{"choices":{"op":"array","values":["I'm a voter","I'm voting"]},"unit":{"op":"get","var":"userid"},"op":"uniformChoice"}}]}
2 | 


--------------------------------------------------------------------------------
/demos/sample_scripts/exp3.planout:
--------------------------------------------------------------------------------
1 | has_banner = bernoulliTrial(p=0.97, unit=userid);
2 | cond_probs = [0.5, 0.95];
3 | has_feed_stories = bernoulliTrial(
4 |    p=cond_probs[has_banner], unit=userid);
5 | button_text = uniformChoice(
6 |    choices=["I'm a voter", "I'm voting"], unit=userid);
7 | 


--------------------------------------------------------------------------------
/demos/sample_scripts/exp4.json:
--------------------------------------------------------------------------------
1 | {"op":"seq","seq":[{"op":"set","var":"prob_collapse","value":{"min":0,"max":1,"unit":{"op":"get","var":"sourceid"},"op":"randomFloat"}},{"op":"set","var":"collapse","value":{"p":{"op":"get","var":"prob_collapse"},"unit":{"op":"array","values":[{"op":"get","var":"storyid"},{"op":"get","var":"viewerid"}]},"op":"bernoulliTrial"}}]}


--------------------------------------------------------------------------------
/demos/sample_scripts/exp4.planout:
--------------------------------------------------------------------------------
1 | prob_collapse = randomFloat(min=0.0, max=1.0,
2 |    unit=sourceid);
3 | collapse = bernoulliTrial(p=prob_collapse,
4 |    unit=[storyid, viewerid]);
5 | 


--------------------------------------------------------------------------------
/demos/simple_experiment_examples.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) 2014, Facebook, Inc.
 2 | # All rights reserved.
 3 | #
 4 | # This source code is licensed under the BSD-style license found in the
 5 | # LICENSE file in the root directory of this source tree. An additional grant
 6 | # of patent rights can be found in the PATENTS file in the same directory.
 7 | 
 8 | from planout.experiment import SimpleExperiment
 9 | from planout.ops.random import *
10 | 
11 | class Exp1(SimpleExperiment):
12 |   def assign(self, e, userid):
13 |     e.group_size = UniformChoice(choices=[1, 10], unit=userid);
14 |     e.specific_goal = BernoulliTrial(p=0.8, unit=userid);
15 |     if e.specific_goal:
16 |       e.ratings_per_user_goal = UniformChoice(
17 |         choices=[8, 16, 32, 64], unit=userid)
18 |       e.ratings_goal = e.group_size * e.ratings_per_user_goal
19 |     return e
20 | 
21 | class Exp2(SimpleExperiment):
22 |   def assign(self, params, userid, pageid, liking_friends):
23 |     params.num_cues = RandomInteger(
24 |       min=1,
25 |       max=min(len(liking_friends), 3),
26 |       unit=[userid, pageid]
27 |     )
28 |     params.friends_shown = Sample(
29 |       choices=liking_friends,
30 |       draws=params.num_cues,
31 |       unit=[userid, pageid]
32 |     )
33 | 
34 | class Exp3(SimpleExperiment):
35 |   def assign(self, e, userid):
36 |     e.has_banner = BernoulliTrial(p=0.97, unit=userid)
37 |     cond_probs = [0.5, 0.95]
38 |     e.has_feed_stories = BernoulliTrial(p=cond_probs[e.has_banner], unit=userid)
39 |     e.button_text = UniformChoice(
40 |       choices=["I'm a voter", "I'm voting"], unit=userid)
41 | 
42 | 
43 | class Exp4(SimpleExperiment):
44 |   def assign(self, e, sourceid, storyid, viewerid):
45 |     e.prob_collapse = RandomFloat(min=0.0, max=1.0, unit=sourceid)
46 |     e.collapse = BernoulliTrial(p=e.prob_collapse, unit=[storyid, viewerid])
47 |     return e
48 | 


--------------------------------------------------------------------------------
/planout-editor/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | 


--------------------------------------------------------------------------------
/planout-editor/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn planout-editor-kernel:app
2 | 


--------------------------------------------------------------------------------
/planout-editor/README.md:
--------------------------------------------------------------------------------
 1 | ## PlanOut Editor
 2 | 
 3 | The PlanOut editor lets you interactively edit and test PlanOut code.  It's built on Flux and React.
 4 | 
 5 | ## Running
 6 | 
 7 | The PlanOut editor executes PlanOut scripts and tests by sending data to a
 8 | Python-based kernel which responds to AJAX requests. To start the kernel,
 9 | run the following from your command line:
10 | 
11 | `python planout-editor-kernel.py`
12 | 
13 | Then, navigate to:
14 | 
15 | [http://localhost:5000/](http://localhost:5000/).
16 | 
17 | 
18 | Note that the kernel requires that you have Flask and PlanOut already installed.
19 | If you don't have either package, you can install them via
20 | [pip](http://pip.readthedocs.org/en/latest/installing.html) by typing
21 | `pip install Flask` and `pip install planout`.
22 | 
23 | 
24 | ## Hacking the Editor
25 | If you want to make changes or contribute to the PlanOut editor, you must first
26 | have [npm](https://www.npmjs.org/) installed on your computer.
27 | 
28 | You can then install all the package dependencies by going to the root directory
29 | and entering in the following into the command line.
30 | 
31 | `npm install`
32 | 
33 | If you don't already have watchify installed, you may have to type `npm install watchify` before entering `npm install`.
34 | 
35 | Then, to build the project, run this command:
36 | 
37 | `npm start`
38 | 
39 | This will perform an initial build and start a watcher process that will
40 | continuously update the main application file, `bundle.js`, with any changes you make.  This way, you can test your changes to the editor by simply saving your code and hitting refresh in your Web browser.
41 | 
42 | This watcher is
43 | based on [Browserify](http://browserify.org/) and
44 | [Watchify](https://github.com/substack/watchify), and it transforms
45 | React's JSX syntax into standard JavaScript with
46 | [Reactify](https://github.com/andreypopp/reactify).
47 | 


--------------------------------------------------------------------------------
/planout-editor/js/actions/PlanOutExperimentActions.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Copyright 2013-2014 Facebook, Inc.
 3 |  *
 4 |  * PlanOutExperimentActions
 5 |  */
 6 | 
 7 | var PlanOutEditorDispatcher = require('../dispatcher/PlanOutEditorDispatcher');
 8 | var PlanOutEditorConstants = require('../constants/PlanOutEditorConstants');
 9 | var ActionTypes = PlanOutEditorConstants.ActionTypes;
10 | 
11 | 
12 | var PlanOutExperimentActions = {
13 |   loadScript: function(/*string*/ script) {
14 |     PlanOutEditorDispatcher.handleViewAction({
15 |       actionType: ActionTypes.EDITOR_LOAD_SCRIPT,
16 |       script: script
17 |     });
18 |   },
19 | 
20 |   compile: function(/*string*/ script) {
21 |     PlanOutEditorDispatcher.handleViewAction({
22 |       actionType: ActionTypes.EDITOR_COMPILE_SCRIPT,
23 |       script: script
24 |     });
25 |   },
26 | 
27 |   updateCompiledCode: function(/*string*/ script, /*string*/ status, /*object*/ json) {
28 |     PlanOutEditorDispatcher.handleViewAction({
29 |       actionType: ActionTypes.EDITOR_UPDATE_COMPILED_CODE,
30 |       script: script,
31 |       status: status,
32 |       json: json
33 |     });
34 |   }
35 | };
36 | 
37 | module.exports = PlanOutExperimentActions;
38 | 


--------------------------------------------------------------------------------
/planout-editor/js/actions/PlanOutTesterActions.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Copyright 2013-2014 Facebook, Inc.
 3 |  *
 4 |  * PlanOutTesterActions
 5 |  */
 6 | 
 7 | var PlanOutEditorDispatcher = require('../dispatcher/PlanOutEditorDispatcher');
 8 | 
 9 | var PlanOutEditorConstants = require('../constants/PlanOutEditorConstants');
10 | var ActionTypes = PlanOutEditorConstants.ActionTypes;
11 | 
12 | 
13 | var PlanOutTesterActions = {
14 |   init: function() {
15 |     PlanOutEditorDispatcher.handleViewAction({
16 |       actionType: ActionTypes.INIT_TESTER_PANEL
17 |     });
18 |   },
19 | 
20 |   create: function() {
21 |     PlanOutEditorDispatcher.handleViewAction({
22 |       actionType: ActionTypes.TESTER_CREATE
23 |     });
24 |   },
25 | 
26 |   /**
27 |    * @param  {objects} tests to initialize PlanOutTesterPanel
28 |    */
29 |   loadTests: function(/*object*/ tests) {
30 |     PlanOutEditorDispatcher.handleViewAction({
31 |       actionType: ActionTypes.LOAD_SERIALIZED_TESTERS,
32 |       tests: tests
33 |     });
34 |   },
35 | 
36 |   /**
37 |    * Re-runs all tests
38 |    */
39 |   refreshAllTests: function() {
40 |     PlanOutEditorDispatcher.handleViewAction({
41 |       actionType: ActionTypes.TESTER_REFRESH_ALL_TESTS
42 |     });
43 |   },
44 | 
45 |   /**
46 |    * @param  {string} id The ID of the tester item
47 |    * @param  {string} fields_to_update named valid JSON representing each field
48 |    */
49 |   updateTester: function( /*string*/ id, /*object*/ fields_to_update) {
50 |     PlanOutEditorDispatcher.handleViewAction({
51 |       actionType: ActionTypes.TESTER_USER_UPDATE_TEST,
52 |       id: id,
53 |       fieldsToUpdate: fields_to_update
54 |     });
55 |   },
56 | 
57 |   // Eventually this might be handled by a UI component that has users enter the
58 |   // data as structured data using some nice form, so we won't need to handle
59 |   // this exception.
60 |   // This is separate from updateTesterOutput because we don't want to keep the
61 |   // output and errors fixed while the user is editing the JSON field
62 |   updateTesterWithInvalidForm: function(/*string*/ id, /*string*/ status) {
63 |     PlanOutEditorDispatcher.handleViewAction({
64 |       actionType: ActionTypes.TESTER_INVALID_TEST_FORM,
65 |       id: id,
66 |       status: status
67 |     });
68 |   },
69 | 
70 |   updateTesterOutput: function(/*string*/ id, /*array*/ errors, /*object*/ results) {
71 |     PlanOutEditorDispatcher.handleViewAction({
72 |       actionType: ActionTypes.TESTER_SERVER_UPDATE_TEST,
73 |       id: id,
74 |       errors: errors,
75 |       results: results
76 |     });
77 |   },
78 | 
79 |   /**
80 |    * @param  {string} id
81 |    */
82 |   destroy: function(/*string*/ id) {
83 |     PlanOutEditorDispatcher.handleViewAction({
84 |       actionType: ActionTypes.TESTER_DESTROY,
85 |       id: id
86 |     });
87 |   }
88 | };
89 | 
90 | module.exports = PlanOutTesterActions;
91 | 


--------------------------------------------------------------------------------
/planout-editor/js/app.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Copyright 2013-2014 Facebook, Inc.
 3 |  *
 4 |  * @jsx React.DOM
 5 |  */
 6 | 
 7 | var React = require('react');
 8 | 
 9 | var PlanOutTesterPanel = require('./components/PlanOutTesterPanel.react');
10 | var PlanOutScriptPanel = require('./components/PlanOutScriptPanel.react');
11 | var PlanOutEditorButtons = require('./components/PlanOutEditorButtons.react');
12 | var PlanOutExperimentActions = require('./actions/PlanOutExperimentActions');
13 | var DemoData = require('./utils/DemoData');
14 | 
15 | React.render(
16 |       <div className="container">
17 |       <div className="page-header">
18 |         <h1>PlanOut Experiment Editor</h1>
19 |       </div>
20 |       <div className="row">
21 |         <div className="col-md-5"><PlanOutScriptPanel/></div>
22 |         <div className="col-md-5"><PlanOutTesterPanel/></div>
23 |       </div>
24 |       <br/>
25 |       <PlanOutEditorButtons/>
26 |       <br/>
27 |       <p><a href="http://facebook.github.io/planout/docs/planout-language.html">
28 |         Learn more about the PlanOut language
29 |       </a></p>
30 |     </div>,
31 |   document.getElementById('planouteditor')
32 | );
33 | 
34 | PlanOutExperimentActions.loadScript(DemoData.getDemoScript());
35 | 


--------------------------------------------------------------------------------
/planout-editor/js/components/PlanOutEditorButtons.react.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Copyright 2013-2014 Facebook, Inc.
  3 |  *
  4 |  * @providesModule PlanOutEditorButtons.react
  5 |  * @jsx React.DOM
  6 |  */
  7 | 
  8 | var _ = require('underscore');
  9 | var React = require('react/addons');
 10 | var ReactPropTypes = React.PropTypes;
 11 | var cx = React.addons.classSet;
 12 | 
 13 | var PlanOutAsyncRequests = require('../utils/PlanOutAsyncRequests');
 14 | var PlanOutExperimentActions = require('../actions/PlanOutExperimentActions');
 15 | var PlanOutExperimentStore = require('../stores/PlanOutExperimentStore');
 16 | var PlanOutTesterActions = require('../actions/PlanOutTesterActions');
 17 | var PlanOutTesterStore = require('../stores/PlanOutTesterStore');
 18 | 
 19 | var DemoData = require('../utils/DemoData');
 20 | 
 21 | 
 22 | function getStateFromStores() /*object*/ {
 23 |   return {
 24 |     doesCompile: PlanOutExperimentStore.doesCompile(),
 25 |     json: PlanOutExperimentStore.getJSON(),
 26 |     passesTests: PlanOutTesterStore.areAllPassing(),
 27 |     script: PlanOutExperimentStore.getScript(),
 28 |     tests: PlanOutTesterStore.getAllTests()
 29 |   };
 30 | }
 31 | 
 32 | var PlanOutEditorButtons = React.createClass({
 33 |   getInitialState: function() /*object*/ {
 34 |     return getStateFromStores();
 35 |   },
 36 | 
 37 |   componentDidMount: function() {
 38 |     PlanOutTesterStore.addChangeListener(this._onChange);
 39 |     PlanOutExperimentStore.addChangeListener(this._onChange);
 40 |   },
 41 | 
 42 |   componentWillUnmount: function() {
 43 |     PlanOutTesterStore.removeChangeListener(this._onChange);
 44 |     PlanOutExperimentStore.removeChangeListener(this._onChange);
 45 |   },
 46 | 
 47 |   _onChange: function() {
 48 |     this.setState(getStateFromStores());
 49 |   },
 50 | 
 51 |   render: function() {
 52 |     var cx = React.addons.classSet;
 53 |     var buttonClass = cx({
 54 |       'btn': true,
 55 |       'btn-default': true,
 56 |       'disabled': !(this.state.doesCompile && this.state.passesTests)
 57 |     });
 58 |     return (
 59 |       <div>
 60 |         <button type="button" className='btn btn-default'
 61 |          onClick={this._loadSampleData}
 62 |         >
 63 |           Load sample experiment
 64 |         </button>
 65 |         &nbsp;&nbsp;
 66 |         <button type="button" className={buttonClass}
 67 |           onClick={this._saveAll}>
 68 |           Save all
 69 |         </button>
 70 |         &nbsp;&nbsp;
 71 |         <button type="button" className={buttonClass}
 72 |          onClick={this._saveJSON}>
 73 |           Save JSON
 74 |         </button>
 75 |       </div>
 76 |     );
 77 |   },
 78 | 
 79 |   _loadSampleData: function() {
 80 |     PlanOutExperimentActions.loadScript(DemoData.getDemoScript());
 81 |     PlanOutTesterActions.loadTests(DemoData.getDemoTests());
 82 |     PlanOutTesterActions.refreshAllTests();
 83 |   },
 84 | 
 85 |   _saveJSON: function() {
 86 |     PlanOutAsyncRequests.saveState(this.state.json, "experiment_code.json");
 87 |   },
 88 | 
 89 |   _saveAll: function() {
 90 |     var all = {
 91 |       script: this.state.script,
 92 |       json: this.state.json,
 93 |       tests: PlanOutTesterStore.getSerializedTests()
 94 |     };
 95 |     PlanOutAsyncRequests.saveState(all, "experiment_all.json");
 96 |   },
 97 | });
 98 | 
 99 | module.exports = PlanOutEditorButtons;
100 | 


--------------------------------------------------------------------------------
/planout-editor/js/components/PlanOutScriptPanel.react.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Copyright 2014 Facebook, Inc.
  3 |  *
  4 |  * @providesModule PlanOutScriptPanel.react
  5 |  * @jsx React.DOM
  6 |  */
  7 | 
  8 | var React = require('react');
  9 | 
 10 | var PlanOutExperimentActions = require('../actions/PlanOutExperimentActions');
 11 | var PlanOutExperimentStore = require('../stores/PlanOutExperimentStore');
 12 | 
 13 | 
 14 | function getStateFromStores() {
 15 |   return {
 16 |     compilerMessage: PlanOutExperimentStore.getCompilerMessage(),
 17 |     doesCompile: PlanOutExperimentStore.doesCompile(),
 18 |     inputVariables: PlanOutExperimentStore.getInputVariables(),
 19 |     json: PlanOutExperimentStore.getJSON(),
 20 |     params: PlanOutExperimentStore.getParams(),
 21 |     script: PlanOutExperimentStore.getScript()
 22 |   };
 23 | }
 24 | 
 25 | var PlanOutScriptPanel = React.createClass({
 26 | 
 27 |   getInitialState: function() /*object*/ {
 28 |     var state_data = getStateFromStores();
 29 |     state_data.showCompiledBlock = false;
 30 |     return state_data;
 31 |   },
 32 | 
 33 |   componentDidMount: function() {
 34 |     PlanOutExperimentStore.addChangeListener(this._onChange);
 35 |   },
 36 | 
 37 |   componentWillUnmount: function() {
 38 |     PlanOutExperimentStore.removeChangeListener(this._onChange);
 39 |   },
 40 | 
 41 |   _onChange: function() {
 42 |     this.setState(getStateFromStores());
 43 |   },
 44 | 
 45 | 
 46 |   render: function() {
 47 |     return (
 48 |       <div>
 49 |         {this.renderCodeBlock()}
 50 |         <div className="disabled">
 51 |           <a href="#" onClick={this._toggleShowCompiled}>
 52 |             {this.state.showCompiledBlock ? 'Hide' : 'Show'} serialized code
 53 |           </a>
 54 |         </div>
 55 |         {this.renderScriptStatus()}
 56 |         <input type="hidden" name="qe_planout_compiled"
 57 |           value={JSON.stringify(this.state.json, false, " ")}/>
 58 |       </div>
 59 |     );
 60 |   },
 61 | 
 62 |   renderCodeBlock: function() {
 63 |     if (this.state.showCompiledBlock) {
 64 |       return this.renderCompiledBlock();
 65 |     } else {
 66 |       return this.renderScriptInputBlock();
 67 |     }
 68 |   },
 69 | 
 70 |   renderScriptStatus: function() {
 71 |     return (
 72 |       <dl className="dl">
 73 |         <dt className="fields">Compilation status</dt>
 74 |         <dd>{this.renderCompileStatus()}</dd>
 75 |         <dt className="fields">Input units</dt>
 76 |         <dd>{this.state.inputVariables.join(', ')}</dd>
 77 |         <dt className="fields">Parameters</dt>
 78 |         <dd>{this.state.params.join(', ')}</dd>
 79 |       </dl>
 80 |     );
 81 |   },
 82 | 
 83 |   /**
 84 |    * This could be swapped out with various open-source components.
 85 |    */
 86 |   renderScriptInputBlock: function() {
 87 |     return (
 88 |       <textarea
 89 |         id="qe_planout_source" ref="qe_planout_source"
 90 |         name="qe_planout_source"
 91 |         spellCheck="false"
 92 |         value={this.state.script}
 93 |         onChange={this._onCodeChange}
 94 |       />
 95 |     );
 96 |   },
 97 | 
 98 |   renderCompileStatus: function() {
 99 |     return (
100 |       <pre className={this.state.doesCompile ? "correct" : "error"}>
101 |         {this.state.compilerMessage}
102 |       </pre>
103 |     );
104 |   },
105 | 
106 |   renderCompiledBlock: function() {
107 |     if (!this.state.showCompiledBlock) {
108 |       return null;
109 |     }
110 |     return (
111 |       <div ref="compiledBlock">
112 |         <textarea lang="json"
113 |           value={JSON.stringify(this.state.json, false, " ")}
114 |           readOnly={true}
115 |         />
116 |       </div>
117 |     );
118 |   },
119 | 
120 |   /**
121 |    *  Toggles whether compiled JSON code gets shown.
122 |    */
123 |   _toggleShowCompiled: function(event) {
124 |     this.setState({showCompiledBlock: !this.state.showCompiledBlock});
125 |     return false;
126 |   },
127 | 
128 |   _onCodeChange: function() {
129 |     var script = this.refs.qe_planout_source.getDOMNode().value;
130 |     PlanOutExperimentActions.compile(script);
131 |   }
132 | });
133 | 
134 | module.exports = PlanOutScriptPanel;
135 | 


--------------------------------------------------------------------------------
/planout-editor/js/components/PlanOutTesterBox.react.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Copyright 2014 Facebook, Inc.
  3 |  *
  4 |  * @providesModule PlanOutTesterBox.react
  5 |  * @jsx React.DOM
  6 |  */
  7 | 
  8 | var React = require('react');
  9 | var ReactPropTypes = React.PropTypes;
 10 | 
 11 | var PlanOutTesterActions = require('../actions/PlanOutTesterActions');
 12 | var PlanOutTesterBoxForm = require('./PlanOutTesterBoxForm.react');
 13 | var PlanOutTesterBoxOutput = require('./PlanOutTesterBoxOutput.react');
 14 | 
 15 | var PlanOutEditorConstants = require('../constants/PlanOutEditorConstants');
 16 | var TesterBoxTypes = PlanOutEditorConstants.TesterBoxTypes;
 17 | var TesterStatusCodes = PlanOutEditorConstants.TesterStatusCodes;
 18 | 
 19 | //var Bootstrap = require('react-bootstrap');
 20 | //var Panel = Bootstrap.Panel;
 21 | 
 22 | 
 23 | var PlanOutTesterBox = React.createClass({
 24 |   propTypes: {
 25 |     id: ReactPropTypes.string.isRequired,
 26 |     assertions: ReactPropTypes.object,
 27 |     errors: ReactPropTypes.array,
 28 |     inputs: ReactPropTypes.object,
 29 |     overrides: ReactPropTypes.object,
 30 |     results: ReactPropTypes.object,
 31 |     status: ReactPropTypes.string.isRequired,
 32 |     type: ReactPropTypes.string.isRequired
 33 |   },
 34 | 
 35 |   getInitialState: function() {
 36 |     return {expanded: this.props.type === TesterBoxTypes.PLAYGROUND};
 37 |   },
 38 | 
 39 |   _toggleExpand: function() {
 40 |     this.setState({expanded: !this.state.expanded});
 41 |   },
 42 | 
 43 |   _destroy: function() {
 44 |     PlanOutTesterActions.destroy(this.props.id);
 45 |   },
 46 | 
 47 |   render: function() {
 48 |     var titleBarSettings = this.getTitleBarStrings();
 49 | 
 50 |     var collapse_class = "panel-collapse collapse";
 51 |     collapse_class += this.state.expanded ? " in" : "";
 52 | 
 53 |     return (
 54 |       <div>
 55 |       <div className={"panel " + titleBarSettings.panel_style}>
 56 |       <div className="panel-heading">
 57 |         <h4 className="panel-title">
 58 |           <a href={'#'+this.props.id} onClick={this._toggleExpand}>
 59 |            {titleBarSettings.title}
 60 |           </a>
 61 |           &nbsp;
 62 |           {this.renderDestroy()}
 63 |         </h4>
 64 |       </div>
 65 |       <div id={this.props.id} className={collapse_class}>
 66 |         <div className="panel-body">
 67 |           {this.renderPlanOutTesterBox()}
 68 |         </div>
 69 |       </div>
 70 |       </div>
 71 |       </div>
 72 |     );
 73 |     /*
 74 |       When react-bootstrap becomes more stable, the above will be replaced with
 75 | 
 76 |       return (
 77 |         <Panel
 78 |           header={titleBarSettings.title}
 79 |           bsStyle={titleBarSettings.panel_style}
 80 |           collapsable={true}
 81 |           expanded={this.state.expanded}
 82 |           onSelect={this._toggleExpand}>
 83 |           {this.renderPlanOutTesterBox()}
 84 |         </Panel>
 85 |       );
 86 |     */
 87 |   },
 88 | 
 89 |   renderPlanOutTesterBox: function() {
 90 |     return (
 91 |       <div>
 92 |         <PlanOutTesterBoxForm
 93 |          id={this.props.id}
 94 |          inputs={this.props.inputs}
 95 |          overrides={this.props.overrides}
 96 |          assertions={this.props.assertions}
 97 |          type={this.props.type}
 98 |         />
 99 |         <PlanOutTesterBoxOutput
100 |          errors={this.props.errors}
101 |          results={this.props.results}
102 |         />
103 |       </div>
104 |     );
105 |   },
106 | 
107 |   renderDestroy: function() {
108 |     // playground cannot be nuked
109 |     if (this.props.type !== TesterBoxTypes.PLAYGROUND) {
110 |       return <a href="#" title="Delete test" onClick={this._destroy}>[x]</a>;
111 |     } else {
112 |       return;
113 |     }
114 |   },
115 | 
116 |   // generate strings for accordian panel title
117 |   getTitleBarStrings: function() {
118 |     var title;
119 |     switch(this.props.status) {
120 |       case TesterStatusCodes.INVALID_FORM:
121 |         return {
122 |           panel_style: "panel-warning",
123 |           title: "Invalid JSON input"
124 |         };
125 |       case TesterStatusCodes.PENDING:
126 |         return {
127 |           panel_style: "panel-warning",
128 |           title: "Pending results..."
129 |         }
130 |       case TesterStatusCodes.FAILURE:
131 |         if (this.props.errors[0].error_code === 'runtime') {
132 |           title = "Runtime error";
133 |         } else if (this.props.errors[0].error_code === 'assertion') {
134 |           title = "Failed assertion";
135 |         } else {
136 |           title = "Unknown other error";
137 |           console.log("Unknown errors:", this.props.errors);
138 |         }
139 |         return {
140 |           panel_style: "panel-danger",
141 |           title: "Test status: " + title
142 |         };
143 |       case TesterStatusCodes.SUCCESS:
144 |         if (this.props.type === TesterBoxTypes.TEST) {
145 |           return {
146 |             panel_style: "panel-success",
147 |             title: "Test status: Success"};
148 |         } else {
149 |           return {
150 |             panel_style: "panel-default",
151 |             title: "Playground"
152 |           };
153 |         }
154 |       default:
155 |         console.log("Unknown status code:", this.props.status);
156 |     }
157 |   }
158 | });
159 | 
160 | 
161 | module.exports = PlanOutTesterBox;
162 | 


--------------------------------------------------------------------------------
/planout-editor/js/components/PlanOutTesterBoxForm.react.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Copyright 2014 Facebook, Inc.
  3 |  *
  4 |  * @providesModule PlanOutTesterBoxForm.react
  5 |  * @jsx React.DOM
  6 |  */
  7 | 
  8 | var React = require('react');
  9 | var ReactPropTypes = React.PropTypes;
 10 | var Input = require('react-bootstrap/Input');
 11 | 
 12 | var PlanOutTesterActions = require('../actions/PlanOutTesterActions');
 13 | 
 14 | var PlanOutEditorConstants = require('../constants/PlanOutEditorConstants');
 15 | var TesterStatusCodes = PlanOutEditorConstants.TesterStatusCodes;
 16 | var TesterBoxTypes = PlanOutEditorConstants.TesterBoxTypes;
 17 | 
 18 | var PlanOutTesterBoxFormInput = require('./PlanOutTesterBoxFormInput.react');
 19 | 
 20 | 
 21 | var PlanOutTesterBoxForm = React.createClass({
 22 |   propTypes: {
 23 |    id: ReactPropTypes.string.isRequired,
 24 |    assertions: ReactPropTypes.object,
 25 |    inputs: ReactPropTypes.object,
 26 |    overrides: ReactPropTypes.object,
 27 |    type: ReactPropTypes.string.isRequired
 28 |   },
 29 | 
 30 |   render: function() {
 31 |     return (
 32 |       <div>
 33 |         <div className="input-group">
 34 |           {this.renderInputItem("Inputs", "inputs")}
 35 |           {this.renderInputItem("Overrides", "overrides")}
 36 |           {
 37 |             this.props.type === TesterBoxTypes.TEST ?
 38 |             this.renderInputItem("Assertions", "assertions") : null
 39 |           }
 40 |         </div>
 41 |       </div>
 42 |     );
 43 |   },
 44 | 
 45 |   renderInputItem: function(label, prop) {
 46 |     return (
 47 |       <PlanOutTesterBoxFormInput
 48 |         json={this.props[prop]}
 49 |         label={label}
 50 |         id={this.props.id}
 51 |         ref={prop}
 52 |         fieldName={prop}/>
 53 |     );
 54 |   },
 55 | 
 56 | 
 57 |   // Parses JSON-encoded form element strings and returns object
 58 |   // containing each element: inputs, overrides, assertions
 59 |   // May be subbed out for other extract
 60 |   extractItemData: function() {
 61 |     var jsonBlob = {
 62 |       inputs: this.refs.inputs.getJSON(),
 63 |       overrides: this.refs.overrides.getJSON()
 64 |     };
 65 |     if (this.props.type === TesterBoxTypes.TEST) {
 66 |       jsonBlob.assertions = this.refs.assertions.getJSON();
 67 |     }
 68 | 
 69 |     for (var key in jsonBlob) {
 70 |       if (jsonBlob[key] === null) {
 71 |         return null;
 72 |       }
 73 |     }
 74 |     return jsonBlob;
 75 |   },
 76 | 
 77 |   _updateJSON: function(event, ref) {
 78 |     this.refs[ref].updateJSON(event.target.value);
 79 |   },
 80 | 
 81 |   _onChange: function(event, ref) {
 82 |                /*
 83 |     var payload = {};
 84 |     payload[ref] = this.refs[ref].getJSON();
 85 |     if (payload[ref]) {
 86 |        PlanOutTesterActions.updateTester(
 87 |          this.props.id,
 88 |          payload
 89 |        );
 90 |     }
 91 |     */
 92 | 
 93 | 
 94 |     var itemData = this.extractItemData();
 95 |     // should probably add more granular checks to make sure
 96 |     // input data is given. these checks may not be necessary
 97 |     // once we move to better form UI components
 98 |     if (itemData) {
 99 |       PlanOutTesterActions.updateTester(
100 |         this.props.id,
101 |         {
102 |           inputs: itemData.inputs,
103 |           overrides: itemData.overrides,
104 |           assertions: itemData.assertions
105 |         }
106 |       );
107 |     }
108 |   }
109 | });
110 | 
111 | module.exports = PlanOutTesterBoxForm;
112 | 


--------------------------------------------------------------------------------
/planout-editor/js/components/PlanOutTesterBoxFormInput.react.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Copyright 2014 Facebook, Inc.
 3 |  *
 4 |  * @providesModule PlanOutTesterBoxFormInput.react
 5 |  * @jsx React.DOM
 6 |  */
 7 | 
 8 | 
 9 | var React = require('react');
10 | var ReactPropTypes = React.PropTypes;
11 | var Input = require('react-bootstrap/Input');
12 | 
13 | var PlanOutTesterActions = require('../actions/PlanOutTesterActions');
14 | 
15 | var PlanOutTesterBoxFormInput = React.createClass({
16 |   propTypes: {
17 |     json: React.PropTypes.object,
18 |     fieldName: React.PropTypes.string.isRequired,
19 |     id: React.PropTypes.string.isRequired,
20 |     label: React.PropTypes.string.isRequired
21 |   },
22 | 
23 |   getInitialState: function() {
24 |     // value can take on any value users types into the textarea
25 |     // json only gets updated when this value is valid JSON
26 |     return {
27 |       isValid: true,
28 |       inFocusValue: JSON.stringify(this.props.json || {}, null, ' '),
29 |       inFocus: false
30 |     };
31 |   },
32 | 
33 |   _onChange: function(event) {
34 |     var value = event.target.value;
35 |     try {
36 |       var payload = {};
37 |       payload[this.props.fieldName] = JSON.parse(value);
38 |       this.setState({isValid: true, inFocusValue: value});
39 |       PlanOutTesterActions.updateTester(
40 |          this.props.id,
41 |          payload
42 |       );
43 |     } catch (e) {
44 |       this.setState({isValid: false, inFocusValue: value});
45 |     }
46 |   },
47 | 
48 |   getDisplayedValue: function() {
49 |     if (this.state.inFocus) {
50 |       return this.state.inFocusValue;
51 |     }
52 |     if (!this.state.isValid) {
53 |       return this.state.inFocusValue;
54 |     }
55 |     return JSON.stringify(this.props.json, null, " ");
56 |   },
57 | 
58 |   _onBlur: function () {
59 |     var payload = {inFocus: false};
60 |     if (this.state.isValid) {
61 |       payload.inFocusValue = JSON.stringify(this.props.json, null, " ");
62 |     }
63 |     this.setState(payload);
64 |   },
65 | 
66 |   _onFocus: function () {
67 |     var payload = {inFocus: true};
68 |     if (this.state.isValid) {
69 |       payload.inFocusValue = JSON.stringify(this.props.json, null, " ");
70 |     }
71 |     this.setState(payload);
72 |   },
73 | 
74 |   jsonHeight: function() {
75 |     return 20 + 20 * ((this.getDisplayedValue() || '').split('\n').length);
76 |   },
77 | 
78 |   render: function() {
79 |     return (
80 |       <Input type="textarea"
81 |        value={this.getDisplayedValue()}
82 |        addonBefore={this.props.label}
83 |        onChange={this._onChange}
84 |        bsStyle={this.state.isValid ? "success" : "error"}
85 |        onBlur={this._onBlur}
86 |        onFocus={this._onFocus}
87 |        help={this.state.isValid ? null : "Invalid JSON"}
88 |        style={{height: this.jsonHeight()}}/>
89 |     );
90 |   }
91 | });
92 | 
93 | module.exports = PlanOutTesterBoxFormInput;
94 | 


--------------------------------------------------------------------------------
/planout-editor/js/components/PlanOutTesterBoxOutput.react.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Copyright 2013-2014 Facebook, Inc.
 3 |  *
 4 |  * PlanOutTesterBoxOutput
 5 |  *
 6 |  * @jsx React.DOM
 7 |  */
 8 | 
 9 | var React = require('react');
10 | var ReactPropTypes = React.PropTypes;
11 | 
12 | 
13 | var PlanOutTesterBoxOutput = React.createClass({
14 |   propTypes: {
15 |     errors: ReactPropTypes.array,
16 |     results: ReactPropTypes.object
17 |   },
18 | 
19 |   render: function() {
20 |     var renderErrorMessage = function(error_message) {
21 |       if ('expected' in error_message) {
22 |         // expected param value assertion always contains expected_value
23 |         return (
24 |           <span>
25 |             Expecting
26 |             <code>{error_message.param}</code> to be
27 |             <code>{error_message.expected}</code> but got
28 |             <code>{error_message.got}</code> instead.
29 |           </span>
30 |         );
31 |       } else {
32 |         // otherwise we are missing an expected key
33 |         return (
34 |           <span>
35 |             Expecting to see a parameter named <code>{error_message.param}</code>,
36 |             but no such parameter could be found in the output.
37 |           </span>);
38 |       }
39 |     };
40 | 
41 |     var my_string = JSON.stringify(this.props.results, null, " ");
42 |     var outputObject;
43 |     if (this.props.errors && this.props.errors.length>0) {
44 |       var firstError = this.props.errors[0];
45 |       if(firstError.error_code !== "assertion") {
46 |         outputObject = <pre className="error">{firstError.message}</pre>;
47 |       } else {
48 |         var n = 0;
49 |         var rows = this.props.errors.map(function(row) {
50 |           return (
51 |             <tr key={"error-row-" + n++}>
52 |               <td>{renderErrorMessage(row.message)}</td>
53 |             </tr>
54 |           )
55 |         }.bind(this));
56 |         outputObject = (
57 |           <div>
58 |             <table className="table table-bordered">
59 |             <tbody>
60 |               {rows}
61 |             </tbody>
62 |             </table>
63 |             <pre className="error">{my_string}</pre>
64 |           </div>
65 |         );
66 |       }
67 |     } else {
68 |       outputObject = <pre className="correct">{my_string}</pre>;
69 |     }
70 |     return (
71 |       <div className="outputBox">
72 |         {outputObject}
73 |       </div>
74 |     );
75 |   }
76 | });
77 | 
78 | module.exports = PlanOutTesterBoxOutput;
79 | 


--------------------------------------------------------------------------------
/planout-editor/js/components/PlanOutTesterPanel.react.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Copyright 2014 Facebook, Inc.
 3 |  *
 4 |  * @providesModule PlanOutTesterPanel.react
 5 |  * @jsx React.DOM
 6 |  */
 7 | 
 8 | var React = require('react');
 9 | 
10 | var PlanOutTesterActions = require('../actions/PlanOutTesterActions');
11 | var PlanOutTesterBox = require('./PlanOutTesterBox.react');
12 | var PlanOutTesterStore = require('../stores/PlanOutTesterStore');
13 | 
14 | //var Bootstrap = require('react-bootstrap');
15 | //var Accordion = Bootstrap.Accordion;
16 | 
17 | 
18 | function getStateFromStores() {
19 |   // will ventually also get data from PlanOutExperimentStore
20 |   return {
21 |     tests: PlanOutTesterStore.getAllTests()
22 |   };
23 | }
24 | 
25 | var PlanOutTesterPanel = React.createClass({
26 | 
27 |   getInitialState: function() {
28 |     PlanOutTesterActions.init();
29 |     var state_data = getStateFromStores();
30 |     //state_data.expandedTest = Object.keys(state_data)[0];
31 |     return state_data;
32 |   },
33 | 
34 |   componentDidMount: function() {
35 |     PlanOutTesterStore.addChangeListener(this._onChange);
36 |   },
37 | 
38 |   componentWillUnmount: function() {
39 |     PlanOutTesterStore.removeChangeListener(this._onChange);
40 |   },
41 | 
42 |   _onChange: function() {
43 |     this.setState(getStateFromStores());
44 |   },
45 | 
46 |   render: function() {
47 |     var boxes = this.state.tests.map(function(test) {
48 |       return (
49 |         <PlanOutTesterBox
50 |           url="tester"
51 |           key={test.id}
52 |           id={test.id}
53 |           inputs={test.inputs}
54 |           overrides={test.overrides}
55 |           status={test.status}
56 |           assertions={test.assertions}
57 |           results={test.results}
58 |           errors={test.errors}
59 |           type={test.type}
60 |         />
61 |       );
62 |     }, this);
63 | 
64 |     return (
65 |       <div>
66 |         <div className="panel-group" id="accordion">
67 |           {boxes}
68 |         </div>
69 |         <button type="button" className="btn btn-default"
70 |           id="#addTest"
71 |           onClick={this._onAddPlanOutTesterBoxEvent}>
72 |           Add test
73 |         </button>
74 |         <input
75 |           type="hidden"
76 |           name="serialized_tests"
77 |           value={JSON.stringify(PlanOutTesterStore.getAllTests())}
78 |         />
79 |       </div>
80 |     );
81 |   },
82 | 
83 |   _onAddPlanOutTesterBoxEvent: function() {
84 |     PlanOutTesterActions.create();
85 |   },
86 | });
87 | 
88 | 
89 | module.exports = PlanOutTesterPanel;
90 | 


--------------------------------------------------------------------------------
/planout-editor/js/constants/PlanOutEditorConstants.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Copyright 2013-2014 Facebook, Inc.
 3 |  *
 4 |  * Licensed under the Apache License, Version 2.0 (the "License");
 5 |  * you may not use this file except in compliance with the License.
 6 |  * You may obtain a copy of the License at
 7 |  *
 8 |  * http://www.apache.org/licenses/LICENSE-2.0
 9 |  *
10 |  * Unless required by applicable law or agreed to in writing, software
11 |  * distributed under the License is distributed on an "AS IS" BASIS,
12 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 |  * See the License for the specific language governing permissions and
14 |  * limitations under the License.
15 |  *
16 |  * PlanOutEditorConstants
17 |  */
18 | 
19 | var keyMirror = require('react/lib/keyMirror');
20 | 
21 | module.exports = {
22 | 
23 |   ActionTypes: keyMirror({
24 |     /**
25 |      * Constants for actions
26 |      *
27 |      */
28 | 
29 |     // Creating and destroying PlanOutTesterBoxes
30 |     CREATE_TESTER: null,
31 |     LOAD_SERIALIZED_TESTERS: null,
32 |     INIT_TESTER_PANEL: null,
33 |     TESTER_DESTROY: null,
34 | 
35 |     // Updating PlanOutTesterBox states
36 |     TESTER_USER_UPDATE_TEST: null,
37 |     TESTER_SERVER_UPDATE_TEST: null,
38 |     TESTER_INVALID_TEST_FORM: null,
39 |     TESTER_REFRESH_TEST: null,
40 |     TESTER_REFRESH_ALL_TESTS: null,
41 | 
42 |     // Code Editor / compilation related actions
43 |     EDITOR_LOAD_SCRIPT: null,
44 |     EDITOR_COMPILE_SCRIPT: null,
45 |     EDITOR_UPDATE_COMPILED_CODE: null
46 |   }),
47 | 
48 |   TesterStatusCodes: keyMirror({
49 |     SUCCESS: null,
50 |     FAILURE: null,
51 |     INVALID_FORM: null,
52 |     PENDING: null
53 |   }),
54 | 
55 |   TesterBoxTypes: keyMirror({
56 |     TEST: null,
57 |     PLAYGROUND: null
58 |   })
59 | };
60 | 


--------------------------------------------------------------------------------
/planout-editor/js/dispatcher/PlanOutEditorDispatcher.js:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * Copyright (c) 2014, Facebook, Inc.
 3 |  * All rights reserved.
 4 |  *
 5 |  * This source code is licensed under the BSD-style license found in the
 6 |  * LICENSE file in the root directory of this source tree. An additional grant
 7 |  * of patent rights can be found in the PATENTS file in the same directory.
 8 |  *
 9 |  * PlanOutEditorDispatcher
10 |  *
11 |  * A singleton that operates as the central hub for application updates.
12 |  */
13 | 
14 | var Dispatcher = require('flux').Dispatcher;
15 | var assign = require('object-assign');
16 | 
17 | var PlanOutEditorDispatcher = assign(new Dispatcher(), {
18 | 
19 |   /**
20 |    * A bridge function between the views and the dispatcher, marking the action
21 |    * as a view action.  Another variant here could be handleServerAction.
22 |    * @param  {object} action The data coming from the view.
23 |    */
24 |   handleViewAction: function(/*object*/ action) {
25 |     this.dispatch({
26 |       source: 'VIEW_ACTION',
27 |       action: action
28 |     });
29 |   }
30 | 
31 | });
32 | 
33 | module.exports = PlanOutEditorDispatcher;
34 | 


--------------------------------------------------------------------------------
/planout-editor/js/stores/PlanOutExperimentStore.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Copyright 2013-2014 Facebook, Inc.
  3 |  *
  4 |  * PlanOutExperimentStore
  5 |  */
  6 | 
  7 | var _ = require('underscore');
  8 | var EventEmitter = require('events').EventEmitter;
  9 | var assign = require('object-assign');
 10 | 
 11 | var PlanOutAsyncRequests = require('../utils/PlanOutAsyncRequests');
 12 | var PlanOutEditorConstants = require('../constants/PlanOutEditorConstants');
 13 | var PlanOutEditorDispatcher = require('../dispatcher/PlanOutEditorDispatcher');
 14 | var ActionTypes = PlanOutEditorConstants.ActionTypes;
 15 | var PlanOutStaticAnalyzer = require('../utils/PlanOutStaticAnalyzer');
 16 | 
 17 | var CHANGE_EVENT = 'change';
 18 | 
 19 | var _script = '';
 20 | var _json = {};
 21 | var _compilation_status = 'success';
 22 | 
 23 | 
 24 | var PlanOutExperimentStore = assign({}, EventEmitter.prototype, {
 25 |   getScript: function() /*string*/ {
 26 |     return _script;
 27 |   },
 28 | 
 29 |   getJSON: function() /*object*/ {
 30 |     return _json;
 31 |   },
 32 | 
 33 |   getInputVariables: function() /*array*/ {
 34 |     return PlanOutStaticAnalyzer.inputVariables(_json);
 35 |   },
 36 | 
 37 |   getParams: function() /*array*/ {
 38 |     return PlanOutStaticAnalyzer.params(_json);
 39 |   },
 40 | 
 41 |   getCompilerMessage: function() /*string*/ {
 42 |     return _compilation_status === 'success' ?
 43 |       'Compilation successful!' : _compilation_status;
 44 |   },
 45 | 
 46 |   doesCompile: function() /*bool*/ {
 47 |     return _compilation_status === 'success';
 48 |   },
 49 | 
 50 |   emitChange: function() {
 51 |     this.emit(CHANGE_EVENT);
 52 |   },
 53 | 
 54 |   /**
 55 |    * @param {function} callback
 56 |    */
 57 |   addChangeListener: function(/*function*/ callback) {
 58 |     this.addListener(CHANGE_EVENT, callback);
 59 |   },
 60 | 
 61 |   /**
 62 |    * @param {function} callback
 63 |    */
 64 |   removeChangeListener: function(/*function*/ callback) {
 65 |     this.removeListener(CHANGE_EVENT, callback);
 66 |   },
 67 | 
 68 |   dispatchToken: PlanOutEditorDispatcher.register(
 69 |     function(/*object*/ payload)
 70 |   {
 71 |     var action = payload.action;
 72 | 
 73 |     switch(action.actionType) {
 74 |       case ActionTypes.EDITOR_COMPILE_SCRIPT:
 75 |         _script = action.script;
 76 |         if (action.script.trim() === "") {
 77 |           _json = {};
 78 |           _compilation_status = 'success';
 79 |           break;
 80 |         }
 81 |         PlanOutAsyncRequests.compileScript(action.script);
 82 |         break;
 83 | 
 84 |       case ActionTypes.EDITOR_LOAD_SCRIPT:
 85 |         _script = action.script;
 86 |         PlanOutAsyncRequests.compileScript(action.script);
 87 |         break;
 88 | 
 89 |       case ActionTypes.EDITOR_UPDATE_COMPILED_CODE:
 90 |         _json = action.json;
 91 |         _compilation_status = action.status;
 92 |         break;
 93 | 
 94 |       default:
 95 |         // no change needed to emit
 96 |         return true;
 97 |     }
 98 | 
 99 |     PlanOutExperimentStore.emitChange();
100 |     return true;
101 |   })
102 | });
103 | 
104 | module.exports = PlanOutExperimentStore;
105 | 


--------------------------------------------------------------------------------
/planout-editor/js/stores/PlanOutTesterStore.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Copyright 2013-2014 Facebook, Inc.
  3 |  *
  4 |  * PlanOutTesterStore
  5 |  * @jsx
  6 |  */
  7 | 
  8 | var _ = require('underscore');
  9 | var EventEmitter = require('events').EventEmitter;
 10 | var assign = require('object-assign');
 11 | 
 12 | var PlanOutAsyncRequests = require('../utils/PlanOutAsyncRequests');
 13 | var PlanOutEditorDispatcher = require('../dispatcher/PlanOutEditorDispatcher');
 14 | var PlanOutExperimentStore = require('../stores/PlanOutExperimentStore');
 15 | 
 16 | var PlanOutEditorConstants = require('../constants/PlanOutEditorConstants');
 17 | var ActionTypes = PlanOutEditorConstants.ActionTypes;
 18 | var TesterStatusCodes = PlanOutEditorConstants.TesterStatusCodes;
 19 | var TesterBoxTypes = PlanOutEditorConstants.TesterBoxTypes;
 20 | 
 21 | 
 22 | var CHANGE_EVENT = 'change';
 23 | var DEFAULT_INPUTS = {'userid': 4}; // default when units cannot be inferred
 24 | 
 25 | 
 26 | var _tests = {};      // indexed set of tests
 27 | var _test_ids = [];   // ids giving the order of tests
 28 | 
 29 | 
 30 | function _createTestBox(testType) {
 31 |   PlanOutEditorDispatcher.waitFor([PlanOutExperimentStore.dispatchToken]);
 32 |   if (PlanOutExperimentStore.doesCompile()) {
 33 | 
 34 |     // generate input blob with reasonable random defaults based on which
 35 |     // variables are used but not set by the PlanOut script
 36 |     var inferred_input_variables = PlanOutExperimentStore.getInputVariables();
 37 | 
 38 |     var input_dict = {};
 39 |     /*
 40 |     if (inferred_input_variables.length > 0) {
 41 |       for(var i = 0; i < inferred_input_variables.length; i++) {
 42 |         input_dict[inferred_input_variables[i]] =
 43 |           Math.floor((Math.random() * 1000));
 44 |       }
 45 |     } else {
 46 |       input_dict = _.clone(DEFAULT_INPUTS);
 47 |     }
 48 |     */
 49 | 
 50 |     // note that action.assertions may be undefined when you have a playground
 51 |     var id = 'test-' + Date.now();
 52 |     _tests[id] = {
 53 |       id: id,
 54 |       inputs: input_dict,
 55 |       overrides: undefined,
 56 |       assertions: {},
 57 |       status: TesterStatusCodes.PENDING,
 58 |       errors: undefined,
 59 |       results: undefined,
 60 |       type: testType,
 61 |       run: PlanOutAsyncRequests.genRunner(id)
 62 |     };
 63 |     _test_ids.push(id);
 64 |     _refreshTest(id);
 65 |   }
 66 | }
 67 | 
 68 | function _createPlayground() {
 69 |   _createTestBox(TesterBoxTypes.PLAYGROUND);
 70 | }
 71 | 
 72 | function _createTest() {
 73 |   _createTestBox(TesterBoxTypes.TEST);
 74 | }
 75 | 
 76 | /**
 77 |  * Update a TESTER item.
 78 |  * @param  {string} id
 79 |  * @param {object} updates An object literal containing only the data to be
 80 |  *     updated.
 81 |  */
 82 | function _update(/*string*/ id, /*object*/ updates) {
 83 |   _tests[id] = assign({}, _tests[id], updates);
 84 | }
 85 | 
 86 | /**
 87 |  * Delete a TEST item.
 88 |  * @param  {string} id
 89 |  */
 90 | function _destroy(/*string*/ id) {
 91 |   _test_ids = _test_ids.filter(function(x) {return x !== id;});
 92 |   delete _tests[id];
 93 | }
 94 | 
 95 | function _getScrubbedInputs(/*string*/ id) {
 96 |   var input_variables = PlanOutExperimentStore.getInputVariables();
 97 |   var inputs = _tests[id].inputs || {};
 98 |   for (var key in inputs) {
 99 |     if (inputs[key] === null) {
100 |       delete inputs[key];
101 |     }
102 |   }
103 |   input_variables.forEach(function(key) {
104 |     if (!inputs.hasOwnProperty(key)) {
105 |       inputs[key] = null;
106 |     }
107 |   });
108 |   return inputs;
109 | }
110 | 
111 | function _refreshTest(/*string*/ id) {
112 |   if (PlanOutExperimentStore.doesCompile()) {
113 |     var request = assign({},
114 |       _tests[id],
115 |       {compiled_code: PlanOutExperimentStore.getJSON()}
116 |     );
117 |     _tests[id].inputs = _getScrubbedInputs(id);
118 |     _tests[id].run(request);
119 |   }
120 | }
121 | 
122 | function _refreshAllTests() {
123 |   for (var id in _tests) {
124 |     _refreshTest(id);
125 |   }
126 | }
127 | 
128 | function _getTestArray() /*array*/ {
129 |     return _test_ids.map(function(id) {return _tests[id];});
130 | }
131 | 
132 | var PlanOutTesterStore = assign({}, EventEmitter.prototype, {
133 | 
134 |   getAllTests: function() /*array*/ {
135 |     return _getTestArray();
136 |   },
137 | 
138 |   getSerializedTests: function() /*array*/ {
139 |     return _getTestArray().map(
140 |       function(x) {
141 |         return _.pick(x, ['id', 'inputs', 'overrides', 'assertions', 'type']);
142 |       }
143 |     );
144 |   },
145 | 
146 |   /**
147 |    * Tests whether all the remaining TESTER items are marked as completed.
148 |    * @return {booleam}
149 |    */
150 |   areAllPassing: function() /*bool*/ {
151 |     for (var id in _tests) {
152 |       if (_tests[id].status !== TesterStatusCodes.SUCCESS) {
153 |         return false;
154 |       }
155 |     }
156 |     return true;
157 |   },
158 | 
159 |   emitChange: function() {
160 |     this.emit(CHANGE_EVENT);
161 |   },
162 | 
163 |   /**
164 |    * @param {function} callback
165 |    */
166 |   addChangeListener: function(/*function*/ callback) {
167 |     this.addListener(CHANGE_EVENT, callback);
168 |   },
169 | 
170 |   /**
171 |    * @param {function} callback
172 |    */
173 |   removeChangeListener: function(/*function*/ callback) {
174 |     this.removeListener(CHANGE_EVENT, callback);
175 |   }
176 | });
177 | 
178 | // Register to handle all updates
179 | PlanOutTesterStore.dispatchToken =
180 |   PlanOutEditorDispatcher.register(function(/*object*/ payload) {
181 |   var action = payload.action;
182 | 
183 |   switch(action.actionType) {
184 |     case ActionTypes.INIT_TESTER_PANEL:
185 |       _createPlayground();
186 |       break;
187 | 
188 |     case ActionTypes.TESTER_CREATE:
189 |       _createTest();
190 |       break;
191 | 
192 |     /**
193 |      * Add multiple existing tests into TestStore.
194 |      * @param {test} object containing tester data
195 |      */
196 |     case ActionTypes.LOAD_SERIALIZED_TESTERS:
197 |       _tests = {};
198 |       _test_ids = [];
199 |       for (var i = 0; i < action.tests.length; i++)  {
200 |         var test = action.tests[i];
201 |         var id = test.id;
202 |         _tests[id] = {
203 |           id: test.id,
204 |           inputs: test.inputs,
205 |           overrides: test.overrides,
206 |           assertions: test.assertions,
207 |           status: TesterStatusCodes.PENDING,
208 |           type: test.type,
209 |           run: PlanOutAsyncRequests.genRunner(id)
210 |         };
211 |         _test_ids.push(id);
212 |       }
213 |       break;
214 | 
215 |     case ActionTypes.TESTER_USER_UPDATE_TEST:
216 |       _update(action.id, action.fieldsToUpdate);
217 |       PlanOutEditorDispatcher.waitFor([PlanOutExperimentStore.dispatchToken]);
218 |       _refreshTest(action.id);
219 |       break;
220 | 
221 |     // callback from server
222 |     case ActionTypes.TESTER_SERVER_UPDATE_TEST:
223 |       _update(action.id, {
224 |         status: (action.errors && action.errors.length > 0) ?
225 |                   TesterStatusCodes.FAILURE : TesterStatusCodes.SUCCESS,
226 |         errors: action.errors,
227 |         results: action.results
228 |       });
229 |       break;
230 | 
231 |     // User enters invalid input into form. Might be deprecated with better UI
232 |     // components
233 |     case ActionTypes.TESTER_INVALID_TEST_FORM:
234 |       _update(action.id, {
235 |         status: action.status
236 |       });
237 |       break;
238 | 
239 |     case ActionTypes.TESTER_DESTROY:
240 |       _destroy(action.id);
241 |       break;
242 | 
243 |     case ActionTypes.EDITOR_UPDATE_COMPILED_CODE:
244 |     case ActionTypes.TESTER_REFRESH_ALL_TESTS:
245 |       PlanOutEditorDispatcher.waitFor([PlanOutExperimentStore.dispatchToken]);
246 |       _refreshAllTests();
247 |       break;
248 | 
249 |     default:
250 |       // no change needed to emit
251 |       return true;
252 |   }
253 | 
254 |   PlanOutTesterStore.emitChange();
255 |   return true; // No errors.  Needed by promise in Dispatcher.
256 | });
257 | 
258 | 
259 | module.exports = PlanOutTesterStore;
260 | 


--------------------------------------------------------------------------------
/planout-editor/js/utils/DemoData.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Copyright 2013-2014 Facebook, Inc.
 3 |  *
 4 |  * DemoData
 5 |  */
 6 | 
 7 | var PlanOutEditorConstants = require('../constants/PlanOutEditorConstants');
 8 | var TesterBoxTypes = PlanOutEditorConstants.TesterBoxTypes;
 9 | 
10 | module.exports = {
11 |   getDemoTests: function() {
12 |     // this will eventually come from the server
13 |     var defaultTests = [
14 |       {
15 |         "id": "playground",
16 |         "inputs": {"userid": 42},
17 |         "type": TesterBoxTypes.PLAYGROUND
18 |       },
19 |       {
20 |         "id": "test3",
21 |         "inputs":{"userid":5243},
22 |         "overrides": {},
23 |         "assertions": {"ratings_goal":640},
24 |         "type": TesterBoxTypes.TEST
25 |       },
26 |       {
27 |         "id": "test2",
28 |         "inputs":{"userid":52433},
29 |         "overrides": {"group_size":10},
30 |         "assertions": {"ratings_goal": 200},
31 |         "type": TesterBoxTypes.TEST
32 |       },
33 |     ];
34 |     return defaultTests;
35 |   },
36 | 
37 |   getDemoScript: function() {
38 |     return [
39 |      "group_size = uniformChoice(choices=[1, 10], unit=userid);",
40 |      "specific_goal = bernoulliTrial(p=0.8, unit=userid);",
41 |      "if (specific_goal) {",
42 |      "  ratings_per_user_goal = uniformChoice(",
43 |      "    choices=[8, 16, 32, 64], unit=userid);",
44 |      "  ratings_goal = group_size * ratings_per_user_goal;",
45 |      "}"
46 |     ].join('\n');
47 |   }
48 | };
49 | 


--------------------------------------------------------------------------------
/planout-editor/js/utils/PlanOutAsyncRequests.js:
--------------------------------------------------------------------------------
  1 | /** Copyright (c) 2014, Facebook, Inc.
  2 |  * All rights reserved.
  3 |  *
  4 |  * This source code is licensed under the BSD-style license found in the
  5 |  * LICENSE file in the root directory of this source tree. An additional grant
  6 |  * of patent rights can be found in the PATENTS file in the same directory.
  7 |  */
  8 | 
  9 | var _ = require('underscore');
 10 | 
 11 | var PlanOutCompiler = require('./planout_compiler');
 12 | var FileSaver = require('./FileSaver');
 13 | 
 14 | var PlanOutTesterActions = require('../actions/PlanOutTesterActions');
 15 | var PlanOutExperimentActions = require('../actions/PlanOutExperimentActions');
 16 | 
 17 | var PlanOutEditorDispatcher = require('../dispatcher/PlanOutEditorDispatcher');
 18 | var PlanOutEditorConstants = require('../constants/PlanOutEditorConstants');
 19 | var TesterBoxTypes = PlanOutEditorConstants.TesterBoxTypes;
 20 | 
 21 | var ASYNC_DELAY = 250;
 22 | 
 23 | 
 24 | // NOTE: need to update python endpoint to return test id
 25 | function _runTest(/*string*/ id, /*object*/ updateBlob) /*bool*/  {
 26 |   var stringBlob = {};
 27 |   for (var key in updateBlob) {
 28 |     stringBlob[key] = JSON.stringify(updateBlob[key]);
 29 |   }
 30 |   $.ajax({
 31 |     url: 'run_test',
 32 |     dataType: 'json',
 33 |     data: stringBlob,
 34 |     success: function(data) {
 35 |       PlanOutTesterActions.updateTesterOutput(
 36 |         id,
 37 |         data.errors,
 38 |         data.results
 39 |       );
 40 |     }.bind(this),
 41 |     error: function(xhr, status, err) {
 42 |       console.error(this.props.url, status, err.toString());
 43 |       return false;
 44 |     }.bind(this)
 45 |   });
 46 |   return true;
 47 | }
 48 | 
 49 | module.exports = {
 50 | 
 51 |   getDemoTests: function() /*array*/  {
 52 |     // this will eventually come from the server
 53 |     var defaultTests = [
 54 |       {
 55 |         "id": "playground",
 56 |         "inputs": {"userid": 42},
 57 |         "type": TesterBoxTypes.PLAYGROUND
 58 |       },
 59 |       {
 60 |         "id": "test3",
 61 |         "inputs":{"userid":5243},
 62 |         "overrides": {},
 63 |         "assertions": {"ratings_goal":640},
 64 |         "type": TesterBoxTypes.TEST
 65 |       },
 66 |       {
 67 |         "id": "test2",
 68 |         "inputs":{"userid":52433},
 69 |         "overrides": {"group_size":10},
 70 |         "assertions": {"ratings_goal": 200},
 71 |         "type": TesterBoxTypes.TEST
 72 |       },
 73 |     ];
 74 |     return defaultTests;
 75 |     //at some point we should call something to update all tester output
 76 |   },
 77 | 
 78 |   getDemoScript: function() /*string*/  {
 79 |     return [
 80 |      "group_size = uniformChoice(choices=[1, 10], unit=userid);",
 81 |      "specific_goal = bernoulliTrial(p=0.8, unit=userid);",
 82 |      "if (specific_goal) {",
 83 |      "  ratings_per_user_goal = uniformChoice(",
 84 |      "    choices=[8, 16, 32, 64], unit=userid);",
 85 |      "  ratings_goal = group_size * ratings_per_user_goal;",
 86 |      "}"
 87 |     ].join('\n');
 88 |   },
 89 | 
 90 |   saveState: function(/*object*/ data, /*string*/ filename) {
 91 |     var blob = new Blob(
 92 |       [JSON.stringify(data, false, " ")],
 93 |       {type: "text/plain;charset=utf-8"}
 94 |     );
 95 |     FileSaver(blob, filename);
 96 |   },
 97 | 
 98 |   genRunner: function(/*string*/ id) /*function*/  {
 99 |     // generates a throttled function for each test id
100 |     return _.throttle(
101 |       function(updateBlob) {
102 |         _runTest(id, updateBlob);
103 |       },
104 |       ASYNC_DELAY
105 |     );
106 |   },
107 | 
108 |   compilerCallback: function(/*string*/ script) {
109 |     try {
110 |       // this can be subbed out for a callback if using server-side compilation
111 |       var json = PlanOutCompiler.parse(script);
112 |       PlanOutExperimentActions.updateCompiledCode(script, "success", json);
113 |     } catch (err) {
114 |       PlanOutExperimentActions.updateCompiledCode(script, err.message, {});
115 |     }
116 |   },
117 | 
118 |   compileScript: _.throttle(function(/*string*/ script) {
119 |     setTimeout((function() {
120 |       this.compilerCallback(script);
121 |     }).bind(this), 1);
122 |   }, ASYNC_DELAY)
123 | };
124 | 


--------------------------------------------------------------------------------
/planout-editor/js/utils/PlanOutStaticAnalyzer.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Copyright 2004-present Facebook. All Rights Reserved.
 3 |  *
 4 |  * @providesModule PlanOutStaticAnalyzer
 5 |  * @typechecks
 6 |  */
 7 | 
 8 | var _ = require('underscore');
 9 | 
10 | // code may be an object or array
11 | var _getVariables = function(code) /*object*/ {
12 |   if (!code || Object.keys(code).length === 0) {
13 |     return {'get_vars': [], 'set_vars': []};
14 |   }
15 |   var get_vars = {};
16 |   var set_vars = {};
17 |   var child_vars = {};
18 | 
19 |   var op = code.op;
20 |   if (op === 'set') {
21 |     set_vars[code['var']] = code.value;
22 |   } else if (op === 'get')  {
23 |     get_vars[code['var']] = code.value;
24 |   }
25 |   for (var key in code) {
26 |     if (typeof code[key] === 'object') {
27 |       child_vars = _getVariables(code[key]);
28 |       if (child_vars.get_vars) {
29 |         for (var ck in child_vars.get_vars) {
30 |           get_vars[ck] = child_vars.get_vars[ck];
31 |         }
32 |       }
33 |       if (child_vars.set_vars) {
34 |         for (var ck in child_vars.set_vars) {
35 |           set_vars[ck] = child_vars.set_vars[ck];
36 |         }
37 |       }
38 |     }
39 |   }
40 | 
41 |   return {'get_vars': get_vars, 'set_vars': set_vars};
42 | };
43 | 
44 | function _getParams(/*object*/ code) /*array*/ {
45 |   var vars = _getVariables(code);
46 |   return Object.keys(vars.set_vars);
47 | }
48 | 
49 | function _getParamValue(/*object*/ code, /*string*/ var_name) {
50 |   return _getVariables(code).set_vars[var_name];
51 | }
52 | 
53 | var PlanOutStaticAnalyzer = {
54 |   inputVariables: function(/*object*/ code) /*array*/ {
55 |     var vars = _getVariables(code);
56 |     var input_vars = [];
57 |     for (var v in vars.get_vars) {
58 |       if (!(v in vars.set_vars)) {
59 |         input_vars.push(v);
60 |       }
61 |     }
62 |     return input_vars;
63 |   },
64 | 
65 |   params: _getParams,
66 | 
67 |   getLoggedParams: function(/*object*/ code) /*array<string>*/ {
68 |     var loggedParams = _getParamValue(code, 'log');
69 |     // interpret array operators as literal arrays
70 |     if (loggedParams instanceof Object && loggedParams.op == 'array') {
71 |       loggedParams = loggedParams.values;
72 |     }
73 |     if (loggedParams) {
74 |       if (loggedParams instanceof Array) {
75 |         return _.intersection(
76 |           _getParams(code),
77 |           loggedParams.filter(function(x) {return typeof x === 'string';})
78 |         );
79 |       } else {
80 |         console.log(
81 |           'Error: PlanOut variable named log must be an array of strings, ' +
82 |           'but got ', loggedParams, ' instead.'
83 |         );
84 |         return [];
85 |       }
86 |     } else {
87 |       return _getParams(code);
88 |     }
89 |   }
90 | };
91 | 
92 | module.exports = PlanOutStaticAnalyzer;
93 | 


--------------------------------------------------------------------------------
/planout-editor/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "planout-editor",
 3 |   "version": "0.0.1",
 4 |   "description": "Reactive PlanOut interface",
 5 |   "main": "js/app.js",
 6 |   "dependencies": {
 7 |     "es6-promise": ">=0.1.1",
 8 |     "underscore": "~1.6.0",
 9 |     "codemirror": "~4.4.0",
10 |     "flux": "^2.0.0",
11 |     "keymirror": "~0.1.0",
12 |     "object-assign": "^1.0.0",
13 |     "react": "^0.12.0",
14 |     "react-bootstrap": "^0.13.0"
15 |   },
16 |   "devDependencies": {
17 |     "browserify": ">=2.36.0",
18 |     "envify": ">=1.2.0",
19 |     "reactify": ">=0.4.0",
20 |     "statics": ">=0.1.0",
21 |     "uglify-js": ">=2.4.13",
22 |     "watchify": ">=0.4.1",
23 |     "jest-cli": "~0.1.5"
24 |   },
25 |   "scripts": {
26 |     "start": "STATIC_ROOT=./static watchify -o static/js/bundle.js -v -d .",
27 |     "build": "STATIC_ROOT=./static NODE_ENV=production browserify . | uglifyjs -cm > static/js/bundle.min.js",
28 |     "collect-static": "collect-static . ./static",
29 |     "test": "jest"
30 |   },
31 |   "author": "Eytan Bakshy",
32 |   "license": "BSD",
33 |   "browserify": {
34 |     "transform": [
35 |       "reactify",
36 |       "envify"
37 |     ]
38 |   },
39 |   "jest": {
40 |     "rootDir": "./js"
41 |   }
42 | }
43 | 


--------------------------------------------------------------------------------
/planout-editor/planout-editor-kernel.py:
--------------------------------------------------------------------------------
 1 | from flask import Flask, jsonify, render_template, request, url_for
 2 | app = Flask(__name__)
 3 | from planout.interpreter import Interpreter
 4 | import traceback
 5 | import json
 6 | import sys
 7 | 
 8 | def testPlanOutScript(script, inputs={}, overrides=None, assertions=None):
 9 |   payload = {}
10 | 
11 |   # make sure experiment runs with the given inputs
12 |   i = Interpreter(script, 'demo_salt', inputs)
13 |   if overrides:
14 |     i.set_overrides(overrides)
15 | 
16 |   try:
17 |     results = dict(i.get_params()) # executes experiment
18 |   except Exception as err:
19 |     #message = "Error running experiment: %s" % traceback.format_exc(0)
20 |     message = "Error running experiment:\n%s" % err
21 |     payload['errors'] = [{
22 |       "error_code": "runtime",
23 |       "message": message
24 |     }]
25 |     return payload
26 | 
27 |   payload['results'] = results
28 | 
29 |   # validate if input contains validation code
30 |   validation_errors = []
31 |   if assertions:
32 |     for (key, value) in assertions.iteritems():
33 |       if key not in results:
34 |         validation_errors.append({
35 |           "error_code": "assertion",
36 |           "message": {"param": key}
37 |         })
38 |       else:
39 |         if results[key] != value:
40 |           message = {'param': key, 'expected': value, 'got': results[key]}
41 |           validation_errors.append({
42 |             "error_code": "assertion",
43 |             "message": message
44 |           })
45 |     if validation_errors:
46 |       payload['errors'] = validation_errors
47 | 
48 |   return payload
49 | 
50 | 
51 | @app.route('/run_test')
52 | def run_test():
53 |   # not sure how to change everything to use POST requests
54 |   raw_script = request.args.get('compiled_code', '')
55 |   raw_inputs = request.args.get('inputs', '')
56 |   raw_overrides = request.args.get('overrides', "{}")
57 |   raw_assertions = request.args.get('assertions', "{}")
58 |   id = request.args.get('id')
59 | 
60 |   script = json.loads(raw_script) if raw_script else {}
61 |   try:
62 |     inputs = json.loads(raw_inputs)
63 |     overrides = json.loads(raw_overrides) if raw_overrides else None
64 |     assertions = json.loads(raw_assertions) if raw_assertions else None
65 |   except:
66 |     return jsonify({
67 |       'errors': [{
68 |         'error_code': "INVALID_FORM",
69 |         'message': 'Invalid form input'
70 |       }],
71 |       'id': id
72 |     })
73 | 
74 |   t = testPlanOutScript(script, inputs, overrides, assertions)
75 |   t['id'] = id
76 |   return jsonify(t)
77 | 
78 | @app.route('/')
79 | def index():
80 |   return render_template('index.html')
81 | 
82 | 
83 | if __name__ == '__main__':
84 |   app.run(debug=True)
85 |   url_for('static', filename='planoutstyle.css')
86 | 


--------------------------------------------------------------------------------
/planout-editor/requirements.txt:
--------------------------------------------------------------------------------
 1 | Flask==0.10.0
 2 | Jinja2==2.7.3
 3 | MarkupSafe==0.23
 4 | PlanOut==0.5
 5 | Werkzeug==0.9.6
 6 | gunicorn==18.0
 7 | itsdangerous==0.24
 8 | wsgiref==0.1.2
 9 | 
10 | 


--------------------------------------------------------------------------------
/planout-editor/static/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebookarchive/planout/eee764781054abb39f003133b00b88a73c8f8982/planout-editor/static/fonts/glyphicons-halflings-regular.eot


--------------------------------------------------------------------------------
/planout-editor/static/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebookarchive/planout/eee764781054abb39f003133b00b88a73c8f8982/planout-editor/static/fonts/glyphicons-halflings-regular.ttf


--------------------------------------------------------------------------------
/planout-editor/static/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebookarchive/planout/eee764781054abb39f003133b00b88a73c8f8982/planout-editor/static/fonts/glyphicons-halflings-regular.woff


--------------------------------------------------------------------------------
/planout-editor/static/js/mode/javascript/index.html:
--------------------------------------------------------------------------------
  1 | <!doctype html>
  2 | 
  3 | <title>CodeMirror: JavaScript mode</title>
  4 | <meta charset="utf-8"/>
  5 | <link rel=stylesheet href="../../doc/docs.css">
  6 | 
  7 | <link rel="stylesheet" href="../../lib/codemirror.css">
  8 | <script src="../../lib/codemirror.js"></script>
  9 | <script src="../../addon/edit/matchbrackets.js"></script>
 10 | <script src="../../addon/comment/continuecomment.js"></script>
 11 | <script src="../../addon/comment/comment.js"></script>
 12 | <script src="javascript.js"></script>
 13 | <style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
 14 | <div id=nav>
 15 |   <a href="http://codemirror.net"><img id=logo src="../../doc/logo.png"></a>
 16 | 
 17 |   <ul>
 18 |     <li><a href="../../index.html">Home</a>
 19 |     <li><a href="../../doc/manual.html">Manual</a>
 20 |     <li><a href="https://github.com/marijnh/codemirror">Code</a>
 21 |   </ul>
 22 |   <ul>
 23 |     <li><a href="../index.html">Language modes</a>
 24 |     <li><a class=active href="#">JavaScript</a>
 25 |   </ul>
 26 | </div>
 27 | 
 28 | <article>
 29 | <h2>JavaScript mode</h2>
 30 | 
 31 | 
 32 | <div><textarea id="code" name="code">
 33 | // Demo code (the actual new parser character stream implementation)
 34 | 
 35 | function StringStream(string) {
 36 |   this.pos = 0;
 37 |   this.string = string;
 38 | }
 39 | 
 40 | StringStream.prototype = {
 41 |   done: function() {return this.pos >= this.string.length;},
 42 |   peek: function() {return this.string.charAt(this.pos);},
 43 |   next: function() {
 44 |     if (this.pos &lt; this.string.length)
 45 |       return this.string.charAt(this.pos++);
 46 |   },
 47 |   eat: function(match) {
 48 |     var ch = this.string.charAt(this.pos);
 49 |     if (typeof match == "string") var ok = ch == match;
 50 |     else var ok = ch &amp;&amp; match.test ? match.test(ch) : match(ch);
 51 |     if (ok) {this.pos++; return ch;}
 52 |   },
 53 |   eatWhile: function(match) {
 54 |     var start = this.pos;
 55 |     while (this.eat(match));
 56 |     if (this.pos > start) return this.string.slice(start, this.pos);
 57 |   },
 58 |   backUp: function(n) {this.pos -= n;},
 59 |   column: function() {return this.pos;},
 60 |   eatSpace: function() {
 61 |     var start = this.pos;
 62 |     while (/\s/.test(this.string.charAt(this.pos))) this.pos++;
 63 |     return this.pos - start;
 64 |   },
 65 |   match: function(pattern, consume, caseInsensitive) {
 66 |     if (typeof pattern == "string") {
 67 |       function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
 68 |       if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) {
 69 |         if (consume !== false) this.pos += str.length;
 70 |         return true;
 71 |       }
 72 |     }
 73 |     else {
 74 |       var match = this.string.slice(this.pos).match(pattern);
 75 |       if (match &amp;&amp; consume !== false) this.pos += match[0].length;
 76 |       return match;
 77 |     }
 78 |   }
 79 | };
 80 | </textarea></div>
 81 | 
 82 |     <script>
 83 |       var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
 84 |         lineNumbers: true,
 85 |         matchBrackets: true,
 86 |         continueComments: "Enter",
 87 |         extraKeys: {"Ctrl-Q": "toggleComment"}
 88 |       });
 89 |     </script>
 90 | 
 91 |     <p>
 92 |       JavaScript mode supports several configuration options:
 93 |       <ul>
 94 |         <li><code>json</code> which will set the mode to expect JSON
 95 |         data rather than a JavaScript program.</li>
 96 |         <li><code>jsonld</code> which will set the mode to expect
 97 |         <a href="http://json-ld.org">JSON-LD</a> linked data rather
 98 |         than a JavaScript program (<a href="json-ld.html">demo</a>).</li>
 99 |         <li><code>typescript</code> which will activate additional
100 |         syntax highlighting and some other things for TypeScript code
101 |         (<a href="typescript.html">demo</a>).</li>
102 |         <li><code>statementIndent</code> which (given a number) will
103 |         determine the amount of indentation to use for statements
104 |         continued on a new line.</li>
105 |       </ul>
106 |     </p>
107 | 
108 |     <p><strong>MIME types defined:</strong> <code>text/javascript</code>, <code>application/json</code>, <code>application/ld+json</code>, <code>text/typescript</code>, <code>application/typescript</code>.</p>
109 |   </article>
110 | 


--------------------------------------------------------------------------------
/planout-editor/static/js/mode/javascript/json-ld.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | 
 3 | <title>CodeMirror: JSON-LD mode</title>
 4 | <meta charset="utf-8"/>
 5 | <link rel=stylesheet href="../../doc/docs.css">
 6 | 
 7 | <link rel="stylesheet" href="../../lib/codemirror.css">
 8 | <script src="../../lib/codemirror.js"></script>
 9 | <script src="../../addon/edit/matchbrackets.js"></script>
10 | <script src="../../addon/comment/continuecomment.js"></script>
11 | <script src="../../addon/comment/comment.js"></script>
12 | <script src="javascript.js"></script>
13 | <style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
14 | <div id="nav">
15 |   <a href="http://codemirror.net"><img id=logo src="../../doc/logo.png"/></a>
16 | 
17 |   <ul>
18 |     <li><a href="../../index.html">Home</a>
19 |     <li><a href="../../doc/manual.html">Manual</a>
20 |     <li><a href="https://github.com/marijnh/codemirror">Code</a>
21 |   </ul>
22 |   <ul>
23 |     <li><a href="../index.html">Language modes</a>
24 |     <li><a class=active href="#">JSON-LD</a>
25 |   </ul>
26 | </div>
27 | 
28 | <article>
29 | <h2>JSON-LD mode</h2>
30 | 
31 | 
32 | <div><textarea id="code" name="code">
33 | {
34 |   "@context": {
35 |     "name": "http://schema.org/name",
36 |     "description": "http://schema.org/description",
37 |     "image": {
38 |       "@id": "http://schema.org/image",
39 |       "@type": "@id"
40 |     },
41 |     "geo": "http://schema.org/geo",
42 |     "latitude": {
43 |       "@id": "http://schema.org/latitude",
44 |       "@type": "xsd:float"
45 |     },
46 |     "longitude": {
47 |       "@id": "http://schema.org/longitude",
48 |       "@type": "xsd:float"
49 |     },
50 |     "xsd": "http://www.w3.org/2001/XMLSchema#"
51 |   },
52 |   "name": "The Empire State Building",
53 |   "description": "The Empire State Building is a 102-story landmark in New York City.",
54 |   "image": "http://www.civil.usherbrooke.ca/cours/gci215a/empire-state-building.jpg",
55 |   "geo": {
56 |     "latitude": "40.75",
57 |     "longitude": "73.98"
58 |   }
59 | }
60 | </textarea></div>
61 | 
62 |     <script>
63 |       var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
64 |         matchBrackets: true,
65 |         autoCloseBrackets: true,
66 |         mode: "application/ld+json",
67 |         lineWrapping: true
68 |       });
69 |     </script>
70 |     
71 |     <p>This is a specialization of the <a href="index.html">JavaScript mode</a>.</p>
72 |   </article>
73 | 


--------------------------------------------------------------------------------
/planout-editor/static/js/mode/javascript/test.js:
--------------------------------------------------------------------------------
  1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others
  2 | // Distributed under an MIT license: http://codemirror.net/LICENSE
  3 | 
  4 | (function() {
  5 |   var mode = CodeMirror.getMode({indentUnit: 2}, "javascript");
  6 |   function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
  7 | 
  8 |   MT("locals",
  9 |      "[keyword function] [variable foo]([def a], [def b]) { [keyword var] [def c] [operator =] [number 10]; [keyword return] [variable-2 a] [operator +] [variable-2 c] [operator +] [variable d]; }");
 10 | 
 11 |   MT("comma-and-binop",
 12 |      "[keyword function](){ [keyword var] [def x] [operator =] [number 1] [operator +] [number 2], [def y]; }");
 13 | 
 14 |   MT("destructuring",
 15 |      "([keyword function]([def a], [[[def b], [def c] ]]) {",
 16 |      "  [keyword let] {[def d], [property foo]: [def c][operator =][number 10], [def x]} [operator =] [variable foo]([variable-2 a]);",
 17 |      "  [[[variable-2 c], [variable y] ]] [operator =] [variable-2 c];",
 18 |      "})();");
 19 | 
 20 |   MT("class_body",
 21 |      "[keyword class] [variable Foo] {",
 22 |      "  [property constructor]() {}",
 23 |      "  [property sayName]() {",
 24 |      "    [keyword return] [string-2 `foo${][variable foo][string-2 }oo`];",
 25 |      "  }",
 26 |      "}");
 27 | 
 28 |   MT("class",
 29 |      "[keyword class] [variable Point] [keyword extends] [variable SuperThing] {",
 30 |      "  [property get] [property prop]() { [keyword return] [number 24]; }",
 31 |      "  [property constructor]([def x], [def y]) {",
 32 |      "    [keyword super]([string 'something']);",
 33 |      "    [keyword this].[property x] [operator =] [variable-2 x];",
 34 |      "  }",
 35 |      "}");
 36 | 
 37 |   MT("module",
 38 |      "[keyword module] [string 'foo'] {",
 39 |      "  [keyword export] [keyword let] [def x] [operator =] [number 42];",
 40 |      "  [keyword export] [keyword *] [keyword from] [string 'somewhere'];",
 41 |      "}");
 42 | 
 43 |   MT("import",
 44 |      "[keyword function] [variable foo]() {",
 45 |      "  [keyword import] [def $] [keyword from] [string 'jquery'];",
 46 |      "  [keyword module] [def crypto] [keyword from] [string 'crypto'];",
 47 |      "  [keyword import] { [def encrypt], [def decrypt] } [keyword from] [string 'crypto'];",
 48 |      "}");
 49 | 
 50 |   MT("const",
 51 |      "[keyword function] [variable f]() {",
 52 |      "  [keyword const] [[ [def a], [def b] ]] [operator =] [[ [number 1], [number 2] ]];",
 53 |      "}");
 54 | 
 55 |   MT("for/of",
 56 |      "[keyword for]([keyword let] [variable of] [keyword of] [variable something]) {}");
 57 | 
 58 |   MT("generator",
 59 |      "[keyword function*] [variable repeat]([def n]) {",
 60 |      "  [keyword for]([keyword var] [def i] [operator =] [number 0]; [variable-2 i] [operator <] [variable-2 n]; [operator ++][variable-2 i])",
 61 |      "    [keyword yield] [variable-2 i];",
 62 |      "}");
 63 | 
 64 |   MT("fatArrow",
 65 |      "[variable array].[property filter]([def a] [operator =>] [variable-2 a] [operator +] [number 1]);",
 66 |      "[variable a];", // No longer in scope
 67 |      "[keyword let] [variable f] [operator =] ([[ [def a], [def b] ]], [def c]) [operator =>] [variable-2 a] [operator +] [variable-2 c];",
 68 |      "[variable c];");
 69 | 
 70 |   MT("spread",
 71 |      "[keyword function] [variable f]([def a], [meta ...][def b]) {",
 72 |      "  [variable something]([variable-2 a], [meta ...][variable-2 b]);",
 73 |      "}");
 74 | 
 75 |   MT("comprehension",
 76 |      "[keyword function] [variable f]() {",
 77 |      "  [[([variable x] [operator +] [number 1]) [keyword for] ([keyword var] [def x] [keyword in] [variable y]) [keyword if] [variable pred]([variable-2 x]) ]];",
 78 |      "  ([variable u] [keyword for] ([keyword var] [def u] [keyword of] [variable generateValues]()) [keyword if] ([variable-2 u].[property color] [operator ===] [string 'blue']));",
 79 |      "}");
 80 | 
 81 |   MT("quasi",
 82 |      "[variable re][string-2 `fofdlakj${][variable x] [operator +] ([variable re][string-2 `foo`]) [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]");
 83 | 
 84 |   MT("quasi_no_function",
 85 |      "[variable x] [operator =] [string-2 `fofdlakj${][variable x] [operator +] [string-2 `foo`] [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]");
 86 | 
 87 |   MT("indent_statement",
 88 |      "[keyword var] [variable x] [operator =] [number 10]",
 89 |      "[variable x] [operator +=] [variable y] [operator +]",
 90 |      "  [atom Infinity]",
 91 |      "[keyword debugger];");
 92 | 
 93 |   MT("indent_if",
 94 |      "[keyword if] ([number 1])",
 95 |      "  [keyword break];",
 96 |      "[keyword else] [keyword if] ([number 2])",
 97 |      "  [keyword continue];",
 98 |      "[keyword else]",
 99 |      "  [number 10];",
100 |      "[keyword if] ([number 1]) {",
101 |      "  [keyword break];",
102 |      "} [keyword else] [keyword if] ([number 2]) {",
103 |      "  [keyword continue];",
104 |      "} [keyword else] {",
105 |      "  [number 10];",
106 |      "}");
107 | 
108 |   MT("indent_for",
109 |      "[keyword for] ([keyword var] [variable i] [operator =] [number 0];",
110 |      "     [variable i] [operator <] [number 100];",
111 |      "     [variable i][operator ++])",
112 |      "  [variable doSomething]([variable i]);",
113 |      "[keyword debugger];");
114 | 
115 |   MT("indent_c_style",
116 |      "[keyword function] [variable foo]()",
117 |      "{",
118 |      "  [keyword debugger];",
119 |      "}");
120 | 
121 |   MT("indent_else",
122 |      "[keyword for] (;;)",
123 |      "  [keyword if] ([variable foo])",
124 |      "    [keyword if] ([variable bar])",
125 |      "      [number 1];",
126 |      "    [keyword else]",
127 |      "      [number 2];",
128 |      "  [keyword else]",
129 |      "    [number 3];");
130 | 
131 |   MT("indent_funarg",
132 |      "[variable foo]([number 10000],",
133 |      "    [keyword function]([def a]) {",
134 |      "  [keyword debugger];",
135 |      "};");
136 | 
137 |   MT("indent_below_if",
138 |      "[keyword for] (;;)",
139 |      "  [keyword if] ([variable foo])",
140 |      "    [number 1];",
141 |      "[number 2];");
142 | 
143 |   MT("multilinestring",
144 |      "[keyword var] [variable x] [operator =] [string 'foo\\]",
145 |      "[string bar'];");
146 | 
147 |   MT("scary_regexp",
148 |      "[string-2 /foo[[/]]bar/];");
149 | 
150 |   var jsonld_mode = CodeMirror.getMode(
151 |     {indentUnit: 2},
152 |     {name: "javascript", jsonld: true}
153 |   );
154 |   function LD(name) {
155 |     test.mode(name, jsonld_mode, Array.prototype.slice.call(arguments, 1));
156 |   }
157 | 
158 |   LD("json_ld_keywords",
159 |     '{',
160 |     '  [meta "@context"]: {',
161 |     '    [meta "@base"]: [string "http://example.com"],',
162 |     '    [meta "@vocab"]: [string "http://xmlns.com/foaf/0.1/"],',
163 |     '    [property "likesFlavor"]: {',
164 |     '      [meta "@container"]: [meta "@list"]',
165 |     '      [meta "@reverse"]: [string "@beFavoriteOf"]',
166 |     '    },',
167 |     '    [property "nick"]: { [meta "@container"]: [meta "@set"] },',
168 |     '    [property "nick"]: { [meta "@container"]: [meta "@index"] }',
169 |     '  },',
170 |     '  [meta "@graph"]: [[ {',
171 |     '    [meta "@id"]: [string "http://dbpedia.org/resource/John_Lennon"],',
172 |     '    [property "name"]: [string "John Lennon"],',
173 |     '    [property "modified"]: {',
174 |     '      [meta "@value"]: [string "2010-05-29T14:17:39+02:00"],',
175 |     '      [meta "@type"]: [string "http://www.w3.org/2001/XMLSchema#dateTime"]',
176 |     '    }',
177 |     '  } ]]',
178 |     '}');
179 | 
180 |   LD("json_ld_fake",
181 |     '{',
182 |     '  [property "@fake"]: [string "@fake"],',
183 |     '  [property "@contextual"]: [string "@identifier"],',
184 |     '  [property "user@domain.com"]: [string "@graphical"],',
185 |     '  [property "@ID"]: [string "@@ID"]',
186 |     '}');
187 | })();
188 | 


--------------------------------------------------------------------------------
/planout-editor/static/js/mode/javascript/typescript.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | 
 3 | <title>CodeMirror: TypeScript mode</title>
 4 | <meta charset="utf-8"/>
 5 | <link rel=stylesheet href="../../doc/docs.css">
 6 | 
 7 | <link rel="stylesheet" href="../../lib/codemirror.css">
 8 | <script src="../../lib/codemirror.js"></script>
 9 | <script src="javascript.js"></script>
10 | <style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
11 | <div id=nav>
12 |   <a href="http://codemirror.net"><img id=logo src="../../doc/logo.png"></a>
13 | 
14 |   <ul>
15 |     <li><a href="../../index.html">Home</a>
16 |     <li><a href="../../doc/manual.html">Manual</a>
17 |     <li><a href="https://github.com/marijnh/codemirror">Code</a>
18 |   </ul>
19 |   <ul>
20 |     <li><a href="../index.html">Language modes</a>
21 |     <li><a class=active href="#">TypeScript</a>
22 |   </ul>
23 | </div>
24 | 
25 | <article>
26 | <h2>TypeScript mode</h2>
27 | 
28 | 
29 | <div><textarea id="code" name="code">
30 | class Greeter {
31 | 	greeting: string;
32 | 	constructor (message: string) {
33 | 		this.greeting = message;
34 | 	}
35 | 	greet() {
36 | 		return "Hello, " + this.greeting;
37 | 	}
38 | }   
39 | 
40 | var greeter = new Greeter("world");
41 | 
42 | var button = document.createElement('button')
43 | button.innerText = "Say Hello"
44 | button.onclick = function() {
45 | 	alert(greeter.greet())
46 | }
47 | 
48 | document.body.appendChild(button)
49 | 
50 | </textarea></div>
51 | 
52 |     <script>
53 |       var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
54 |         lineNumbers: true,
55 |         matchBrackets: true,
56 |         mode: "text/typescript"
57 |       });
58 |     </script>
59 | 
60 |     <p>This is a specialization of the <a href="index.html">JavaScript mode</a>.</p>
61 |   </article>
62 | 


--------------------------------------------------------------------------------
/planout-editor/static/js/mode/planout/index.html:
--------------------------------------------------------------------------------
  1 | <!doctype html>
  2 | 
  3 | <title>CodeMirror: JavaScript mode</title>
  4 | <meta charset="utf-8"/>
  5 | <link rel=stylesheet href="../../doc/docs.css">
  6 | 
  7 | <link rel="stylesheet" href="../../lib/codemirror.css">
  8 | <script src="../../lib/codemirror.js"></script>
  9 | <script src="../../addon/edit/matchbrackets.js"></script>
 10 | <script src="../../addon/comment/continuecomment.js"></script>
 11 | <script src="../../addon/comment/comment.js"></script>
 12 | <script src="javascript.js"></script>
 13 | <style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
 14 | <div id=nav>
 15 |   <a href="http://codemirror.net"><img id=logo src="../../doc/logo.png"></a>
 16 | 
 17 |   <ul>
 18 |     <li><a href="../../index.html">Home</a>
 19 |     <li><a href="../../doc/manual.html">Manual</a>
 20 |     <li><a href="https://github.com/marijnh/codemirror">Code</a>
 21 |   </ul>
 22 |   <ul>
 23 |     <li><a href="../index.html">Language modes</a>
 24 |     <li><a class=active href="#">JavaScript</a>
 25 |   </ul>
 26 | </div>
 27 | 
 28 | <article>
 29 | <h2>JavaScript mode</h2>
 30 | 
 31 | 
 32 | <div><textarea id="code" name="code">
 33 | // Demo code (the actual new parser character stream implementation)
 34 | 
 35 | function StringStream(string) {
 36 |   this.pos = 0;
 37 |   this.string = string;
 38 | }
 39 | 
 40 | StringStream.prototype = {
 41 |   done: function() {return this.pos >= this.string.length;},
 42 |   peek: function() {return this.string.charAt(this.pos);},
 43 |   next: function() {
 44 |     if (this.pos &lt; this.string.length)
 45 |       return this.string.charAt(this.pos++);
 46 |   },
 47 |   eat: function(match) {
 48 |     var ch = this.string.charAt(this.pos);
 49 |     if (typeof match == "string") var ok = ch == match;
 50 |     else var ok = ch &amp;&amp; match.test ? match.test(ch) : match(ch);
 51 |     if (ok) {this.pos++; return ch;}
 52 |   },
 53 |   eatWhile: function(match) {
 54 |     var start = this.pos;
 55 |     while (this.eat(match));
 56 |     if (this.pos > start) return this.string.slice(start, this.pos);
 57 |   },
 58 |   backUp: function(n) {this.pos -= n;},
 59 |   column: function() {return this.pos;},
 60 |   eatSpace: function() {
 61 |     var start = this.pos;
 62 |     while (/\s/.test(this.string.charAt(this.pos))) this.pos++;
 63 |     return this.pos - start;
 64 |   },
 65 |   match: function(pattern, consume, caseInsensitive) {
 66 |     if (typeof pattern == "string") {
 67 |       function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
 68 |       if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) {
 69 |         if (consume !== false) this.pos += str.length;
 70 |         return true;
 71 |       }
 72 |     }
 73 |     else {
 74 |       var match = this.string.slice(this.pos).match(pattern);
 75 |       if (match &amp;&amp; consume !== false) this.pos += match[0].length;
 76 |       return match;
 77 |     }
 78 |   }
 79 | };
 80 | </textarea></div>
 81 | 
 82 |     <script>
 83 |       var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
 84 |         lineNumbers: true,
 85 |         matchBrackets: true,
 86 |         continueComments: "Enter",
 87 |         extraKeys: {"Ctrl-Q": "toggleComment"}
 88 |       });
 89 |     </script>
 90 | 
 91 |     <p>
 92 |       JavaScript mode supports several configuration options:
 93 |       <ul>
 94 |         <li><code>json</code> which will set the mode to expect JSON
 95 |         data rather than a JavaScript program.</li>
 96 |         <li><code>jsonld</code> which will set the mode to expect
 97 |         <a href="http://json-ld.org">JSON-LD</a> linked data rather
 98 |         than a JavaScript program (<a href="json-ld.html">demo</a>).</li>
 99 |         <li><code>typescript</code> which will activate additional
100 |         syntax highlighting and some other things for TypeScript code
101 |         (<a href="typescript.html">demo</a>).</li>
102 |         <li><code>statementIndent</code> which (given a number) will
103 |         determine the amount of indentation to use for statements
104 |         continued on a new line.</li>
105 |       </ul>
106 |     </p>
107 | 
108 |     <p><strong>MIME types defined:</strong> <code>text/javascript</code>, <code>application/json</code>, <code>application/ld+json</code>, <code>text/typescript</code>, <code>application/typescript</code>.</p>
109 |   </article>
110 | 


--------------------------------------------------------------------------------
/planout-editor/static/js/mode/planout/json-ld.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | 
 3 | <title>CodeMirror: JSON-LD mode</title>
 4 | <meta charset="utf-8"/>
 5 | <link rel=stylesheet href="../../doc/docs.css">
 6 | 
 7 | <link rel="stylesheet" href="../../lib/codemirror.css">
 8 | <script src="../../lib/codemirror.js"></script>
 9 | <script src="../../addon/edit/matchbrackets.js"></script>
10 | <script src="../../addon/comment/continuecomment.js"></script>
11 | <script src="../../addon/comment/comment.js"></script>
12 | <script src="javascript.js"></script>
13 | <style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
14 | <div id="nav">
15 |   <a href="http://codemirror.net"><img id=logo src="../../doc/logo.png"/></a>
16 | 
17 |   <ul>
18 |     <li><a href="../../index.html">Home</a>
19 |     <li><a href="../../doc/manual.html">Manual</a>
20 |     <li><a href="https://github.com/marijnh/codemirror">Code</a>
21 |   </ul>
22 |   <ul>
23 |     <li><a href="../index.html">Language modes</a>
24 |     <li><a class=active href="#">JSON-LD</a>
25 |   </ul>
26 | </div>
27 | 
28 | <article>
29 | <h2>JSON-LD mode</h2>
30 | 
31 | 
32 | <div><textarea id="code" name="code">
33 | {
34 |   "@context": {
35 |     "name": "http://schema.org/name",
36 |     "description": "http://schema.org/description",
37 |     "image": {
38 |       "@id": "http://schema.org/image",
39 |       "@type": "@id"
40 |     },
41 |     "geo": "http://schema.org/geo",
42 |     "latitude": {
43 |       "@id": "http://schema.org/latitude",
44 |       "@type": "xsd:float"
45 |     },
46 |     "longitude": {
47 |       "@id": "http://schema.org/longitude",
48 |       "@type": "xsd:float"
49 |     },
50 |     "xsd": "http://www.w3.org/2001/XMLSchema#"
51 |   },
52 |   "name": "The Empire State Building",
53 |   "description": "The Empire State Building is a 102-story landmark in New York City.",
54 |   "image": "http://www.civil.usherbrooke.ca/cours/gci215a/empire-state-building.jpg",
55 |   "geo": {
56 |     "latitude": "40.75",
57 |     "longitude": "73.98"
58 |   }
59 | }
60 | </textarea></div>
61 | 
62 |     <script>
63 |       var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
64 |         matchBrackets: true,
65 |         autoCloseBrackets: true,
66 |         mode: "application/ld+json",
67 |         lineWrapping: true
68 |       });
69 |     </script>
70 |     
71 |     <p>This is a specialization of the <a href="index.html">JavaScript mode</a>.</p>
72 |   </article>
73 | 


--------------------------------------------------------------------------------
/planout-editor/static/js/mode/planout/test.js:
--------------------------------------------------------------------------------
  1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others
  2 | // Distributed under an MIT license: http://codemirror.net/LICENSE
  3 | 
  4 | (function() {
  5 |   var mode = CodeMirror.getMode({indentUnit: 2}, "javascript");
  6 |   function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
  7 | 
  8 |   MT("locals",
  9 |      "[keyword function] [variable foo]([def a], [def b]) { [keyword var] [def c] [operator =] [number 10]; [keyword return] [variable-2 a] [operator +] [variable-2 c] [operator +] [variable d]; }");
 10 | 
 11 |   MT("comma-and-binop",
 12 |      "[keyword function](){ [keyword var] [def x] [operator =] [number 1] [operator +] [number 2], [def y]; }");
 13 | 
 14 |   MT("destructuring",
 15 |      "([keyword function]([def a], [[[def b], [def c] ]]) {",
 16 |      "  [keyword let] {[def d], [property foo]: [def c][operator =][number 10], [def x]} [operator =] [variable foo]([variable-2 a]);",
 17 |      "  [[[variable-2 c], [variable y] ]] [operator =] [variable-2 c];",
 18 |      "})();");
 19 | 
 20 |   MT("class_body",
 21 |      "[keyword class] [variable Foo] {",
 22 |      "  [property constructor]() {}",
 23 |      "  [property sayName]() {",
 24 |      "    [keyword return] [string-2 `foo${][variable foo][string-2 }oo`];",
 25 |      "  }",
 26 |      "}");
 27 | 
 28 |   MT("class",
 29 |      "[keyword class] [variable Point] [keyword extends] [variable SuperThing] {",
 30 |      "  [property get] [property prop]() { [keyword return] [number 24]; }",
 31 |      "  [property constructor]([def x], [def y]) {",
 32 |      "    [keyword super]([string 'something']);",
 33 |      "    [keyword this].[property x] [operator =] [variable-2 x];",
 34 |      "  }",
 35 |      "}");
 36 | 
 37 |   MT("module",
 38 |      "[keyword module] [string 'foo'] {",
 39 |      "  [keyword export] [keyword let] [def x] [operator =] [number 42];",
 40 |      "  [keyword export] [keyword *] [keyword from] [string 'somewhere'];",
 41 |      "}");
 42 | 
 43 |   MT("import",
 44 |      "[keyword function] [variable foo]() {",
 45 |      "  [keyword import] [def $] [keyword from] [string 'jquery'];",
 46 |      "  [keyword module] [def crypto] [keyword from] [string 'crypto'];",
 47 |      "  [keyword import] { [def encrypt], [def decrypt] } [keyword from] [string 'crypto'];",
 48 |      "}");
 49 | 
 50 |   MT("const",
 51 |      "[keyword function] [variable f]() {",
 52 |      "  [keyword const] [[ [def a], [def b] ]] [operator =] [[ [number 1], [number 2] ]];",
 53 |      "}");
 54 | 
 55 |   MT("for/of",
 56 |      "[keyword for]([keyword let] [variable of] [keyword of] [variable something]) {}");
 57 | 
 58 |   MT("generator",
 59 |      "[keyword function*] [variable repeat]([def n]) {",
 60 |      "  [keyword for]([keyword var] [def i] [operator =] [number 0]; [variable-2 i] [operator <] [variable-2 n]; [operator ++][variable-2 i])",
 61 |      "    [keyword yield] [variable-2 i];",
 62 |      "}");
 63 | 
 64 |   MT("fatArrow",
 65 |      "[variable array].[property filter]([def a] [operator =>] [variable-2 a] [operator +] [number 1]);",
 66 |      "[variable a];", // No longer in scope
 67 |      "[keyword let] [variable f] [operator =] ([[ [def a], [def b] ]], [def c]) [operator =>] [variable-2 a] [operator +] [variable-2 c];",
 68 |      "[variable c];");
 69 | 
 70 |   MT("spread",
 71 |      "[keyword function] [variable f]([def a], [meta ...][def b]) {",
 72 |      "  [variable something]([variable-2 a], [meta ...][variable-2 b]);",
 73 |      "}");
 74 | 
 75 |   MT("comprehension",
 76 |      "[keyword function] [variable f]() {",
 77 |      "  [[([variable x] [operator +] [number 1]) [keyword for] ([keyword var] [def x] [keyword in] [variable y]) [keyword if] [variable pred]([variable-2 x]) ]];",
 78 |      "  ([variable u] [keyword for] ([keyword var] [def u] [keyword of] [variable generateValues]()) [keyword if] ([variable-2 u].[property color] [operator ===] [string 'blue']));",
 79 |      "}");
 80 | 
 81 |   MT("quasi",
 82 |      "[variable re][string-2 `fofdlakj${][variable x] [operator +] ([variable re][string-2 `foo`]) [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]");
 83 | 
 84 |   MT("quasi_no_function",
 85 |      "[variable x] [operator =] [string-2 `fofdlakj${][variable x] [operator +] [string-2 `foo`] [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]");
 86 | 
 87 |   MT("indent_statement",
 88 |      "[keyword var] [variable x] [operator =] [number 10]",
 89 |      "[variable x] [operator +=] [variable y] [operator +]",
 90 |      "  [atom Infinity]",
 91 |      "[keyword debugger];");
 92 | 
 93 |   MT("indent_if",
 94 |      "[keyword if] ([number 1])",
 95 |      "  [keyword break];",
 96 |      "[keyword else] [keyword if] ([number 2])",
 97 |      "  [keyword continue];",
 98 |      "[keyword else]",
 99 |      "  [number 10];",
100 |      "[keyword if] ([number 1]) {",
101 |      "  [keyword break];",
102 |      "} [keyword else] [keyword if] ([number 2]) {",
103 |      "  [keyword continue];",
104 |      "} [keyword else] {",
105 |      "  [number 10];",
106 |      "}");
107 | 
108 |   MT("indent_for",
109 |      "[keyword for] ([keyword var] [variable i] [operator =] [number 0];",
110 |      "     [variable i] [operator <] [number 100];",
111 |      "     [variable i][operator ++])",
112 |      "  [variable doSomething]([variable i]);",
113 |      "[keyword debugger];");
114 | 
115 |   MT("indent_c_style",
116 |      "[keyword function] [variable foo]()",
117 |      "{",
118 |      "  [keyword debugger];",
119 |      "}");
120 | 
121 |   MT("indent_else",
122 |      "[keyword for] (;;)",
123 |      "  [keyword if] ([variable foo])",
124 |      "    [keyword if] ([variable bar])",
125 |      "      [number 1];",
126 |      "    [keyword else]",
127 |      "      [number 2];",
128 |      "  [keyword else]",
129 |      "    [number 3];");
130 | 
131 |   MT("indent_funarg",
132 |      "[variable foo]([number 10000],",
133 |      "    [keyword function]([def a]) {",
134 |      "  [keyword debugger];",
135 |      "};");
136 | 
137 |   MT("indent_below_if",
138 |      "[keyword for] (;;)",
139 |      "  [keyword if] ([variable foo])",
140 |      "    [number 1];",
141 |      "[number 2];");
142 | 
143 |   MT("multilinestring",
144 |      "[keyword var] [variable x] [operator =] [string 'foo\\]",
145 |      "[string bar'];");
146 | 
147 |   MT("scary_regexp",
148 |      "[string-2 /foo[[/]]bar/];");
149 | 
150 |   var jsonld_mode = CodeMirror.getMode(
151 |     {indentUnit: 2},
152 |     {name: "javascript", jsonld: true}
153 |   );
154 |   function LD(name) {
155 |     test.mode(name, jsonld_mode, Array.prototype.slice.call(arguments, 1));
156 |   }
157 | 
158 |   LD("json_ld_keywords",
159 |     '{',
160 |     '  [meta "@context"]: {',
161 |     '    [meta "@base"]: [string "http://example.com"],',
162 |     '    [meta "@vocab"]: [string "http://xmlns.com/foaf/0.1/"],',
163 |     '    [property "likesFlavor"]: {',
164 |     '      [meta "@container"]: [meta "@list"]',
165 |     '      [meta "@reverse"]: [string "@beFavoriteOf"]',
166 |     '    },',
167 |     '    [property "nick"]: { [meta "@container"]: [meta "@set"] },',
168 |     '    [property "nick"]: { [meta "@container"]: [meta "@index"] }',
169 |     '  },',
170 |     '  [meta "@graph"]: [[ {',
171 |     '    [meta "@id"]: [string "http://dbpedia.org/resource/John_Lennon"],',
172 |     '    [property "name"]: [string "John Lennon"],',
173 |     '    [property "modified"]: {',
174 |     '      [meta "@value"]: [string "2010-05-29T14:17:39+02:00"],',
175 |     '      [meta "@type"]: [string "http://www.w3.org/2001/XMLSchema#dateTime"]',
176 |     '    }',
177 |     '  } ]]',
178 |     '}');
179 | 
180 |   LD("json_ld_fake",
181 |     '{',
182 |     '  [property "@fake"]: [string "@fake"],',
183 |     '  [property "@contextual"]: [string "@identifier"],',
184 |     '  [property "user@domain.com"]: [string "@graphical"],',
185 |     '  [property "@ID"]: [string "@@ID"]',
186 |     '}');
187 | })();
188 | 


--------------------------------------------------------------------------------
/planout-editor/static/js/mode/planout/typescript.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | 
 3 | <title>CodeMirror: TypeScript mode</title>
 4 | <meta charset="utf-8"/>
 5 | <link rel=stylesheet href="../../doc/docs.css">
 6 | 
 7 | <link rel="stylesheet" href="../../lib/codemirror.css">
 8 | <script src="../../lib/codemirror.js"></script>
 9 | <script src="javascript.js"></script>
10 | <style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
11 | <div id=nav>
12 |   <a href="http://codemirror.net"><img id=logo src="../../doc/logo.png"></a>
13 | 
14 |   <ul>
15 |     <li><a href="../../index.html">Home</a>
16 |     <li><a href="../../doc/manual.html">Manual</a>
17 |     <li><a href="https://github.com/marijnh/codemirror">Code</a>
18 |   </ul>
19 |   <ul>
20 |     <li><a href="../index.html">Language modes</a>
21 |     <li><a class=active href="#">TypeScript</a>
22 |   </ul>
23 | </div>
24 | 
25 | <article>
26 | <h2>TypeScript mode</h2>
27 | 
28 | 
29 | <div><textarea id="code" name="code">
30 | class Greeter {
31 | 	greeting: string;
32 | 	constructor (message: string) {
33 | 		this.greeting = message;
34 | 	}
35 | 	greet() {
36 | 		return "Hello, " + this.greeting;
37 | 	}
38 | }   
39 | 
40 | var greeter = new Greeter("world");
41 | 
42 | var button = document.createElement('button')
43 | button.innerText = "Say Hello"
44 | button.onclick = function() {
45 | 	alert(greeter.greet())
46 | }
47 | 
48 | document.body.appendChild(button)
49 | 
50 | </textarea></div>
51 | 
52 |     <script>
53 |       var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
54 |         lineNumbers: true,
55 |         matchBrackets: true,
56 |         mode: "text/typescript"
57 |       });
58 |     </script>
59 | 
60 |     <p>This is a specialization of the <a href="index.html">JavaScript mode</a>.</p>
61 |   </article>
62 | 


--------------------------------------------------------------------------------
/planout-editor/static/planoutstyle.css:
--------------------------------------------------------------------------------
 1 | pre.correct {
 2 |   width: 100%;
 3 | }
 4 | 
 5 | pre.error {
 6 |   width: 100%;
 7 |   background-color: #ffaaaa;
 8 | }
 9 | 
10 | div.testerBox {
11 |   width: 450px;
12 | }
13 | 
14 | textarea.testcode {
15 |   height: 200px;
16 |   width: 100%;
17 | }
18 | 
19 | div.boundingbox {
20 |   width: 450px;
21 | }
22 | 
23 | textarea {
24 |     background-color: #2d2d2d;
25 |     border-radius: 2px;
26 |     border: 10px #2d2d2d solid;
27 |     box-shadow: 0 2px 4px rgba(0,0,0,0.4);
28 |     color: #cccccc;
29 |       font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
30 |     font-size: 14px;
31 |     height: 300px;
32 |     max-width: 100%;
33 |     width: 100%;
34 |     -moz-box-sizing: border-box;
35 |     -webkit-box-sizing: border-box;
36 |     box-sizing: border-box;
37 | }
38 | 
39 | dt.fields {
40 |   padding-bottom: 1px;
41 |   padding-top: 12px;
42 | }
43 | 


--------------------------------------------------------------------------------
/planout-editor/templates/index.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | <html lang="en">
 3 | 	<head>
 4 | 		<meta charset="utf-8">
 5 | 		<title>PlanOut Experiment Editor</title>
 6 |     <link rel="stylesheet" href="static/bootstrap.min.css">
 7 |     <link rel="stylesheet" href="static/bootstrap-theme.min.css">
 8 |     <link rel="stylesheet" type="text/css" href="static/planoutstyle.css">
 9 |     <script src="static/js/jquery-1.11.1.min.js"></script>
10 |     <script src="static/js/bootstrap.min.js"></script>
11 | 	</head>
12 | 	<body>
13 | 		<section id="planouteditor"></section>
14 | 		<script src="static/js/bundle.js"></script>
15 | 	</body>
16 | </html>
17 | 


--------------------------------------------------------------------------------
/planout-editor/templates/layout.html:
--------------------------------------------------------------------------------
 1 | <!-- template.html -->
 2 | <html>
 3 |   <head>
 4 |     <title>Reactive PlanOut</title>
 5 |     <script src="static/js/react-0.11.0.js"></script>
 6 |     <script src="static/js/JSXTransformer-0.11.0.js"></script>
 7 |     <script src="static/js/jquery-1.11.1.min.js"></script>
 8 |     <script src="static/js/bootstrap.min.js"></script>
 9 |     <script src="static/js/react-bootstrap.min.js"></script>
10 | 
11 |     <link rel="stylesheet" href="static/bootstrap.min.css">
12 |     <link rel="stylesheet" href="static/bootstrap-theme.min.css">
13 |     <link rel="stylesheet" type="text/css" href="static/planoutstyle.css">
14 |   </head>
15 |  <body>
16 | {% block body %}{% endblock %}
17 |   </body>
18 | </html>
19 | 


--------------------------------------------------------------------------------
/python/docs/00-getting-started.md:
--------------------------------------------------------------------------------
 1 | # Getting started
 2 | 
 3 | 
 4 | ## Quick install
 5 | You can install PlanOut using `pip`. Within a [`virtualenv`](http://www.virtualenv.org/en/latest/), install with
 6 | 
 7 | ```
 8 | pip install planout
 9 | ```
10 | 
11 | You can also install directly from source. Check out the git repository, create a virtualenv, activate it and run:
12 | 
13 | ```
14 | python setup.py install
15 | ```
16 | 


--------------------------------------------------------------------------------
/python/docs/01-why-planout.md:
--------------------------------------------------------------------------------
 1 | A/B tests and other randomized experiments are widely used as part of continually improving Web and mobile apps and services. PlanOut makes it easy to run both simple and complex experiments.
 2 | 
 3 | ## Focus on parameters
 4 | 
 5 | PlanOut is all about providing randomized values of parameters that control your service. Instead of using a constant, just use PlanOut to determine these parameters (e.g., text size or color, the presence of a new feature, the number of items in a list). Now you have an experiment.
 6 | 
 7 | ## From simple to complex
 8 | 
 9 | It is easy to implement an A/B test in PlanOut, or other simple experiments like those involving a factorial design. But is not much harder to implement more complex designs. Multiple types of units (e.g., users, pieces of content) can be randomly assigned to parameter values in the same experiment. Experiments can also involve directly randomizing other inputs, such as randomly selecting which three friends to display to a user.
10 | 
11 | ## Automatic logging
12 | You will often want to keep track of which users (or other units) have been exposed to your experiment. This can make subsequent analysis more precise and prevent common errors in analysis. PlanOut calls your logging code whenever a parameter value is checked.
13 | 
14 | 
15 | ## Advanced features
16 | 
17 | We created PlanOut to meet requirements from running experiments at Facebook, which gives rise to some of its more advanced features.
18 | 
19 | ### Serialization
20 | Experiments can also be specified through JSON code. This can enable separate review processes for changes to the experiment, support multi-platform execution, and restrict the range of operations that should occur during experimental assignment (for reasons of, e.g., performance, correctness, static analysis). It also allows developers to implement their own tools to specify experiments without writing any code at all.
21 | 
22 | ### Domain-specific language
23 | PlanOut experiments can be specified through the PlanOut language, which concisely describes an experiment using a set of primitive operations.  PlanOut language code is compiled into the JSON serialization, which can be executed by the PlanOut interpreter as needed.
24 | 
25 | ### Iterative experimentation
26 | The PlanOut library includes a basic namespace class (link) for managing multiple, iterative experiments that run concurrently.
27 | 


--------------------------------------------------------------------------------
/python/docs/02-operators.md:
--------------------------------------------------------------------------------
 1 | # Extending PlanOut
 2 | 
 3 | Experiments
 4 | - Logging
 5 | - Assignment scheme
 6 | - Salt
 7 | - Uses a mapper
 8 | 
 9 | Assignment schemes
10 | - Mappers are helpers for doing deterministic pseudorandom assignment
11 | 
12 | Mappers:
13 |  - One where you code in python
14 |  - 
15 | 
16 | ## Core concepts
17 | An *Experiment* object takes inputs and maps it to parameter assignments.  Experiment objects also handle logging and caching.
18 | 
19 | *Mappers* are execution environments used to implement experiments. They execute *operators* which are functions that perform basic operations, including deterministic random assignment.
20 | 
21 | 
22 | ## Mappers
23 | A mapper translates inputs to parameter assignments.
24 | There are two main types of PlanOut mappers: `PlanOutKitMapper`, which is useful for many (ad hoc) experimentation needs, and `PlanOutInterpreterMapper`, which reads in serialized experiment definitions and is suitable for use in production environments when experiments are centrally managed via a Web interface. ``PlanOutInterpreterMapper``
25 | 
26 | ## Experiment class
27 | 
28 | The experiment class implements core functionality associated with each experiment. In particular, every experiment has a:
29 |  - name
30 |  - experiment-level salt
31 |  - an assignment scheme using a PlanOut mapper that translates inputs to parameter values.
32 |  - logging, which by default maintains an "exposure log" of when inputs (e.g., userids) get mapped to parameter. This makes it easier to keep track of who was in your experiment, and restrict your analysis to the experiment.
33 | 
34 | To define a new experiment, one subclasses the Experiment class. By default, the name of the experiment will be the name of the subclass, and the experiment-level salt will be the name of the experiment.
35 | 


--------------------------------------------------------------------------------
/python/docs/03-how-planout-works.md:
--------------------------------------------------------------------------------
 1 | # How PlanOut works
 2 | 
 3 | PlanOut works by hashing input data into numbers, and using these numbers to generate what are effectively pseudo-random values to pick numbers. All PlanOut operators include basic unit tests (link) to verify that they generate assignments with the expected distribution.
 4 | 
 5 | Good randomization procedures produce assignments that are independent of one another. Below, we show how PlanOut uses experiment-level and variable-level "salts" (strings that get appended to the data thats being hashed) to make sure that variables within and across experiments remain independent.
 6 | 
 7 | ## Pseudo-random assignment through hashing
 8 | Consider the following experiment:
 9 | ```python
10 | class SharingExperiment(SimpleExperiment):
11 |   def set_attributes(self):
12 |     self.name = 'sharing_name'
13 |     self.salt = 'sharing_salt'
14 | 
15 |   def assign(self, params, userid):
16 |     params.button_text = UniformChoice(
17 |       choices=['OK', 'Share', 'Share with friends'],
18 |       unit=userid
19 |     )
20 | ```
21 | Here, we define a single randomized parameter, `button_text`. The assignment is generated by first composing a string containing experiment-level salt, `sharing_salt`, the parameter-level salt `button_text`, and the input unit. By default, PlanOut uses the variable name as the parameter-level salt.
22 | 
23 | When we choose the button text for a particular unit, e.g.,
24 | 
25 | ```python
26 | SharingExperiment(userid=4).get('button_text')
27 | ```
28 | 
29 | PlanOut would compute the SHA1 checksum for:
30 | ```
31 | sharing_salt.button_text.4
32 | ```
33 | and then use the last few digits of this checksum to index into the given list of `choices`.  Since SHA1 is cryptographically safe, even minor changes to the hashing string (e.g., considering `userid=41` instead of 4) will result in a totally different number.
34 | 
35 | Multiple units are handled through concatenation. Had the `unit` parameter been `unit=[userid, url]`,
36 | 
37 | ```python
38 | SharingExperiment(userid=4, url='http://www.facebook.com').get('button_text')
39 | ```
40 | 
41 | PlanOut would compute the SHA1 checksum for:
42 | ```
43 | sharing_salt.button_text.4.http://www.facebook.com
44 | ```
45 | 
46 | Note that because PlanOut simply concatenates the units, the order in which you specify lists of units matters.
47 | 
48 | 
49 | ## Salts
50 | 
51 | ### Experiment-level salts
52 | Experiment-level salts can be manually in the  `set_attributes()` method (as we have above). If the salt is not specified, then the experiment name is used as the salt. With SimpleExperiment, if the name is not set in `set_attributes()`, then the name of the class is used as the experiment name.
53 | 
54 | ### Parameter-level salts
55 | The parameter name is automatically used to salt random assignment operations, but parameter level salts can be specified manually. For example, in the following code
56 | 
57 | ```python
58 | params.x = UniformChoice(choices=['a','b'], unit=userid)
59 | params.y = UniformChoice(choices=['a','b'], unit=userid, salt='x')
60 | ```
61 | 
62 | both `x` and `y` will always be assigned to the same exact same value.
63 | 
64 | This lets you change the name of the variable you are logging without changing the assignment. Use variable level salts with caution, since they might lead to failures in randomization (link).
65 | 
66 | ### Salts with namespaces
67 | Namespaces (link) are a way to manage concurrent and iterative experiments. When using `SimpleNamespace`, the namespace-level salt is appended to the experiment-level salt. This ensures that random assignment is independent across experiments with the same name running under different namespaces.
68 | 


--------------------------------------------------------------------------------
/python/docs/04-logging.md:
--------------------------------------------------------------------------------
 1 | # Logging
 2 | 
 3 | You will usually want to log which units (e.g., users) are exposed to your experiment.
 4 | 
 5 | Logging this information enables monitoring your experiment and improving your analysis of the results. In particular, many experiments only change your service for the small portion of users that use a particular part of the service; keeping track of these users will make your analysis more precise.
 6 | 
 7 | The `SimpleExperiment` class providing functionality for logging data to a file using Python's `logger` module. Additionally, you can extend `Experiment` to call your logging code instead (link to extending-logging).
 8 | 
 9 | ## Anatomy of a log
10 | Consider Experiment 1 from the PlanOut paper (link):
11 | ```
12 | class Exp1(SimpleExperiment):
13 |   def assign(self, e, userid):
14 |     e.group_size = UniformChoice(choices=[1, 10], unit=userid);
15 |     e.specific_goal = BernoulliTrial(p=0.8, unit=userid);
16 |     if e.specific_goal:
17 |       e.ratings_per_user_goal = UniformChoice(
18 |         choices=[8, 16, 32, 64], unit=userid)
19 |       e.ratings_goal = e.group_size * e.ratings_per_user_goal
20 |     return e
21 | ```
22 | It takes a `userid` as input, and assigns three paramers, `group_size`, `specific_goal`, and `ratings goal`. It does not specify a custom salt (link) or experiment name, so the experiment salt and name are automatically set to the class name `Exp3`. The default logger in `SimpleExperiment` log all of these fields:
23 | 
24 | ```
25 | {'inputs': {'userid': 3}, 'checksum': '4b80881d', 'salt': 'Exp1', 'name': 'Exp1', 'params': {'specific_goal': 1, 'ratings_goal': 160, 'group_size': 10, 'ratings_per_user_goal': 16}}
26 | ```
27 | 
28 | In addition, a `checksum` field is attached. This is part of an MD5 checksum of your code, so that analysts can keep track of when an experiments' assignments have potentially changed: whenever the checksum changes, the assignment procedure code is different, and whenever the salt changes, the assignments will be completely different.
29 | 
30 | TODO: exposure-log logs should have a field that specifies its an exposure. This payload should also include a timestamp.
31 | 
32 | ## Types of logs
33 | 
34 | ### Auto-exposure logging
35 | By default, exposures are logged once per instance of an experiment object when you get a parameter. This is auto-exposure logging. It is recommended for most situations, since you will want to track whenever a unit is exposed to your experiment. Generally, any unit for which a parameter has been retrieved should be counted as exposed, unless you wish to make further assumptions.
36 | 
37 | ### Manual exposure logging
38 | In some cases, you might want to choose exactly when exposures are logged. You can disable auto-exposure logging  with the `set_auto_exposure_logging` method and instead choose to directly call `log_exposure` to keep track of exposures.
39 | 
40 | Why might you want to do this? You might be adding experimental assignment information to other existing logs of outcome data, but many of the users who have outcome observations may not actually have been exposed. Other cases occur when some advance preparation of some components (e.g., UI) or data are required, but you can assume that parameter values set at this stage do not yet affect the user.
41 | 
42 | ### Conversion logging
43 | You want to see how the parameters you are manipulating affect outcomes or conversion events. It can also be convenient to log these events along with exposures. You can do this by calling the `log_outcome` method.
44 | 
45 | You may have existing logs for many of these events. In this case, you could add experimental assignment information to these logs by instantiating an experiment, turning off auto-exposure logging for that instance, and adding parameter information to your logs. Alternatively, you can later join exposure and outcome data on unit identifiers (e.g., user IDs).
46 | 


--------------------------------------------------------------------------------
/python/docs/04.1-extending-logging.md:
--------------------------------------------------------------------------------
 1 | ## Extending logging functionality
 2 | If you have already adopted some method for logging events, you may want to use that method to log experiment-related events, such as exposures and experiment-specific outcomes. You can do this by extending the `Experiment` abstract class and overriding the `log` method.
 3 | 
 4 | ### Overriding the `log` method
 5 | To log exposures using your existing logging system, just override the `log` method when extending the `Experiment` abstract class. For example, if you write to your logs with `MyLogger`, then you might create a class as follows.
 6 | ```python
 7 | 
 8 |   def log(self, data):
 9 |     MyLogger.log(data)
10 | 
11 | ```
12 | 
13 | ### Writing a log configuration method
14 | For some logging frameworks, you may want to have a way to setup your logger once per `Experiment` instance. In this case, you can implement a method to do this setup, which then gets called, as needed, by your `log` method.
15 | 
16 | 
17 | ### Adding caching
18 | By default, each instance of an Experiment class will only write to your logs once. But this can still result in a lot of writing when there are many instances created in a single request. So you may want to add some caching to prevent lots of unnecessary logging. Perhaps your logging system already handles this. Otherwise, you can add a key to a cache in the `log` method and check it before actually logging.
19 | 


--------------------------------------------------------------------------------
/python/docs/05-namespaces.md:
--------------------------------------------------------------------------------
 1 | # Namespaces
 2 | 
 3 | Namespaces are used to manage related experiments that manipulate the same parameter. These experiments might be run sequentially (over time) or in parallel. When experiments are conducted in parallel, namespaces can be used to keep experiments "exclusive" or "non-overlapping".
 4 | 
 5 | 
 6 | Namespaces and models like [Google's "layers"](http://research.google.com/pubs/pub36500.html) and Facebook's "universes" solve the overlapping experiment problem by centering experiments around a primary unit, such as user IDs.  Within a given namespace, each primary unit belongs to at most one experiment.
 7 | 
 8 | 
 9 | ### How do namespaces work?
10 | Rather than requesting a parameter from an experiment, developers request a parameter from a namespace, which then handles identifying the experiment (if there is one) that that unit is part of.
11 | 
12 | Under the hood, primary units are mapped to one of a large number of segments (e.g., 10,000).
13 | These segments are allocated to experiments. For any given unit, a namespace manager looks up that unit's segment. If the segment is allocated to an experiment, the input data is passed to the experiment, and random assignment occurs using the regular logic of the corresponding `Experiment` object.
14 | 
15 | If the primary unit is not mapped to an experiment, or a parameter is requested that is not defined by the experiment, a default experiment or value may be used.
16 |  This allows experimenters to configure default values for parameters on the fly in a way that does not interfere with currently running experiments.
17 | 
18 | 
19 | ### When do I need to use a namespace?
20 | Namespaces are useful whenever there is at least one variable in your code base that you would like to experiment with, either over time or simultaneously.
21 | 
22 | As a starting point, PlanOut provides a basic implementation of namespaces with the `SimpleNamespace` class.
23 | 


--------------------------------------------------------------------------------
/python/docs/05.1-simple-namespaces.md:
--------------------------------------------------------------------------------
 1 | # Iterating on experiments with `SimpleNamespace`
 2 | 
 3 | `SimpleNamespace` provides namespace functionality without being backed by a database or involving a larger experimentation management system. For both organizational and performance reasons, it is not recommended for namespaces that would be used to run many (e.g., thousands) experiments, but it should be sufficient for iterating on smaller-scale experiments.
 4 | 
 5 | Similar to how you create experiments with the `SimpleExperiment` class, new namespaces are created through subclassing.  `SimpleNamespace` two requires that developers implement two methods:
 6 |  - `setup_attributes()`: this method sets the namespace's name, the primary unit, and number of segments. The primary unit is the input unit that gets mapped to segments, which are allocated to experiments.
 7 |  - `setup_experiments()`: this method allocates and deallocates experiments. When used in production, lines of code should only be added to this method.
 8 | 
 9 | ```python
10 | class ButtonNamespace(SimpleNamespace):
11 |   def setup_attributes(self):
12 |     self.name = 'my_demo'
13 |     self.primary_unit = 'userid'
14 |     self.num_segments = 1000
15 | 
16 |   def setup_experiments():
17 |     # create and remove experiments here
18 | ```
19 | 
20 | In the example above, the name of the namespace is `my_demo`. This gets used, in addition to the experiment name and variable names, to hash units to experimental conditions. The number of segments is the granularity of the experimental groups.
21 | 
22 | ### Allocating and deallocating segments to experiments
23 | When you extend `SimpleNamespace` class, you implement the `setup_experiments` method. This specifies a series of allocations and deallocations of segments to experiments.
24 | 
25 | For example, the following setup method:
26 | ```python
27 | def setup_experiments():
28 |   self.add_experiment('first experiment', MyFirstExperiment, 100)
29 | ```
30 | would allocate  'first experiment' is the name of your experiment, 100 of 1000 segments, selected at random to be allocated to the `MyFirstExperiment` class. In this example (see demo/demo_namespace.py), `MyFirstExperiment` is a subclass of `SimpleExperiment`.
31 | 
32 | Adding additional experiments would just involve appending additional lines to the method. For example, you might run a larger follow-up experiment with the same design:
33 | ```python
34 | def setup_experiments():
35 |   self.add_experiment('first experiment', MyFirstExperiment, 10)
36 |   self.add_experiment('first experiment, replication 1', MyFirstExperiment, 40)
37 | ```
38 | Or you might run a new experiment with a different design:
39 | ```python
40 | def setup_experiments():
41 |   self.add_experiment('first experiment', MyFirstExperiment, 10)
42 |   self.add_experiment('second experiment', MySecondExperiment, 20)
43 | ```
44 | When an experiment is complete and you wish to make its segments available to new experiments, append a call to `remove_experiment`:
45 | ```python
46 | def setup_experiments():
47 |   self.add_experiment('first experiment', MyFirstExperiment, 10)
48 |   self.add_experiment('second experiment', MySecondExperiment, 20)
49 |   self.remove_experiment('first experiment')
50 |   self.add_experiment('third experiment', MyThirdExperiment, 50)
51 | ```
52 | Note: In modifying this method, you should only add lines after all previous lines. Inserting calls to `add_experiment` or `remove_experiment` will likely move segments from one experiment to another -- not what you want! This is because there is no stored allocation state (e.g., in a database), so the current allocation is dependent on the full history.
53 | 


--------------------------------------------------------------------------------
/python/docs/06-best-practices.md:
--------------------------------------------------------------------------------
 1 | # Best practices
 2 | 
 3 | PlanOut makes it easy to implement bug-free code that randomly assigns users (or other units) to parameters. The Experiment and Namespace classes are designed to reduce common errors in deploying and logging experiments. Here are a few tips for running experiments:
 4 | 
 5 | * Use auto-exposure logging (link), which is enabled by default.  Auto-exposure logging makes it easier to check that your assignment procedure is working correctly, increases the precision of your experiment, and reduces errors in downstream analysis.
 6 | * Avoid changing an experiment while it is running. Instead, either run a follow-up experiment using a namespace (link), or create a new experiment with a different salt to re-randomize the units. These experiments should be analyzed separately from the original experiment.
 7 | * Automate the analysis of your experiment. If you are running multiple related experiments, create a pipeline to automatically do the analysis.
 8 | 
 9 | 
10 | There are no hard and fast rules for what kinds of changes are actually a problem, but if you follow the best practices above, you should be in reasonable shape.
11 | 
12 | 
13 | ## Randomization failures
14 | Experiments are used to test the change of one or more parameters on some average outcome (e.g., messages sent, or clicks on a button). Differences can be safely attributed to a change in parameters if treatments are assigned to users completely at random.
15 | 
16 | In practice, there are a number of common ways for two groups to not be equivalent (beyond random imbalance):
17 |  - Some units from one group were previously in a different group, while users from the other group were not.
18 |  - Some units in one group were recently added to the experiment.
19 |  - There was a bug in the code for one group but not the other, and that bug recently got fixed.
20 | 
21 | In these cases, we suggest that you launch a new experiment, either through the use of namespaces (links), or by re-assigning all of the units in your experiment.  This can be done by simply changing the salt of your experiment:
22 | 
23 | ```python
24 | class MyNewExperiment(MyOldExperiment):
25 |   def set_experiment_properties(self):
26 |     self.name = 'new_experiment_name'
27 |     self.salt = 'new_experiment_salt'
28 | ```
29 | 
30 | 
31 | ### Unanticipated consequences from changing experiments
32 | Changes to experiment definitions will generally alter which parameters users are assigned to. For example, consider an experiment that manipulates the label of a button for sharing a link. The main outcome of interest is the effect of this text on how many links users share per day.
33 | 
34 | 
35 | ```python
36 | class SharingExperiment(SimpleExperiment):
37 |   def assign(self, params, userid):
38 |     params.button_text = UniformChoice(
39 |       choices=['OK', 'Share', 'Share with friends'],
40 |       unit=userid
41 |     )
42 | ```
43 | Changing the variable name `button_text` changes the assignment, since it is used to salt (link) to assignment procedure (see `Experiment` intro document).
44 | 
45 | Changing the number of choices for the `button_text` also affects users previously randomized into other conditions.  For example, removing the 'Share' item from the `choices` list, will allocate some users who were previosuly in the 'Share' condition to the 'OK' and 'Share with friends group'. Their outcomes will now be a weighted average of the two, which may decrease the observed difference between 'OK' and 'Share with friends'.
46 | 
47 | If an additional choice were added to `choices`, some percentage of each prior choice would be allocated to the new choice, whose outcome represents an average of all groups. Comparisons between users still in the old groups (the newly added parameters may be subject to greater novelty effects.
48 | 
49 | ## Detecting problems
50 | If you suspect your experiment might have changed, check the `salt` and `checksum` fields of your log. If either of these items change, it is likely that your assignments have also changed mid-way through the experiment.
51 | 
52 | ## Learn more
53 | [Designing and Deploying Online Field Experiments](http://www-personal.umich.edu/~ebakshy/planout.pdf).  WWW 2014. Eytan Bakshy, Dean Eckles, Michael S. Bernstein.
54 | [Seven Pitfalls to Avoid when Running Controlled Experiments on the Web.](http://www.exp-platform.com/Documents/2009-ExPpitfalls.pdf) KDD 2009. Thomas Crook, Brian Frasca, Ron Kohavi, and Roger Longbotham.
55 | 


--------------------------------------------------------------------------------
/python/docs/07-language.md:
--------------------------------------------------------------------------------
 1 | # The PlanOut Language
 2 | The PlanOut language is a way to concisely define experiments.
 3 | PlanOut language code is compiled into JSON.  The language is very basic: it contains basic logical operators, conditional execution, and arrays, but does not include things like loops and function definitions. This makes it easier to statically analyze experiments, and prevents users from shooting themselves in the foot. The syntax mostly resembles JavaScript.
 4 | 
 5 | All variables set by PlanOut code are passed back via `Interpreter.get_params()` and by default, are automatically logged when used in conjunction with `SimpleInterpretedExperiment`.
 6 | 
 7 | ## Overview
 8 | * Lines are terminated with a `;`
 9 | * Arrays are defined like `[1,2,3]`, and are indexed starting at `0`.
10 | * Random assignment operators (e.g., `uniformChoice`, `weightedChoice`, `bernoulliTrial`) require named parameters, and the ordering of parameter is arbitrary.
11 | * `True` and `False` values are equivalent to `1` and `0`.
12 | * `#`s are used to write comments
13 | * You can use the `PlanOutLanguageInspector` class to validate whether all PlanOut operators use the required and optional methods.
14 | 
15 | ## Compiling PlanOut code
16 | PlanOut code can be compiled via the [Web-based compiler interface](http://facebook.github.io/planout/demo/planout-compiler.html) or using  the node.js script in the `compiler/` directory of the PlanOut github repository:
17 | 
18 | ```
19 | node compiler/planout.js planoutscriptname
20 | ```
21 | 
22 | ## Built-in operators
23 | 
24 | ### Random assignment operators
25 | The operators described in [...] can be used similar to how they are used in Python, except are lower case. The variable named given on the left hand side of an assignment operation is used as the `salt' given random assignment operators if no salt is specified manually.
26 | 
27 | ```
28 | colors = ['#aa2200', '#22aa00', '#0022aa'];
29 | x = uniformChoice(choices=colors, unit=userid); # 'x' used as salt
30 | y = uniformChoice(choices=colors, unit=userid); # 'y' used as salt, generally != x
31 | z = uniformChoice(choices=colors, unit=userid, salt='x'); # same value as x
32 | ```
33 | 
34 | 
35 | ### Array operators
36 | Arrays can include constants and variables, and can be arbitrarily nested, and can contain arbitrary types.
37 | 
38 | ```
39 | a = [4, 5, 'foo'];
40 | b = [a, 2, 3];      # evaluates to [[1,2,'foo'], 2,3]
41 | x = a[0];           # evaluates to 4
42 | y = b[0][2]         # evaluates to 'foo'
43 | l = length(b);      # evaluates to 3
44 | ```
45 | 
46 | ### Logical operators
47 | Logical operators include *and* (`&&`), *or* (`||`), *not* (`!`), as in:
48 | 
49 | ```
50 |   a = 1; b = 0; c = 1;
51 |   x = a && b;       # evaluates to False
52 |   y = a || b || c;  # evaluates to True
53 |   y = !b;           # evaluates to True
54 | ```
55 | 
56 | ### Control flow
57 | PlanOut supports if / else if / else.
58 | ```
59 | if (country == 'US') {
60 |   p = 0.2;
61 | } else if (country == 'UK') {
62 |   p = 0.4;
63 | } else {
64 |   p = 0.1;
65 | }
66 | ```
67 | 
68 | ### Arithmetic
69 | Current arithmetic operators supported in the PlanOut language are: addition, subtraction, modulo, multiplication, and division.
70 | ```
71 |  a = 2 + 3 + 4;  # 9
72 |  b = 2 * 3 * 4;  # 24
73 |  c = -2;         # -2
74 |  d = 2 + 3 - 4;  # 1
75 |  e = 4 % 2;      # 0
76 |  f = 4 / 2;      # 2.0
77 | ```
78 | 
79 | ### Other operators
80 | Other operators that are part of the core language include `min` and `max`:
81 | ```
82 | x = min(1, 2, -4);   # -4
83 | y = min([1, 2, -4])  # -4
84 | y = max(1, 2, -4)    # 2
85 | ```
86 | 


--------------------------------------------------------------------------------
/python/docs/08-about-planout.md:
--------------------------------------------------------------------------------
 1 | # About PlanOut
 2 | PlanOut was originally developed by Eytan Bakshy and Dean Eckles at Facebook as a language for  describing experimental designs. PlanOut is one of a few ways of setting up experiments at Facebook. The PlanOut interpreter runs on top of QuickExperiment, which is developed by Breno Roberto and Wesley May.
 3 | 
 4 | This open source release accompanies "Designing and Deploying Online Field Experiments", which describes the PlanOut language and the semantics of our deployment and logging infrastructure.
 5 | 
 6 | We hope that the software is useful for researchers and small businesses who want to run experiments out of the box, but have also tried to build things in a way that is easy to port and adapt to work with production systems.
 7 | 
 8 | 
 9 | ### Acknowledgements
10 | 


--------------------------------------------------------------------------------
/python/planout/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebookarchive/planout/eee764781054abb39f003133b00b88a73c8f8982/python/planout/__init__.py


--------------------------------------------------------------------------------
/python/planout/assignment.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) 2014, Facebook, Inc.
 2 | # All rights reserved.
 3 | #
 4 | # This source code is licensed under the BSD-style license found in the
 5 | # LICENSE file in the root directory of this source tree. An additional grant
 6 | # of patent rights can be found in the PATENTS file in the same directory.
 7 | 
 8 | from .ops.random import *
 9 | from .ops.base import PlanOutOp
10 | from collections import MutableMapping
11 | 
12 | 
13 | # The Assignment class is the main work horse that lets you to execute
14 | # random operators using the names of variables being assigned as salts.
15 | # It is a MutableMapping, which means it plays nice with things like Flask
16 | # template renders.
17 | class Assignment(MutableMapping):
18 | 
19 |     """
20 |     A mutable mapping that contains the result of an assign call.
21 |     """
22 | 
23 |     def __init__(self, experiment_salt, overrides={}):
24 |         self.experiment_salt = experiment_salt
25 |         self._overrides = overrides.copy()
26 |         self._data = overrides.copy()
27 |         self.salt_sep = '.' # separates unit from experiment/variable salt
28 | 
29 |     def evaluate(self, value):
30 |         return value
31 | 
32 |     def get_overrides(self):
33 |         return self._overrides
34 | 
35 |     def set_overrides(self, overrides):
36 |         # maybe this should be a deep copy?
37 |         self._overrides = overrides.copy()
38 |         for var in self._overrides:
39 |             self._data[var] = self._overrides[var]
40 | 
41 |     def __setitem__(self, name, value):
42 |         if name in ('_data', '_overrides', 'salt_sep', 'experiment_salt'):
43 |             self.__dict__[name] = value
44 |             return
45 | 
46 |         if name in self._overrides:
47 |             return
48 | 
49 |         if isinstance(value, PlanOutOpRandom):
50 |             if 'salt' not in value.args:
51 |                 value.args['salt'] = name
52 |             self._data[name] = value.execute(self)
53 |         else:
54 |             self._data[name] = value
55 | 
56 |     __setattr__ = __setitem__
57 | 
58 |     def __getitem__(self, name):
59 |         if name in ('_data', '_overrides', 'experiment_salt'):
60 |             return self.__dict__[name]
61 |         else:
62 |             return self._data[name]
63 | 
64 |     __getattr__ = __getitem__
65 | 
66 |     def __delitem__(self, name):
67 |         del self._data[name]
68 | 
69 |     def __iter__(self):
70 |         return iter(self._data)
71 | 
72 |     def __len__(self):
73 |         return len(self._data)
74 | 
75 |     def __str__(self):
76 |         return str(self._data)
77 | 


--------------------------------------------------------------------------------
/python/planout/interpreter.py:
--------------------------------------------------------------------------------
  1 | # Copyright (c) 2014, Facebook, Inc.
  2 | # All rights reserved.
  3 | #
  4 | # This source code is licensed under the BSD-style license found in the
  5 | # LICENSE file in the root directory of this source tree. An additional grant
  6 | # of patent rights can be found in the PATENTS file in the same directory.
  7 | 
  8 | from copy import deepcopy
  9 | from .ops.utils import Operators, StopPlanOutException
 10 | from .assignment import Assignment
 11 | 
 12 | 
 13 | Operators.initFactory()
 14 | 
 15 | 
 16 | class Interpreter(object):
 17 | 
 18 |     """PlanOut interpreter"""
 19 | 
 20 |     def __init__(self, serialization, experiment_salt='global_salt',
 21 |                  inputs={}, environment=None):
 22 |         self._serialization = serialization
 23 |         if environment is None:
 24 |             self._env = Assignment(experiment_salt)
 25 |         else:
 26 |             self._env = environment
 27 |         self.experiment_salt = self._experiment_salt = experiment_salt
 28 |         self._evaluated = False
 29 |         self._in_experiment = True
 30 |         self._inputs = inputs.copy()
 31 | 
 32 |     def register_operators(self, operators):
 33 |         Operators.registerOperators(operators)
 34 |         return self
 35 | 
 36 |     def get_params(self):
 37 |         """Get all assigned parameter values from an executed interpreter script"""
 38 |         # evaluate code if it hasn't already been evaluated
 39 |         if not self._evaluated:
 40 |             try:
 41 |                 self.evaluate(self._serialization)
 42 |             except StopPlanOutException as e:
 43 |                 # StopPlanOutException is raised when script calls "return", which
 44 |                 # short circuits execution and sets in_experiment
 45 |                 self._in_experiment = e.in_experiment
 46 |             self._evaluated = True
 47 |         return self._env
 48 | 
 49 |     @property
 50 |     def in_experiment(self):
 51 |         return self._in_experiment
 52 | 
 53 |     @property
 54 |     def salt_sep(self):
 55 |         return self._env.salt_sep
 56 | 
 57 |     def set_env(self, new_env):
 58 |         """Replace the current environment with a dictionary"""
 59 |         self._env = deepcopy(new_env)
 60 |         # note that overrides are inhereted from new_env
 61 |         return self
 62 | 
 63 |     def has(self, name):
 64 |         """Check if a variable exists in the PlanOut environment"""
 65 |         return name in self._env
 66 | 
 67 |     def get(self, name, default=None):
 68 |         """Get a variable from the PlanOut environment"""
 69 |         return self._env.get(name, self._inputs.get(name, default))
 70 | 
 71 |     def set(self, name, value):
 72 |         """Set a variable in the PlanOut environment"""
 73 |         self._env[name] = value
 74 |         return self
 75 | 
 76 |     def set_overrides(self, overrides):
 77 |         """
 78 |         Sets variables to maintain a frozen state during the interpreter's
 79 |         execution. This is useful for debugging PlanOut scripts.
 80 |         """
 81 |         self._env.set_overrides(overrides)
 82 |         return self
 83 | 
 84 |     def get_overrides(self):
 85 |         """Get a dictionary of all overrided values"""
 86 |         return self._env.get_overrides()
 87 | 
 88 |     def has_override(self, name):
 89 |         """Check to see if a variable has an override."""
 90 |         return name in self.get_overrides()
 91 | 
 92 |     def evaluate(self, planout_code):
 93 |         """Recursively evaluate PlanOut interpreter code"""
 94 |         # if the object is a PlanOut operator, execute it it.
 95 |         if type(planout_code) is dict and 'op' in planout_code:
 96 |             return Operators.operatorInstance(planout_code).execute(self)
 97 |         # if the object is a list, iterate over the list and evaluate each
 98 |         # element
 99 |         elif type(planout_code) is list:
100 |             return [self.evaluate(i) for i in planout_code]
101 |         else:
102 |             return planout_code  # data is a literal
103 | 


--------------------------------------------------------------------------------
/python/planout/namespace.py:
--------------------------------------------------------------------------------
  1 | import six
  2 | from abc import ABCMeta, abstractmethod, abstractproperty
  3 | from operator import itemgetter
  4 | 
  5 | from .experiment import Experiment, DefaultExperiment
  6 | from .ops.random import Sample, FastSample, RandomInteger
  7 | from .assignment import Assignment
  8 | 
  9 | # decorator for methods that assume assignments have been made
 10 | 
 11 | 
 12 | def requires_experiment(f):
 13 |     def wrapped_f(self, *args, **kwargs):
 14 |         if not self._experiment:
 15 |             self._assign_experiment()
 16 |         return f(self, *args, **kwargs)
 17 |     return wrapped_f
 18 | 
 19 | 
 20 | def requires_default_experiment(f):
 21 |     def wrapped_f(self, *args, **kwargs):
 22 |         if not self._default_experiment:
 23 |             self._assign_default_experiment()
 24 |         return f(self, *args, **kwargs)
 25 |     return wrapped_f
 26 | 
 27 | 
 28 | class Namespace(object):
 29 |     __metaclass__ = ABCMeta
 30 | 
 31 |     def __init__(self, **kwargs):
 32 |         pass
 33 | 
 34 |     @abstractmethod
 35 |     def add_experiment(self, name, exp_object, num_segments, **kwargs):
 36 |         pass
 37 | 
 38 |     @abstractmethod
 39 |     def remove_experiment(self, name):
 40 |         pass
 41 | 
 42 |     @abstractmethod
 43 |     def set_auto_exposure_logging(self, value):
 44 |         pass
 45 | 
 46 |     @abstractproperty
 47 |     def in_experiment(self):
 48 |         pass
 49 | 
 50 |     @abstractmethod
 51 |     def get(self, name, default):
 52 |         pass
 53 | 
 54 |     @abstractmethod
 55 |     def log_exposure(self, extras=None):
 56 |         pass
 57 | 
 58 |     @abstractmethod
 59 |     def log_event(self, event_type, extras=None):
 60 |         pass
 61 | 
 62 | 
 63 | class SimpleNamespace(Namespace):
 64 |     __metaclass__ = ABCMeta
 65 | 
 66 |     def __init__(self, **kwargs):
 67 |         self.name = self.__class__  # default name is the class name
 68 |         self.inputs = kwargs
 69 |         self.num_segments = None
 70 | 
 71 |         # dictionary mapping segments to experiment names
 72 |         self.segment_allocations = {}
 73 | 
 74 |         # dictionary mapping experiment names to experiment objects
 75 |         self.current_experiments = {}
 76 | 
 77 |         self._experiment = None          # memoized experiment object
 78 |         self._default_experiment = None  # memoized default experiment object
 79 |         self.default_experiment_class = DefaultExperiment
 80 |         self._in_experiment = False
 81 | 
 82 |         # setup name, primary key, number of segments, etc
 83 |         self.setup()
 84 |         self.available_segments = set(range(self.num_segments))
 85 | 
 86 |         # load namespace with experiments
 87 |         self.setup_experiments()
 88 | 
 89 |     @abstractmethod
 90 |     def setup(self):
 91 |         """Sets up experiment"""
 92 |         # Developers extending this class should set the following variables
 93 |         # self.name = 'sample namespace'
 94 |         # self.primary_unit = 'userid'
 95 |         # self.num_segments = 10000
 96 |         pass
 97 | 
 98 |     @abstractmethod
 99 |     def setup_experiments(self):
100 |         # e.g.,
101 |         # self.add_experiment('first experiment', Exp1, 100)
102 |         pass
103 | 
104 |     @property
105 |     def primary_unit(self):
106 |         return self._primary_unit
107 | 
108 |     @primary_unit.setter
109 |     def primary_unit(self, value):
110 |         # later on we require that the primary key is a list, so we use
111 |         # a setter to convert strings to a single element list
112 |         if type(value) is list:
113 |             self._primary_unit = value
114 |         else:
115 |             self._primary_unit = [value]
116 | 
117 |     def add_experiment(self, name, exp_object, segments):
118 |         num_avail = len(self.available_segments)
119 |         if num_avail < segments:
120 |             print('error: %s segments requested, only %s available.' %
121 |                   (segments, num_avail))
122 |             return False
123 |         if name in self.current_experiments:
124 |             print('error: there is already an experiment called %s.' % name)
125 |             return False
126 | 
127 |         # randomly select the given number of segments from all available
128 |         # segments
129 |         a = Assignment(self.name)
130 |         if 'use_fast_sample' in self.inputs:
131 |             a.sampled_segments = \
132 |                 FastSample(choices=list(self.available_segments),
133 |                     draws=segments, unit=name)
134 |         else:
135 |             a.sampled_segments = \
136 |                 Sample(choices=list(self.available_segments),
137 |                        draws=segments, unit=name)
138 | 
139 |         # assign each segment to the experiment name
140 |         for segment in a.sampled_segments:
141 |             self.segment_allocations[segment] = name
142 |             self.available_segments.remove(segment)
143 | 
144 |         # associate the experiment name with an object
145 |         self.current_experiments[name] = exp_object
146 | 
147 |     def remove_experiment(self, name):
148 |         if name not in self.current_experiments:
149 |             print('error: there is no experiment called %s.' % name)
150 |             return False
151 | 
152 |         segments_to_free = \
153 |             [s for s, n in six.iteritems(self.segment_allocations) if n == name]
154 | 
155 |         for segment in segments_to_free:
156 |             del self.segment_allocations[segment]
157 |             self.available_segments.add(segment)
158 |         del self.current_experiments[name]
159 | 
160 |         return True
161 | 
162 |     def get_segment(self):
163 |         # randomly assign primary unit to a segment
164 |         a = Assignment(self.name)
165 |         a.segment = RandomInteger(min=0, max=self.num_segments - 1,
166 |                                   unit=itemgetter(*self.primary_unit)(self.inputs))
167 |         return a.segment
168 | 
169 |     def _assign_experiment(self):
170 |         "assign primary unit to an experiment"
171 |         segment = self.get_segment()
172 |         # is the unit allocated to an experiment?
173 |         if segment in self.segment_allocations:
174 |             experiment_name = self.segment_allocations[segment]
175 |             experiment = self.current_experiments[
176 |                 experiment_name](**self.inputs)
177 |             experiment.name = '%s-%s' % (self.name, experiment_name)
178 |             experiment.salt = '%s.%s' % (self.name, experiment_name)
179 |             self._experiment = experiment
180 |             self._in_experiment = experiment.in_experiment
181 |         # if the unit does not belong to an experiment, or the unit has been
182 |         # disqualified from being in the assigned experiment, fall back on the
183 |         # default experiment
184 |         if not self._in_experiment:
185 |             self._assign_default_experiment()
186 | 
187 |     def _assign_default_experiment(self):
188 |         self._default_experiment = self.default_experiment_class(**self.inputs)
189 | 
190 |     @requires_default_experiment
191 |     def default_get(self, name, default=None):
192 |         return self._default_experiment.get(name, default)
193 | 
194 |     @property
195 |     @requires_experiment
196 |     def in_experiment(self):
197 |         return self._in_experiment
198 | 
199 |     @in_experiment.setter
200 |     def in_experiment(self, value):
201 |         # in_experiment cannot be externally modified
202 |         pass
203 | 
204 |     @requires_experiment
205 |     def set_auto_exposure_logging(self, value):
206 |         self._experiment.set_auto_exposure_logging(value)
207 | 
208 |     @requires_experiment
209 |     def get(self, name, default=None):
210 |         if self._experiment is None:
211 |             return self.default_get(name, default)
212 |         else:
213 |             return self._experiment.get(name, self.default_get(name, default))
214 | 
215 |     @requires_experiment
216 |     def log_exposure(self, extras=None):
217 |         """Logs exposure to treatment"""
218 |         if self._experiment is None:
219 |             pass
220 |         self._experiment.log_exposure(extras)
221 | 
222 |     @requires_experiment
223 |     def log_event(self, event_type, extras=None):
224 |         """Log an arbitrary event"""
225 |         if self._experiment is None:
226 |             pass
227 |         self._experiment.log_event(event_type, extras)
228 | 


--------------------------------------------------------------------------------
/python/planout/ops/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebookarchive/planout/eee764781054abb39f003133b00b88a73c8f8982/python/planout/ops/__init__.py


--------------------------------------------------------------------------------
/python/planout/ops/base.py:
--------------------------------------------------------------------------------
  1 | # Copyright (c) 2014, Facebook, Inc.
  2 | # All rights reserved.
  3 | #
  4 | # This source code is licensed under the BSD-style license found in the
  5 | # LICENSE file in the root directory of this source tree. An additional grant
  6 | # of patent rights can be found in the PATENTS file in the same directory.
  7 | 
  8 | import logging
  9 | from abc import ABCMeta, abstractmethod
 10 | import six
 11 | 
 12 | from .utils import Operators
 13 | 
 14 | 
 15 | class PlanOutOp(object):
 16 | 
 17 |     """Abstract base class for PlanOut Operators"""
 18 |     __metaclass__ = ABCMeta
 19 |     # all PlanOut operator have some set of args that act as required and
 20 |     # optional arguments
 21 | 
 22 |     def __init__(self, **args):
 23 |         self.args = args
 24 | 
 25 |     # all PlanOut operators must implement execute
 26 |     @abstractmethod
 27 |     def execute(self, mapper):
 28 |         pass
 29 | 
 30 |     def prettyArgs(self):
 31 |         return Operators.prettyParamFormat(self.args)
 32 | 
 33 |     def pretty(self):
 34 |         return '%s(%s)' % (self.args['op'], self.prettyArgs())
 35 | 
 36 |     def getArgMixed(self, name):
 37 |         assert name in self.args, \
 38 |             "%s: missing argument: %s." % (self.__class__, name)
 39 |         return self.args[name]
 40 | 
 41 |     def getArgInt(self, name):
 42 |         arg = self.getArgMixed(name)
 43 |         assert isinstance(arg, six.integer_types), \
 44 |             "%s: %s must be an int." % (self.__class__, name)
 45 |         return arg
 46 | 
 47 |     def getArgFloat(self, name):
 48 |         arg = self.getArgMixed(name)
 49 |         assert isinstance(arg, (six.integer_types, float)), \
 50 |             "%s: %s must be a number." % (self.__class__, name)
 51 |         return float(arg)
 52 | 
 53 |     def getArgString(self, name):
 54 |         arg = self.getArgMixed(name)
 55 |         assert isinstance(arg, six.string_types), \
 56 |             "%s: %s must be a string." % (self.__class__, name)
 57 |         return arg
 58 | 
 59 |     def getArgNumeric(self, name):
 60 |         arg = self.getArgMixed(name)
 61 |         assert isinstance(arg, (six.integer_types, float)), \
 62 |             "%s: %s must be a numeric." % (self.__class__, name)
 63 |         return arg
 64 | 
 65 |     def getArgList(self, name):
 66 |         arg = self.getArgMixed(name)
 67 |         assert isinstance(arg, (list, tuple)), \
 68 |             "%s: %s must be a list." % (self.__class__, name)
 69 |         return arg
 70 | 
 71 |     def getArgMap(self, name):
 72 |         arg = self.getArgMixed(name)
 73 |         assert isinstance(arg, dict), \
 74 |             "%s: %s must be a map." % (self.__class__, name)
 75 |         return arg
 76 | 
 77 |     def getArgIndexish(self, name):
 78 |         arg = self.getArgMixed(name)
 79 |         assert isinstance(arg, (dict, list, tuple)), \
 80 |             "%s: %s must be a map or list." % (self.__class__, name)
 81 |         return arg
 82 | 
 83 | # PlanOutOpSimple is the easiest way to implement simple operators.
 84 | # The class automatically evaluates the values of all args passed in via
 85 | # execute(), and stores the PlanOut mapper object and evaluated
 86 | # args as instance variables.  The user can then extend PlanOutOpSimple
 87 | # and implement simpleExecute().
 88 | 
 89 | class PlanOutOpSimple(PlanOutOp):
 90 |     __metaclass__ = ABCMeta
 91 | 
 92 |     def execute(self, mapper):
 93 |         self.mapper = mapper
 94 |         parameter_names = self.args.keys()
 95 |         for param in parameter_names:
 96 |             self.args[param] = mapper.evaluate(self.args[param])
 97 |         return self.simpleExecute()
 98 | 
 99 | 
100 | class PlanOutOpBinary(PlanOutOpSimple):
101 |     __metaclass__ = ABCMeta
102 | 
103 |     def simpleExecute(self):
104 |         return self.binaryExecute(
105 |             self.getArgMixed('left'),
106 |             self.getArgMixed('right'))
107 | 
108 |     def pretty(self):
109 |         return '%s %s %s' % (
110 |             Operators.pretty(self.args['left']),
111 |             self.getInfixString(),
112 |             Operators.pretty(self.args['right']))
113 | 
114 |     def getInfixString(self):
115 |         return self.args['op']
116 | 
117 |     @abstractmethod
118 |     def binaryExecute(self, left, right):
119 |         pass
120 | 
121 | 
122 | class PlanOutOpUnary(PlanOutOpSimple):
123 |     __metaclass__ = ABCMeta
124 | 
125 |     def simpleExecute(self):
126 |         return self.unaryExecute(self.getArgMixed('value'))
127 | 
128 |     def pretty(self):
129 |         return self.getUnaryString + Operators.pretty(self.getArgMixed('value'))
130 | 
131 |     def getUnaryString(self):
132 |         return self.args['op']
133 | 
134 |     @abstractmethod
135 |     def unaryExecute(self, value):
136 |         pass
137 | 
138 | 
139 | class PlanOutOpCommutative(PlanOutOpSimple):
140 |     __metaclass__ = ABCMeta
141 | 
142 |     def simpleExecute(self):
143 |         assert ('values' in self.args), "expected argument 'values'"
144 |         return self.commutativeExecute(self.getArgList('values'))
145 | 
146 |     def pretty(self):
147 |         values = Operators.strip_array(self.getArgList('values'))
148 |         if type(values) is list:
149 |             pretty_values = ', '.join([Operators.pretty(i) for i in values])
150 |         else:
151 |             pretty_values = Operators.pretty(values)
152 | 
153 |         return '%s(%s)' % (self.getCommutativeString(), pretty_values)
154 | 
155 |     def getCommutativeString(self):
156 |         return self.args['op']
157 | 
158 |     @abstractmethod
159 |     def commutativeExecute(self, values):
160 |         pass
161 | 


--------------------------------------------------------------------------------
/python/planout/ops/random.py:
--------------------------------------------------------------------------------
  1 | import hashlib
  2 | import six
  3 | from .base import PlanOutOpSimple
  4 | 
  5 | 
  6 | class PlanOutOpRandom(PlanOutOpSimple):
  7 |     LONG_SCALE = float(0xFFFFFFFFFFFFFFF)
  8 | 
  9 |     def getUnit(self, appended_unit=None):
 10 |         unit = self.getArgMixed('unit')
 11 |         if type(unit) is not list:
 12 |             unit = [unit]
 13 |         if appended_unit is not None:
 14 |             unit += [appended_unit]
 15 |         return unit
 16 | 
 17 |     def getHash(self, appended_unit=None):
 18 |         if 'full_salt' in self.args:
 19 |             full_salt = self.getArgString('full_salt') + '.'  # do typechecking
 20 |         else:
 21 |             full_salt = '%s.%s%s' % (
 22 |                 self.mapper.experiment_salt,
 23 |                 self.getArgString('salt'),
 24 |                 self.mapper.salt_sep)
 25 | 
 26 |         unit_str = '.'.join(map(str, self.getUnit(appended_unit)))
 27 |         hash_str = '%s%s' % (full_salt, unit_str)
 28 |         if not isinstance(hash_str, six.binary_type):
 29 |             hash_str = hash_str.encode("ascii")
 30 |         return int(hashlib.sha1(hash_str).hexdigest()[:15], 16)
 31 | 
 32 |     def getUniform(self, min_val=0.0, max_val=1.0, appended_unit=None):
 33 |         zero_to_one = self.getHash(appended_unit) / PlanOutOpRandom.LONG_SCALE
 34 |         return min_val + (max_val - min_val) * zero_to_one
 35 | 
 36 | 
 37 | class RandomFloat(PlanOutOpRandom):
 38 | 
 39 |     def simpleExecute(self):
 40 |         min_val = self.getArgFloat('min')
 41 |         max_val = self.getArgFloat('max')
 42 | 
 43 |         return self.getUniform(min_val, max_val)
 44 | 
 45 | 
 46 | class RandomInteger(PlanOutOpRandom):
 47 | 
 48 |     def simpleExecute(self):
 49 |         min_val = self.getArgInt('min')
 50 |         max_val = self.getArgInt('max')
 51 | 
 52 |         return min_val + self.getHash() % (max_val - min_val + 1)
 53 | 
 54 | 
 55 | class BernoulliTrial(PlanOutOpRandom):
 56 | 
 57 |     def simpleExecute(self):
 58 |         p = self.getArgNumeric('p')
 59 |         assert p >= 0 and p <= 1.0, \
 60 |             '%s: p must be a number between 0.0 and 1.0, not %s!' \
 61 |             % (self.__class__, p)
 62 | 
 63 |         rand_val = self.getUniform(0.0, 1.0)
 64 |         return 1 if rand_val <= p else 0
 65 | 
 66 | 
 67 | class BernoulliFilter(PlanOutOpRandom):
 68 | 
 69 |     def simpleExecute(self):
 70 |         p = self.getArgNumeric('p')
 71 |         values = self.getArgList('choices')
 72 |         assert p >= 0 and p <= 1.0, \
 73 |             '%s: p must be a number between 0.0 and 1.0, not %s!' \
 74 |             % (self.__class__, p)
 75 | 
 76 |         if len(values) == 0:
 77 |             return []
 78 |         return [i for i in values if self.getUniform(0.0, 1.0, i) <= p]
 79 | 
 80 | 
 81 | class UniformChoice(PlanOutOpRandom):
 82 | 
 83 |     def simpleExecute(self):
 84 |         choices = self.getArgList('choices')
 85 | 
 86 |         if len(choices) == 0:
 87 |             return []
 88 |         rand_index = self.getHash() % len(choices)
 89 |         return choices[rand_index]
 90 | 
 91 | 
 92 | class WeightedChoice(PlanOutOpRandom):
 93 | 
 94 |     def simpleExecute(self):
 95 |         choices = self.getArgList('choices')
 96 |         weights = self.getArgList('weights')
 97 | 
 98 |         if len(choices) == 0:
 99 |             return []
100 |         cum_weights = dict(enumerate(weights))
101 |         cum_sum = 0.0
102 |         for index in cum_weights:
103 |             cum_sum += cum_weights[index]
104 |             cum_weights[index] = cum_sum
105 |         stop_value = self.getUniform(0.0, cum_sum)
106 |         for index in cum_weights:
107 |             if stop_value <= cum_weights[index]:
108 |                 return choices[index]
109 | 
110 | 
111 | class BaseSample(PlanOutOpRandom):
112 | 
113 |     def copyChoices(self):
114 |         return [x for x in self.getArgList('choices')]
115 | 
116 |     def getNumDraws(self, choices):
117 |         if 'draws' in self.args:
118 |             num_draws = self.getArgInt('draws')
119 |             assert num_draws <= len(choices), \
120 |                 "%s: cannot make %s draws when only %s choices are available" \
121 |                 % (self.__class__, num_draws, len(choices))
122 |             return num_draws
123 |         else:
124 |             return len(choices)
125 | 
126 | class FastSample(BaseSample):
127 | 
128 |     def simpleExecute(self):
129 |         choices = self.copyChoices()
130 |         num_draws = self.getNumDraws(choices)
131 |         stopping_point = len(choices) - num_draws
132 | 
133 |         for i in six.moves.range(len(choices) - 1, 0, -1):
134 |             j = self.getHash(i) % (i + 1)
135 |             choices[i], choices[j] = choices[j], choices[i]
136 |             if stopping_point == i:
137 |                 return choices[i:]
138 |         return choices[:num_draws]
139 | 
140 | class Sample(BaseSample):
141 | 
142 |     def simpleExecute(self):
143 |         choices = self.copyChoices()
144 |         num_draws = self.getNumDraws(choices)
145 | 
146 |         for i in six.moves.range(len(choices) - 1, 0, -1):
147 |             j = self.getHash(i) % (i + 1)
148 |             choices[i], choices[j] = choices[j], choices[i]
149 |         return choices[:num_draws]
150 | 


--------------------------------------------------------------------------------
/python/planout/ops/utils.py:
--------------------------------------------------------------------------------
  1 | import json
  2 | import six
  3 | 
  4 | class StopPlanOutException(Exception):
  5 | 
  6 |     """Exception that gets raised when "return" op is evaluated"""
  7 | 
  8 |     def __init__(self, in_experiment):
  9 |         self.in_experiment = in_experiment
 10 | 
 11 | 
 12 | class Operators():
 13 |     """Singleton class for inspecting and registering operators"""
 14 | 
 15 |     @staticmethod
 16 |     def initFactory():
 17 |         from . import core, random
 18 |         Operators.operators = {
 19 |             "literal": core.Literal,
 20 |             "get": core.Get,
 21 |             "seq": core.Seq,
 22 |             "set": core.Set,
 23 |             "return": core.Return,
 24 |             "index": core.Index,
 25 |             "array": core.Array,
 26 |             "map": core.Map,
 27 |             "equals": core.Equals,
 28 |             "cond": core.Cond,
 29 |             "and": core.And,
 30 |             "or": core.Or,
 31 |             ">": core.GreaterThan,
 32 |             "<": core.LessThan,
 33 |             ">=": core.GreaterThanOrEqualTo,
 34 |             "<=": core.LessThanOrEqualTo,
 35 |             "%": core.Mod,
 36 |             "/": core.Divide,
 37 |             "not": core.Not,
 38 |             "round": core.Round,
 39 |             "negative": core.Negative,
 40 |             "min": core.Min,
 41 |             "max": core.Max,
 42 |             "length": core.Length,
 43 |             "coalesce": core.Coalesce,
 44 |             "product": core.Product,
 45 |             "sum": core.Sum,
 46 |             "exp": core.Exp,
 47 |             "sqrt": core.Sqrt,
 48 |             "randomFloat": random.RandomFloat,
 49 |             "randomInteger": random.RandomInteger,
 50 |             "bernoulliTrial": random.BernoulliTrial,
 51 |             "bernoulliFilter": random.BernoulliFilter,
 52 |             "uniformChoice": random.UniformChoice,
 53 |             "weightedChoice": random.WeightedChoice,
 54 |             "sample": random.Sample,
 55 |             "fastSample": random.FastSample
 56 |         }
 57 | 
 58 |     @staticmethod
 59 |     def registerOperators(operators):
 60 |         for op, obj in six.iteritems(operators):
 61 |             assert op not in Operators.operators
 62 |             Operators.operators[op] = operators[op]
 63 | 
 64 |     @staticmethod
 65 |     def isOperator(op):
 66 |         return type(op) is dict and "op" in op
 67 | 
 68 |     @staticmethod
 69 |     def operatorInstance(params):
 70 |         op = params['op']
 71 |         assert (op in Operators.operators), "Unknown operator: %s" % op
 72 |         return Operators.operators[op](**params)
 73 | 
 74 |     @staticmethod
 75 |     def prettyParamFormat(params):
 76 |         ps = [p + '=' + Operators.pretty(params[p])
 77 |               for p in params if p != 'op']
 78 |         return ', '.join(ps)
 79 | 
 80 |     @staticmethod
 81 |     def strip_array(params):
 82 |         if type(params) is list:
 83 |             return params
 84 |         if type(params) is dict and params.get('op', None) == 'array':
 85 |             return params['values']
 86 |         else:
 87 |             return params
 88 | 
 89 |     @staticmethod
 90 |     def pretty(params):
 91 |         if Operators.isOperator(params):
 92 |             try:
 93 |                 # if an op is invalid, we may not be able to pretty print it
 94 |                 my_pretty = Operators.operatorInstance(params).pretty()
 95 |             except:
 96 |                 my_pretty = params
 97 |             return my_pretty
 98 |         elif type(params) is list:
 99 |             return '[%s]' % ', '.join([Operators.pretty(p) for p in params])
100 |         else:
101 |             return json.dumps(params)
102 | 


--------------------------------------------------------------------------------
/python/planout/test/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/facebookarchive/planout/eee764781054abb39f003133b00b88a73c8f8982/python/planout/test/__init__.py


--------------------------------------------------------------------------------
/python/planout/test/test_assignment.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) 2014, Facebook, Inc.
 2 | # All rights reserved.
 3 | #
 4 | # This source code is licensed under the BSD-style license found in the
 5 | # LICENSE file in the root directory of this source tree. An additional grant
 6 | # of patent rights can be found in the PATENTS file in the same directory.
 7 | 
 8 | import unittest
 9 | 
10 | from planout.assignment import Assignment
11 | from planout.ops.random import UniformChoice
12 | 
13 | 
14 | class AssignmentTest(unittest.TestCase):
15 |     tester_unit = 4
16 |     tester_salt = 'test_salt'
17 | 
18 |     def test_set_get_constant(self):
19 |         a = Assignment(self.tester_salt)
20 |         a.foo = 12
21 |         self.assertEqual(a.foo, 12)
22 | 
23 |     def test_set_get_uniform(self):
24 |         a = Assignment(self.tester_salt)
25 |         a.foo = UniformChoice(choices=['a', 'b'], unit=self.tester_unit)
26 |         a.bar = UniformChoice(choices=['a', 'b'], unit=self.tester_unit)
27 |         a.baz = UniformChoice(choices=['a', 'b'], unit=self.tester_unit)
28 |         self.assertEqual(a.foo, 'b')
29 |         self.assertEqual(a.bar, 'a')
30 |         self.assertEqual(a.baz, 'a')
31 | 
32 |     def test_overrides(self):
33 |         a = Assignment(self.tester_salt)
34 |         a.set_overrides({'x': 42, 'y': 43})
35 |         a.x = 5
36 |         a.y = 6
37 |         self.assertEqual(a.x, 42)
38 |         self.assertEqual(a.y, 43)
39 | 
40 |     def test_custom_salt(self):
41 |         a = Assignment(self.tester_salt)
42 |         custom_salt = lambda x,y: '%s-%s' % (x,y)
43 |         a.foo = UniformChoice(choices=list(range(8)), unit=self.tester_unit)
44 |         self.assertEqual(a.foo, 7)
45 | 
46 | if __name__ == '__main__':
47 |     unittest.main()
48 | 


--------------------------------------------------------------------------------
/python/planout/test/test_interpreter.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) 2014, Facebook, Inc.
 2 | # All rights reserved.
 3 | #
 4 | # This source code is licensed under the BSD-style license found in the
 5 | # LICENSE file in the root directory of this source tree. An additional grant
 6 | # of patent rights can be found in the PATENTS file in the same directory.
 7 | 
 8 | import json
 9 | import unittest
10 | 
11 | from planout.interpreter import Interpreter
12 | 
13 | 
14 | class InterpreterTest(unittest.TestCase):
15 |     compiled = json.loads("""
16 | {"op":"seq","seq":[{"op":"set","var":"group_size","value":{"choices":{"op":"array","values":[1,10]},"unit":{"op":"get","var":"userid"},"op":"uniformChoice"}},{"op":"set","var":"specific_goal","value":{"p":0.8,"unit":{"op":"get","var":"userid"},"op":"bernoulliTrial"}},{"op":"cond","cond":[{"if":{"op":"get","var":"specific_goal"},"then":{"op":"seq","seq":[{"op":"set","var":"ratings_per_user_goal","value":{"choices":{"op":"array","values":[8,16,32,64]},"unit":{"op":"get","var":"userid"},"op":"uniformChoice"}},{"op":"set","var":"ratings_goal","value":{"op":"product","values":[{"op":"get","var":"group_size"},{"op":"get","var":"ratings_per_user_goal"}]}}]}}]}]}
17 |   """)
18 |     interpreter_salt = 'foo'
19 | 
20 |     def test_interpreter(self):
21 |         proc = Interpreter(
22 |             self.compiled, self.interpreter_salt, {'userid': 123454})
23 |         params = proc.get_params()
24 |         self.assertEqual(proc.get_params().get('specific_goal'), 1)
25 |         self.assertEqual(proc.get_params().get('ratings_goal'), 320)
26 | 
27 |     def test_interpreter_overrides(self):
28 |         # test overriding a parameter that gets set by the experiment
29 |         proc = Interpreter(
30 |             self.compiled, self.interpreter_salt, {'userid': 123454})
31 |         proc.set_overrides({'specific_goal': 0})
32 |         self.assertEqual(proc.get_params().get('specific_goal'), 0)
33 |         self.assertEqual(proc.get_params().get('ratings_goal'), None)
34 | 
35 |         # test to make sure input data can also be overridden
36 |         proc = Interpreter(
37 |             self.compiled, self.interpreter_salt, {'userid': 123453})
38 |         proc.set_overrides({'userid': 123454})
39 |         self.assertEqual(proc.get_params().get('specific_goal'), 1)
40 | 
41 |     def test_register_ops(self):
42 |         from planout.ops.base import PlanOutOpCommutative
43 |         class CustomOp(PlanOutOpCommutative):
44 |             def commutativeExecute(self, values):
45 |                 return sum(values)
46 | 
47 |         custom_op_script = {"op":"seq","seq":[{"op":"set","var":"x","value":{"values":[2,4],"op":"customOp"}}]}
48 |         proc = Interpreter(
49 |             custom_op_script, self.interpreter_salt, {'userid': 123454})
50 | 
51 |         proc.register_operators({'customOp': CustomOp})
52 |         self.assertEqual(proc.get_params().get('x'), 6)
53 | 
54 | 
55 | 
56 | if __name__ == '__main__':
57 |     unittest.main()
58 | 


--------------------------------------------------------------------------------
/python/planout/test/test_namespace.py:
--------------------------------------------------------------------------------
 1 | import unittest
 2 | 
 3 | from planout.namespace import SimpleNamespace
 4 | from planout.experiment import DefaultExperiment
 5 | 
 6 | 
 7 | class VanillaExperiment(DefaultExperiment):
 8 |     def setup(self):
 9 |         self.name = 'test_name'
10 | 
11 |     def assign(self, params, i):
12 |         params.foo = 'bar'
13 | 
14 | 
15 | class DefaultExperiment(DefaultExperiment):
16 |     def get_default_params(self):
17 |         return {'foo': 'default'}
18 | 
19 | 
20 | class NamespaceTest(unittest.TestCase):
21 |     def test_namespace_remove_experiment(self):
22 |         class TestVanillaNamespace(SimpleNamespace):
23 |             def setup(self):
24 |                 self.name = 'test_namespace'
25 |                 self.primary_unit = 'i'
26 |                 self.num_segments = 100
27 |                 self.default_experiment_class = DefaultExperiment
28 | 
29 |             def setup_experiments(self):
30 |                 self.add_experiment('test_name', VanillaExperiment, 100)
31 |                 self.remove_experiment('test_name')
32 | 
33 |         assert TestVanillaNamespace(i=1).get('foo') == 'default'
34 | 
35 |     def test_namespace_add_experiment(self):
36 |         class TestVanillaNamespace(SimpleNamespace):
37 |             def setup(self):
38 |                 self.name = 'test_namespace'
39 |                 self.primary_unit = 'i'
40 |                 self.num_segments = 100
41 |                 self.default_experiment_class = DefaultExperiment
42 | 
43 |             def setup_experiments(self):
44 |                 self.add_experiment('test_name', VanillaExperiment, 100)
45 | 
46 |         assert TestVanillaNamespace(i=1).get('foo') == 'bar'
47 | 
48 | 
49 | if __name__ == '__main__':
50 |     unittest.main()
51 | 


--------------------------------------------------------------------------------
/python/setup.py:
--------------------------------------------------------------------------------
 1 | from distutils.core import setup
 2 | 
 3 | setup(
 4 |     name='PlanOut',
 5 |     version='0.6.0',
 6 |     author='Facebook, Inc.',
 7 |     author_email='eytan@fb.com',
 8 |     packages=[
 9 |         'planout',
10 |         'planout.ops',
11 |         'planout.test'
12 |     ],
13 |     requires=[
14 |         'six'
15 |     ],
16 |     url='http://pypi.python.org/pypi/PlanOut/',
17 |     license='LICENSE',
18 |     description='PlanOut is a framework for online field experimentation.',
19 |     keywords=['experimentation', 'A/B testing'],
20 |     classifiers=[
21 |         'Development Status :: 5 - Production/Stable',
22 |         'Intended Audience :: Developers',
23 |         'License :: OSI Approved :: BSD License',
24 |         'Programming Language :: Python',
25 |         'Programming Language :: Python :: 2',
26 |         'Programming Language :: Python :: 2.7',
27 |         'Programming Language :: Python :: 3',
28 |         'Programming Language :: Python :: 3.4',
29 |         'Programming Language :: Python :: 3.5',
30 |         'Topic :: Software Development :: Libraries',
31 |         'Topic :: Software Development :: Testing',
32 |     ],
33 |     long_description="""PlanOut is a framework for online field experimentation.
34 |     PlanOut makes it easy to design both simple A/B tests and more complex
35 |     experiments, including multi-factorial designs and within-subjects designs.
36 |     It also includes advanced features, including built-in logging, experiment
37 |     management, and serialization of experiments via a domain-specific language.
38 |     """,
39 | )
40 | 
41 | # long_description=open('README.md').read(),
42 | 


--------------------------------------------------------------------------------