├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── pipelines.nimble ├── pipelines ├── pipelines └── pipelines.nim └── tests ├── ages.pipeline ├── ages.py ├── ages_data.csv ├── ages_utils.py ├── fizzbuzz.pipeline ├── fizzbuzz.py ├── fizzbuzz_client.py └── fizzbuzz_utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | piipelines/pipelines 2 | pipelines/nimcache/ 3 | *.pyc 4 | *i~ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Caleb Winston 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | nimble install -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | 7 | 8 | Pipelines is a language and runtime for crafting massively parallel pipelines. Unlike other languages for defining data flow, the Pipeline language requires implementation of components to be defined separately in the Python scripting language. This allows the details of implementations to be separated from the structure of the pipeline, while providing access to thousands of active libraries for machine learning, data analysis and processing. Skip to [Getting Started](https://github.com/calebwin/pipelines#some-next-steps) to install the Pipeline compiler. 9 | 10 | ### An example 11 | 12 | As an introductory example, a simple pipeline for Fizz Buzz on even numbers could be written as follows - 13 | 14 | ```python 15 | from fizzbuzz import numbers 16 | from fizzbuzz import even 17 | from fizzbuzz import fizzbuzz 18 | from fizzbuzz import printer 19 | 20 | numbers 21 | /> even 22 | |> fizzbuzz where (number=*, fizz="Fizz", buzz="Buzz") 23 | |> printer 24 | ``` 25 | 26 | Meanwhile, the implementation of the components would be written in Python - 27 | 28 | ```python 29 | def numbers(): 30 | for number in range(1, 100): 31 | yield number 32 | 33 | def even(number): 34 | return number % 2 == 0 35 | 36 | def fizzbuzz(number, fizz, buzz): 37 | if number % 15 == 0: return fizz + buzz 38 | elif number % 3 == 0: return fizz 39 | elif number % 5 == 0: return buzz 40 | else: return number 41 | 42 | def printer(number): 43 | print(number) 44 | ``` 45 | 46 | Running the Pipeline document would safely execute each component of the pipeline in parallel and output the expected result. 47 | 48 | ### The imports 49 | 50 | Components are scripted in Python and linked into a pipeline using imports. The syntax for an import has 3 parts - (1) the path to the module, (2) the name of the function, and (3) the alias for the component. Here's an example - 51 | ```python 52 | from parser import parse_fasta as parse 53 | ``` 54 | That's really all there is to imports. Once a component is imported it can be referenced anywhere in the document with the alias. 55 | 56 | ### The stream 57 | 58 | Every pipeline is operated on a stream of data. The stream of data is created by a Python [generator](https://docs.python.org/3/tutorial/classes.html#generators). The following is an example of a generator that generates a stream of numbers from 0 to 1000. 59 | ```python 60 | def numbers(): 61 | for number in range(0, 1000): 62 | yield number 63 | ``` 64 | Here's a generator that reads entries from a file 65 | ```python 66 | def customers(): 67 | for line in open("customers.csv", 'r'): 68 | yield line 69 | ``` 70 | The first component in a pipeline is always the generator. The generator is run in parallel with all other components and each element of data is passed through the other components. 71 | ```python 72 | from utils import customers as customers # a generator function in the utils module 73 | from utils import parse_row as parser 74 | from utils import get_recommendations as recommender 75 | from utils import print_recommendations as printer 76 | 77 | customers |> parser |> recommender |> printer 78 | ``` 79 | 80 | ### The pipes 81 | 82 | Pipes are what connect components together to form a pipeline. As of now, there are 2 types of pipes in the Pipeline language - (1) transformer pipes, and (2) filter pipes. Transformer pipes are used when input is to be passed through a component. For example, a function can be defined to determine the potential of a particle and a function can be defined to print the potential. 83 | ```python 84 | particles |> get_potential |> printer 85 | ``` 86 | The above pipeline code would pass data from the stream generated by `particles` through `get_potential` and then the output of `get_potential` through `printer`. Filter pipes work similarly except they use the following component to filter data. For example, a function can be defined to determine if a person is over 50 and then print their names to a file. 87 | ```python 88 | population /> over_50 |> printer 89 | ``` 90 | This would use the function referenced by `over_50` to filter out data from the stream generated by `population` and then pass output to `printer`. 91 | 92 | ### The `where` keyword 93 | 94 | The `where` keyword lets you pass in multiple parameters to a component as opposed to just what the output from the previous component was. For example, a function can be defined to print to a file the names of all applicants under a certain age. 95 | ```python 96 | applicants 97 | |> printer where (person=*, age_limit=21) 98 | ``` 99 | This could be done using a filter as well. 100 | ```python 101 | applicants 102 | /> age_limit where (person=*, age=21) 103 | |> printer 104 | ``` 105 | In this case, the function for `age_limit` could look something like this - 106 | ```python 107 | def age_limit(person, age): 108 | return person.age <= age 109 | ``` 110 | Note that this function still has just one return value - the boolean expression that is used to determine wether input to the component is passed on as output. 111 | 112 | ### The `to` keyword 113 | The `to` keyword is for when you want the previous component has multiple return values and you want to specify which ones to pass on to the next component. As an example, if you had a function for calculating the electronegativity and electron affinity of an atom, you could use it in a pipeline as follows - 114 | ```python 115 | atoms 116 | |> calculator to (electronegativity, electron_affinity) 117 | |> printer where (line=electronegativity) 118 | ``` 119 | Here's an example using a filter. 120 | ```python 121 | atoms 122 | /> below where (atom=*, limit=2) to (is_below, electronegativity, electron_affinity) with is_below 123 | |> printer where (line=electronegativity) 124 | ``` 125 | Note the use of the `with` keyword here. This is necessary for filters to specify which return value of the function is used to filter out elements in the stream. 126 | 127 | ### Getting started 128 | All you need to get started is the Pipelines compiler. You can install it by downloading the executable from [Releases](https://github.com/calebwin/pipelines/releases). 129 | > If you have the [Nimble](https://github.com/nim-lang/nimble/) package manager installed and `~/.nimble/bin` permanantly added to your PATH environment variable (look this up > if you don't know how to do this), you can also install by running the following command. 130 | > ``` 131 | > nimble install pipelines 132 | > ``` 133 | Pipelines' only dependency is [the Python interpreter](https://www.python.org/downloads/release/python-2715/) being installed on your system. At the moment, most versions 2.7 and earlier are supported and support for Python 3 is in the works. Once Pipelines is installed and added to your PATH, you can create a `.pipeline` file, run or compile anywhere on your system - 134 | ```console 135 | $ pipelines 136 | the .pipeline compiler (v:0.1.0) 137 | 138 | usage: 139 | pipelines Show this 140 | pipelines Compile .pipeline file 141 | pipelines Compile all .pipeline files in folder 142 | pipelines run Run .pipeline file 143 | pipelines clean Remove all compiled .py files from folder 144 | 145 | for more info, go to github.com/calebwin/pipelines 146 | ``` 147 | 148 | ### Some next steps 149 | 150 | There are several things I'm hoping to implement in the future for this project. I'm hoping to implement some sort of `and` operator for piping data from the stream into multiple components in parallel with the output ending up in the stream in a nondeterministic order. Further down the line, I plan on porting the whole thing to C and putting in a complete error handling system 151 | 157 | -------------------------------------------------------------------------------- /pipelines.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.2" 4 | author = "calebwin" 5 | description = "A tiny framework & language for crafting massively parallel data pipelines" 6 | license = "MIT" 7 | bin = @["pipelines/pipelines"] 8 | 9 | # Dependencies 10 | 11 | requires "nim >= 0.17.2", "python >= 0.0.0" 12 | -------------------------------------------------------------------------------- /pipelines/pipelines: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calebwin/pipelines/e623d2d03e42467eb50dd1882e3864108fbff0ed/pipelines/pipelines -------------------------------------------------------------------------------- /pipelines/pipelines.nim: -------------------------------------------------------------------------------- 1 | import python # TODO support python3 2 | import os, strutils, sequtils, tables 3 | 4 | const debug = false 5 | 6 | type 7 | Path = tuple[module: string, function: string] # path to a function - defined by name of module and name of function in it 8 | Pipe = tuple[origin: string, modifiers: tuple[mWhere: string, mTo: string, mWith: string], destination: string, pipeType: Pipes] # pipe between 2 components - defined by origin component and destination component 9 | Pipes = enum 10 | pTransformer, pFilter 11 | 12 | # compiles pipeline document at given path to python code 13 | # TODO parallelize compilation 14 | proc compile*(path: string): string = 15 | # get contents of file 16 | let contents: string = readFile(path) 17 | 18 | # (1) parse contents of file 19 | 20 | var 21 | paths: Table[string, Path] = initTable[string, Path]() # table mapping alias to path 22 | pipes: seq[Pipe] = @[] # pipes connecting components 23 | 24 | # get tokens from contents 25 | var 26 | tokens: seq[string] = @[] 27 | token: string = "" 28 | index: int = 0 29 | 30 | # iterate through characters in contents and tokenize 31 | while index <= contents.len - 1: 32 | let character: string = $contents[index] # get character at current index 33 | 34 | # handle character being an end-of-token character 35 | if character.isNilOrWhitespace or character == "(" or character == "#": 36 | if token.strip().len > 0: 37 | tokens.add(token.strip()) # add token to tokens if current character is an end-of-token-character 38 | token = "" 39 | 40 | # handle character being start of parameter declaration 41 | if character == "(": 42 | let parameterDeclaration: string = contents[index .. contents.find(')', index)] # get whole parameter declaration 43 | tokens.add(parameterDeclaration) 44 | index = contents.find(')', index) + 1 # move to end of parameter declaration 45 | # handle character being start of comment 46 | elif character == "#": 47 | index = contents.find(NewLines, index) + 1 # move to end of comment 48 | else: 49 | token &= character # otherwise, append character to token 50 | # the character is only appended if it's not the end of a token or the start of a parameter declaration 51 | index += 1 # move to next character 52 | 53 | # parse tokens to paths and pipes 54 | var tokenIndex: int = 0 55 | for token in tokens: 56 | case token: 57 | of "import": 58 | # parse import statement 59 | let 60 | newPathModule: string = tokens[tokenIndex - 1] # module for new path 61 | newPathFunction: string = tokens[tokenIndex + 1] # function for new path 62 | newPathAlias: string = if tokens[tokenIndex + 1 + 1] == "as": tokens[tokenIndex + 1 + 2] else: newPathFunction # alias of new path 63 | newPath: Path = (module : newPathModule, function : newPathFunction) # new path 64 | 65 | # add new path to paths 66 | paths[newPathAlias] = newPath 67 | of "|>": 68 | # parse tranformer pipe statement 69 | let 70 | prevToken: string = tokens[tokenIndex - 1] # previous token from contents 71 | nextToken: string = tokens[tokenIndex + 1] # next token from contnets 72 | newPipeOrigin: string = prevToken # origin of new pipe 73 | newPipeDestination: string = nextToken # destination of new pipe 74 | 75 | # extract where, to, with modifiers 76 | var 77 | numModifiers: int = 0 78 | modifiers: tuple[mWhere: string, mTo: string, mWith: string] = (mWhere : "", mTo : "", mWith : "") 79 | 80 | if numModifiers == 0 and tokenIndex + 3 <= tokens.len - 1: 81 | numModifiers += 1 # increment number of modifiers 82 | case tokens[tokenIndex + 2]: 83 | of "where": modifiers.mWhere = tokens[tokenIndex + 3] 84 | of "to": modifiers.mTo = tokens[tokenIndex + 3] 85 | of "with": modifiers.mWith = tokens[tokenIndex + 3] 86 | 87 | if numModifiers == 1 and tokenIndex + 5 <= tokens.len - 1: 88 | numModifiers += 1 # increment number of modifiers 89 | case tokens[tokenIndex + 4]: 90 | of "where": modifiers.mWhere = tokens[tokenIndex + 5] 91 | of "to": modifiers.mTo = tokens[tokenIndex + 5] 92 | of "with": modifiers.mWith = tokens[tokenIndex + 5] 93 | 94 | if numModifiers == 2 and tokenIndex + 7 <= tokens.len - 1: 95 | numModifiers += 1 # increment number of modifiers 96 | case tokens[tokenIndex + 6]: 97 | of "where": modifiers.mWhere = tokens[tokenIndex + 7] 98 | of "to": modifiers.mTo = tokens[tokenIndex + 7] 99 | of "with": modifiers.mWith = tokens[tokenIndex + 7] 100 | 101 | # add new pipe 102 | pipes.add((origin : newPipeOrigin, modifiers : modifiers, destination : newPipeDestination, pipeType : pTransformer)) 103 | of "/>": 104 | # parse tranformer pipe statement 105 | let 106 | prevToken: string = tokens[tokenIndex - 1] # previous token from contents 107 | nextToken: string = tokens[tokenIndex + 1] # next token from contnets 108 | newPipeOrigin: string = prevToken # origin of new pipe 109 | newPipeDestination: string = nextToken # destination of new pipe 110 | 111 | # extract where, to, with modifiers 112 | var 113 | numModifiers: int = 0 114 | modifiers: tuple[mWhere: string, mTo: string, mWith: string] = (mWhere : "", mTo : "", mWith : "") 115 | 116 | if numModifiers == 0 and tokenIndex + 3 <= tokens.len - 1: 117 | numModifiers += 1 # increment number of modifiers 118 | case tokens[tokenIndex + 2]: 119 | of "where": modifiers.mWhere = tokens[tokenIndex + 3] 120 | of "to": modifiers.mTo = tokens[tokenIndex + 3] 121 | of "with": modifiers.mWith = tokens[tokenIndex + 3] 122 | else: numModifiers -= 1 # undo increment 123 | 124 | if numModifiers == 1 and tokenIndex + 5 <= tokens.len - 1: 125 | numModifiers += 1 # increment number of modifiers 126 | case tokens[tokenIndex + 4]: 127 | of "where": modifiers.mWhere = tokens[tokenIndex + 5] 128 | of "to": modifiers.mTo = tokens[tokenIndex + 5] 129 | of "with": modifiers.mWith = tokens[tokenIndex + 5] 130 | else: numModifiers -= 1 # undo increment 131 | 132 | if numModifiers == 2 and tokenIndex + 7 <= tokens.len - 1: 133 | numModifiers += 1 # increment number of modifiers 134 | case tokens[tokenIndex + 6]: 135 | of "where": modifiers.mWhere = tokens[tokenIndex + 7] 136 | of "to": modifiers.mTo = tokens[tokenIndex + 7] 137 | of "with": modifiers.mWith = tokens[tokenIndex + 7] 138 | else: numModifiers -= 1 # undo increment 139 | 140 | # add new pipe 141 | pipes.add((origin : newPipeOrigin, modifiers : modifiers, destination : newPipeDestination, pipeType : pFilter)) 142 | else: 143 | discard 144 | 145 | # update index 146 | tokenIndex += 1 147 | 148 | # (2) generate target code 149 | 150 | # initialize code with import statements 151 | var 152 | code: string = "from multiprocessing import Process, Queue\n" 153 | mainCode: string = "" 154 | 155 | # IMPORTS 156 | 157 | # import functions 158 | for alias, path in pairs(paths): 159 | code &= "from " & path.module & " import " & path.function & " as " & alias & "\n" 160 | 161 | # FUNCTIONS 162 | 163 | # define pipe sentinel 164 | code &= "class PLPipeSentinel: pass\n" 165 | 166 | # define function to run generator 167 | let generatorName: string = pipes[0].origin 168 | 169 | code &= "def pl_run_" & generatorName & "(pl_stream, pl_out_queue):\n" 170 | code &= "\tfor pl_data in pl_stream:\n" 171 | code &= "\t\tpl_out_queue.put(pl_data)\n" 172 | code &= "\tpl_out_queue.put(PLPipeSentinel())\n" 173 | 174 | # define functions for process for each component 175 | var componentIndex: int = 0 176 | for pipe in pipes: 177 | let 178 | component: string = pipe.destination # destination of pipe ~ the component to define a function for 179 | modifiers: tuple[mWhere: string, mTo: string, mWith: string] = pipe.modifiers # modifiers of pipe 180 | parameters: string = if modifiers.mWhere.len > 0: modifiers.mWhere.replace("*", "pl_inp") else: "(pl_inp)" # parameters of pipe 181 | inputs: string = if componentIndex > 0: pipes[componentIndex - 1].modifiers.mTo.replace("(", "").replace(")", "") else: "" # names of inputs into current compoent 182 | # TODO handle with modifier 183 | 184 | var componentCode: string = "" # code for running component 185 | 186 | # initialize to variables to None 187 | for to in modifiers.mTo.replace("(", "").replace(")", "").split(","): 188 | componentCode &= to.strip() & " = None\n" 189 | 190 | # loop indefinitely 191 | componentCode &= "while 1:\n" 192 | 193 | # block and get next element from in queue` 194 | componentCode &= "\tpl_inp = pl_in_queue.get()\n" 195 | 196 | # unpackaget tuple input and reate local variables to access inputs if necessary 197 | # this is only done when input is from a transformer pipe (filter pipes always have only one input) 198 | if inputs != "" and pipes[componentIndex - 1].pipeType == pTransformer: 199 | componentCode &= "\t" & inputs & " = pl_inp\n" 200 | 201 | # TODO use unique sentinel value 202 | # check if element from in queue is sentinel value 203 | componentCode &= "\tif isinstance(pl_inp, PLPipeSentinel):\n" 204 | # put sentinel value in queue to next component 205 | componentCode &= "\t\tpl_outp = PLPipeSentinel()\n" 206 | 207 | case pipe.pipeType: 208 | of pTransformer: 209 | # get output from passing element into component if element is not sentinel 210 | componentCode &= "\tif not isinstance(pl_inp, PLPipeSentinel):\n" 211 | componentCode &= "\t\tpl_outp = " & component & parameters & "\n" 212 | # unpackage output and update to variables 213 | if modifiers.mTo.len > 0: 214 | componentCode &= "\t\t" & modifiers.mTo.replace("(", "").replace(")", "") & " = pl_outp\n" 215 | of pFilter: 216 | # get output from passing element into component if element is not sentinel 217 | componentCode &= "\tif not isinstance(pl_inp, PLPipeSentinel):\n" 218 | componentCode &= "\t\tpl_result = " & component & parameters & "\n" 219 | if modifiers.mTo.len > 0: 220 | componentCode &= "\t\t" & modifiers.mTo.replace("(", "").replace(")", "") & " = pl_result\n" # unpackage output and update to variables 221 | if modifiers.mWith.len > 0: 222 | componentCode &= "\t\tif " & modifiers.mWith.replace("(", "").replace(")", "") & ":\n" # send input through filter 223 | else: 224 | componentCode &= "\t\tif pl_result:\n" # send input through filter 225 | componentCode &= "\t\t\tpl_outp = pl_inp\n" # pass on input as output 226 | componentCode &= "\t\telse:\n" 227 | componentCode &= "\t\t\tcontinue\n" # otherwise continue to next input 228 | 229 | # check if queue to next component exists 230 | componentCode &= "\tif pl_out_queue is not None:\n" 231 | 232 | # put output into queue to next component if queue to next component exists 233 | componentCode &= "\t\tpl_out_queue.put(pl_outp)\n" 234 | 235 | # break if element from in queue is sentinel value 236 | componentCode &= "\tif isinstance(pl_inp, PLPipeSentinel):\n" 237 | componentCode &= "\t\tbreak\n" 238 | 239 | # update index of component 240 | componentIndex += 1 241 | 242 | # append component code to code 243 | code &= "def pl_run_" & component & "(pl_in_queue, pl_out_queue):\n" # function header 244 | componentCode.removeSuffix("\n") # remove last newline 245 | code &= componentCode.indent(1, "\t") & "\n" # indent and add newline 246 | 247 | # MAIN 248 | 249 | # get iterator over stream of data 250 | let iteratorModule: string = pipes[0].origin 251 | mainCode &= "pl_data = " & iteratorModule & "()\n" 252 | 253 | # create queues into components 254 | for pipe in pipes: 255 | let component: string = pipe.destination # component to create a queue into 256 | 257 | # create queue into component 258 | mainCode &= "pl_in_" & component & " = Queue()\n" 259 | 260 | # create process for generator 261 | mainCode &= "pl_" & generatorName & "_process = Process(target=pl_run_" & generatorName & ", args=(pl_data, pl_in_" & pipes[0].destination & "))\n" 262 | 263 | # create processes for each component 264 | var pipeIndex: int = 0 265 | for pipe in pipes: 266 | let 267 | component: string = pipes[pipeIndex].destination # component to create process for 268 | componentQueue: string = "pl_in_" & component # queue to component to create process for 269 | nextComponentQueue: string = if pipeIndex < pipes.len - 1: "pl_in_" & pipes[pipeIndex + 1].destination else: "None" # queue to next component in pipeline 270 | 271 | # creat process for component passing in queue to the component and queue to next component 272 | mainCode &= "pl_" & component & "_process = Process(target=pl_run_" & component & ", args=(" & componentQueue & "," & nextComponentQueue & ",))\n" 273 | 274 | # update index in pipes 275 | pipeIndex += 1 276 | 277 | # start generator process 278 | mainCode &= "pl_" & generatorName & "_process.start()\n" 279 | 280 | # start processes 281 | for pipe in pipes: 282 | let component: string = pipe.destination # component to start process of 283 | 284 | # start process of component 285 | mainCode &= "pl_" & component & "_process.start()\n" 286 | 287 | # block main process till all process have finished 288 | for pipe in pipes: 289 | let component: string = pipe.destination # component to join process of 290 | 291 | # join process of component 292 | mainCode &= "pl_" & component & "_process.join()\n" 293 | 294 | # append main code to code 295 | code &= "if __name__ == \"__main__\":\n" 296 | mainCode.removeSuffix("\n") # remove last newline 297 | code &= mainCode.indent(1, "\t") & "\n" # indent main code and add newline 298 | 299 | # TODO append main code to code as "execute" method 300 | code &= "def execute():\n" 301 | mainCode.removeSuffix("\n") # remove last newline 302 | code &= mainCode.indent(1, "\t") & "\n" # indent main code and add newline 303 | 304 | # return code 305 | result = code 306 | 307 | if debug: 308 | echo(code) 309 | 310 | # runs pipeline document at given path 311 | proc runFile*(path: string) = 312 | # get compiled Python code 313 | let code: cstring = compile(path) 314 | 315 | # initialize the Python interpreter 316 | initialize() 317 | 318 | # add directory containing .pipeline file to PYTHONPATH 319 | when defined windows: 320 | syssetpath($getPath() & ";" & path[0 .. path.rfind("/")]) 321 | else: 322 | syssetpath($getPath() & ":" & path[0 .. path.rfind("/")]) 323 | 324 | # run code 325 | discard runSimpleString(code) 326 | 327 | # finalize the Python interpreter 328 | finalize() 329 | 330 | # compiles pipeline document at given path to python file at same path with ,py file extension 331 | proc compileFile*(path: string) = 332 | # compile pipeline document at given path 333 | let code: string = compile(path) 334 | 335 | # get path to new python file 336 | let targetPath: string = path.replace(".pipeline", ".py") 337 | 338 | # print code to file at target path 339 | writeFile(targetPath, code) 340 | 341 | proc main() = 342 | case paramCount(): 343 | of 0: 344 | # print welcome message 345 | echo("the .pipeline compiler (v:0.1.0)\n") 346 | 347 | # print usage 348 | echo("usage:") 349 | echo(" pipelines Show this") 350 | echo(" pipelines Compile .pipeline file") 351 | echo(" pipelines Compile all .pipeline files in folder") 352 | echo(" pipelines run Run .pipeline file") 353 | echo(" pipelines clean Remove all compiled .py files from folder\n") 354 | 355 | # print info 356 | echo("for more info, go to github.com/calebwin/pipelines") 357 | of 1: 358 | # get path to file to run 359 | let path: string = paramStr(1) 360 | 361 | # files compiled 362 | var filesCompiled: seq[string] = @[] 363 | 364 | # run file 365 | if fileExists(path): 366 | compileFile(path) 367 | filesCompiled.add(path) 368 | elif dirExists(path): 369 | for kind, file in walkDir(path): 370 | if file.endsWith(".pipeline"): 371 | compileFile(file) 372 | filesCompiled.add(file) 373 | else: 374 | echo(path & " not found") 375 | 376 | # print info 377 | echo($filesCompiled.len & " " & (if filesCompiled.len == 1: "file" else: "files") & " compiled" & (if dirExists(path): " at " & path else: "") & ":") 378 | for file in filesCompiled: 379 | echo(" " & file) 380 | of 2: 381 | case paramStr(1): 382 | of "r", "run": 383 | # get path to file to run 384 | let path: string = paramStr(2) 385 | 386 | # run file 387 | if existsFile(path): 388 | runFile(path) 389 | elif existsDir(path): 390 | for kind, file in walkDir(path): 391 | if file.endsWith(".pipeline"): 392 | runFile(file) 393 | else: 394 | echo(path & " not found") 395 | of "c", "clean": 396 | # get path to folder to clean 397 | let path: string = paramStr(2) 398 | 399 | # number of files removed 400 | var filesRemoved: seq[string] = @[] 401 | 402 | # clean folder 403 | for kind, file in walkDir(path): 404 | # compiled .py file 405 | let compiledFile: string = if file.endsWith(".pipeline"): file.replace(".pipeline", ".py") else: "" 406 | 407 | # remove compiled file if it exists 408 | if compiledFile != "": 409 | removeFile(compiledFile) 410 | filesRemoved.add(compiledFile) 411 | 412 | # print info 413 | echo($filesRemoved.len & " " & (if filesRemoved.len == 1: "file" else: "files") & " removed from " & path & ":") 414 | for file in filesRemoved: 415 | echo(" " & file) 416 | else: 417 | discard 418 | 419 | main() -------------------------------------------------------------------------------- /tests/ages.pipeline: -------------------------------------------------------------------------------- 1 | from ages_utils import ages 2 | from ages_utils import age_segment 3 | from ages_utils import is_under_age 4 | from ages_utils import print_age 5 | 6 | # TODO fix error where you need to have a line at end 7 | 8 | ages |> age_segment /> is_under_age |> print_age 9 | -------------------------------------------------------------------------------- /tests/ages.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Process, Queue 2 | from ages_utils import age_segment as age_segment 3 | from ages_utils import is_under_age as is_under_age 4 | from ages_utils import ages as ages 5 | from ages_utils import print_age as print_age 6 | class PLPipeSentinel: pass 7 | def run_ages(stream, out_queue): 8 | for data in stream: 9 | out_queue.put(data) 10 | out_queue.put(PLPipeSentinel()) 11 | def run_age_segment(in_queue, out_queue): 12 | while 1: 13 | inp = in_queue.get() 14 | if isinstance(inp, PLPipeSentinel): 15 | outp = PLPipeSentinel() 16 | if not isinstance(inp, PLPipeSentinel): 17 | outp = age_segment(inp) 18 | if out_queue is not None: 19 | out_queue.put(outp) 20 | if isinstance(inp, PLPipeSentinel): 21 | break 22 | def run_is_under_age(in_queue, out_queue): 23 | while 1: 24 | inp = in_queue.get() 25 | if isinstance(inp, PLPipeSentinel): 26 | outp = PLPipeSentinel() 27 | if not isinstance(inp, PLPipeSentinel): 28 | result = is_under_age(inp) 29 | if result: 30 | outp = inp 31 | else: 32 | continue 33 | if out_queue is not None: 34 | out_queue.put(outp) 35 | if isinstance(inp, PLPipeSentinel): 36 | break 37 | def run_print_age(in_queue, out_queue): 38 | while 1: 39 | inp = in_queue.get() 40 | if isinstance(inp, PLPipeSentinel): 41 | outp = PLPipeSentinel() 42 | if not isinstance(inp, PLPipeSentinel): 43 | outp = print_age(inp) 44 | if out_queue is not None: 45 | out_queue.put(outp) 46 | if isinstance(inp, PLPipeSentinel): 47 | break 48 | if __name__ == "__main__": 49 | data = ages() 50 | in_age_segment = Queue() 51 | in_is_under_age = Queue() 52 | in_print_age = Queue() 53 | ages_process = Process(target=run_ages, args=(data, in_age_segment)) 54 | age_segment_process = Process(target=run_age_segment, args=(in_age_segment,in_is_under_age,)) 55 | is_under_age_process = Process(target=run_is_under_age, args=(in_is_under_age,in_print_age,)) 56 | print_age_process = Process(target=run_print_age, args=(in_print_age,None,)) 57 | ages_process.start() 58 | age_segment_process.start() 59 | is_under_age_process.start() 60 | print_age_process.start() 61 | age_segment_process.join() 62 | is_under_age_process.join() 63 | print_age_process.join() 64 | -------------------------------------------------------------------------------- /tests/ages_data.csv: -------------------------------------------------------------------------------- 1 | 5 2 | 19 3 | 89 4 | 26 5 | 28 6 | 48 7 | 79 8 | 48 9 | 20 10 | 34 11 | 41 12 | 39 13 | 69 14 | 73 15 | 29 16 | 28 17 | 13 18 | 5 19 | 7 20 | 9 -------------------------------------------------------------------------------- /tests/ages_utils.py: -------------------------------------------------------------------------------- 1 | def ages(): 2 | # read lines of data 3 | lines = [] 4 | with open('ages_data.csv') as f: 5 | lines = f.read().splitlines() 6 | 7 | # get numbers from strings 8 | ages = [int(age) for age in lines] 9 | 10 | for age in ages: 11 | yield age 12 | 13 | def age_segment(age): 14 | if age < 12: return 0 15 | elif age < 18: return 1 16 | elif age < 28: return 2 17 | elif age < 48: return 3 18 | elif age < 68: return 4 19 | elif age < 88: return 5 20 | else: return 6 21 | 22 | def is_under_age(age_segment): 23 | if age_segment <= 1: 24 | return True 25 | return False 26 | 27 | def print_age(age_segment): 28 | if age_segment == 0: print("0-12") 29 | elif age_segment == 1: print("12-18") 30 | elif age_segment == 2: print("18-18") 31 | elif age_segment == 3: print("28-48") 32 | elif age_segment == 4: print("48-68") 33 | elif age_segment == 5: print("68-88") 34 | elif age_segment == 6: print("88+") -------------------------------------------------------------------------------- /tests/fizzbuzz.pipeline: -------------------------------------------------------------------------------- 1 | from fizzbuzz_utils import numbers # a sequence of numbers 2 | from fizzbuzz_utils import even # a filter for even numbers 3 | from fizzbuzz_utils import fizzbuzz # fizzbuzz 4 | from fizzbuzz_utils import printer # a printer 5 | 6 | numbers 7 | /> even where (number=*, counter=count) to (is_even, count) with is_even 8 | |> fizzbuzz where (number=*, fizz="fizz", buzz="buzz") to (number) 9 | |> printer where (number=number) 10 | -------------------------------------------------------------------------------- /tests/fizzbuzz.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Process, Queue 2 | from fizzbuzz_utils import fizzbuzz as fizzbuzz 3 | from fizzbuzz_utils import printer as printer 4 | from fizzbuzz_utils import even as even 5 | from fizzbuzz_utils import numbers as numbers 6 | class PLPipeSentinel: pass 7 | def pl_run_numbers(pl_stream, pl_out_queue): 8 | for pl_data in pl_stream: 9 | pl_out_queue.put(pl_data) 10 | pl_out_queue.put(PLPipeSentinel()) 11 | def pl_run_even(pl_in_queue, pl_out_queue): 12 | is_even = None 13 | count = None 14 | while 1: 15 | pl_inp = pl_in_queue.get() 16 | if isinstance(pl_inp, PLPipeSentinel): 17 | pl_outp = PLPipeSentinel() 18 | if not isinstance(pl_inp, PLPipeSentinel): 19 | pl_result = even(number=pl_inp, counter=count) 20 | is_even, count = pl_result 21 | if is_even: 22 | pl_outp = pl_inp 23 | else: 24 | continue 25 | if pl_out_queue is not None: 26 | pl_out_queue.put(pl_outp) 27 | if isinstance(pl_inp, PLPipeSentinel): 28 | break 29 | def pl_run_fizzbuzz(pl_in_queue, pl_out_queue): 30 | number = None 31 | while 1: 32 | pl_inp = pl_in_queue.get() 33 | if isinstance(pl_inp, PLPipeSentinel): 34 | pl_outp = PLPipeSentinel() 35 | if not isinstance(pl_inp, PLPipeSentinel): 36 | pl_outp = fizzbuzz(number=pl_inp, fizz="fizz", buzz="buzz") 37 | number = pl_outp 38 | if pl_out_queue is not None: 39 | pl_out_queue.put(pl_outp) 40 | if isinstance(pl_inp, PLPipeSentinel): 41 | break 42 | def pl_run_printer(pl_in_queue, pl_out_queue): 43 | while 1: 44 | pl_inp = pl_in_queue.get() 45 | number = pl_inp 46 | if isinstance(pl_inp, PLPipeSentinel): 47 | pl_outp = PLPipeSentinel() 48 | if not isinstance(pl_inp, PLPipeSentinel): 49 | pl_outp = printer(number=number) 50 | if pl_out_queue is not None: 51 | pl_out_queue.put(pl_outp) 52 | if isinstance(pl_inp, PLPipeSentinel): 53 | break 54 | if __name__ == "__main__": 55 | pl_data = numbers() 56 | pl_in_even = Queue() 57 | pl_in_fizzbuzz = Queue() 58 | pl_in_printer = Queue() 59 | pl_numbers_process = Process(target=pl_run_numbers, args=(pl_data, pl_in_even)) 60 | pl_even_process = Process(target=pl_run_even, args=(pl_in_even,pl_in_fizzbuzz,)) 61 | pl_fizzbuzz_process = Process(target=pl_run_fizzbuzz, args=(pl_in_fizzbuzz,pl_in_printer,)) 62 | pl_printer_process = Process(target=pl_run_printer, args=(pl_in_printer,None,)) 63 | pl_numbers_process.start() 64 | pl_even_process.start() 65 | pl_fizzbuzz_process.start() 66 | pl_printer_process.start() 67 | pl_even_process.join() 68 | pl_fizzbuzz_process.join() 69 | pl_printer_process.join() 70 | -------------------------------------------------------------------------------- /tests/fizzbuzz_client.py: -------------------------------------------------------------------------------- 1 | import fizzbuzz 2 | 3 | fizzbuzz.execute() -------------------------------------------------------------------------------- /tests/fizzbuzz_utils.py: -------------------------------------------------------------------------------- 1 | def numbers(): 2 | for number in range(1, 101): 3 | yield number 4 | 5 | def even(number, counter): 6 | return number % 2 == 0, 0 if counter is None else counter + 1 7 | 8 | def fizzbuzz(number, fizz, buzz): 9 | if number % 15 == 0: return fizz + buzz 10 | elif number % 3 == 0: return fizz 11 | elif number % 5 == 0: return buzz 12 | else: return number 13 | 14 | def printer(number): 15 | print(number) --------------------------------------------------------------------------------