├── .bowerrc ├── .gitignore ├── .node-version ├── .pylintrc ├── .python-version ├── .slugignore ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.coffee ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── Procfile ├── README.md ├── assets ├── .gitignore ├── lib │ └── css │ │ └── bootstrap.css └── src │ ├── coffee │ ├── controllers │ │ ├── .gitkeep │ │ ├── menu_controller.coffee │ │ └── regex_parser.coffee │ ├── directives │ │ ├── .gitkeep │ │ ├── a.coffee │ │ ├── auto_grow.coffee │ │ └── py_timer.coffee │ ├── filters │ │ ├── escape_html.coffee │ │ └── length.coffee │ ├── main.coffee │ ├── regex_builder.coffee │ └── resources │ │ ├── .gitkeep │ │ └── regex_resource.coffee │ ├── favicon.ico │ ├── html │ └── templates │ │ ├── index.html │ │ └── regex │ │ ├── error.html │ │ ├── result.html │ │ └── start.html │ ├── images │ └── pyregex.svg │ └── less │ └── main.less ├── bower.json ├── grunt └── templates │ └── default_md.jst ├── install_assets_deps.sh ├── nose.cfg ├── package.json ├── public ├── favicon.ico ├── index.html └── sitemap.xml ├── pyregex ├── __init__.py ├── api.py ├── service.py └── util.py ├── runtime.txt ├── tests ├── api_tests.py ├── client │ ├── controllers │ │ ├── regex_parser_controller_spec.coffee │ │ └── regex_parser_controller_spec.js │ ├── karma.conf.js │ ├── module_config_spec.coffee │ └── regex_builder_spec.coffee └── service_tests.py └── uwsgi.ini /.bowerrc: -------------------------------------------------------------------------------- 1 | {"directory": "assets/bower_components"} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | .DS_Store 4 | assets/.sass-cache 5 | .coverage 6 | coverage/ 7 | node_modules/ 8 | *.sublime-project 9 | *.sublime-workspace 10 | public/assets 11 | vendor 12 | .bundle 13 | aws_credentials.json 14 | .*.swp 15 | tmp 16 | tags 17 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 0.10.13 2 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 21 | # number of processors available to use. 22 | jobs=1 23 | 24 | # Control the amount of potential inferred values when inferring a single 25 | # object. This can help the performance when dealing with large functions or 26 | # complex, nested conditions. 27 | limit-inference-results=100 28 | 29 | # List of plugins (as comma separated values of python module names) to load, 30 | # usually to register additional checkers. 31 | load-plugins=pylint_flask 32 | 33 | # Pickle collected data for later comparisons. 34 | persistent=yes 35 | 36 | # Specify a configuration file. 37 | #rcfile= 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | file-ignored, 77 | suppressed-message, 78 | useless-suppression, 79 | deprecated-pragma, 80 | use-symbolic-message-instead, 81 | apply-builtin, 82 | basestring-builtin, 83 | buffer-builtin, 84 | cmp-builtin, 85 | coerce-builtin, 86 | execfile-builtin, 87 | file-builtin, 88 | long-builtin, 89 | raw_input-builtin, 90 | reduce-builtin, 91 | standarderror-builtin, 92 | unicode-builtin, 93 | xrange-builtin, 94 | coerce-method, 95 | delslice-method, 96 | getslice-method, 97 | setslice-method, 98 | no-absolute-import, 99 | old-division, 100 | dict-iter-method, 101 | dict-view-method, 102 | next-method-called, 103 | metaclass-assignment, 104 | indexing-exception, 105 | raising-string, 106 | reload-builtin, 107 | oct-method, 108 | hex-method, 109 | nonzero-method, 110 | cmp-method, 111 | input-builtin, 112 | round-builtin, 113 | intern-builtin, 114 | unichr-builtin, 115 | map-builtin-not-iterating, 116 | zip-builtin-not-iterating, 117 | range-builtin-not-iterating, 118 | filter-builtin-not-iterating, 119 | using-cmp-argument, 120 | eq-without-hash, 121 | div-method, 122 | idiv-method, 123 | rdiv-method, 124 | exception-message-attribute, 125 | invalid-str-codec, 126 | sys-max-int, 127 | bad-python3-import, 128 | deprecated-string-function, 129 | deprecated-str-translate-call, 130 | deprecated-itertools-function, 131 | deprecated-types-field, 132 | next-method-defined, 133 | dict-items-not-iterating, 134 | dict-keys-not-iterating, 135 | dict-values-not-iterating, 136 | deprecated-operator-function, 137 | deprecated-urllib-function, 138 | xreadlines-attribute, 139 | deprecated-sys-function, 140 | exception-escape, 141 | comprehension-escape 142 | 143 | # Enable the message, report, category or checker with the given id(s). You can 144 | # either give multiple identifier separated by comma (,) or put this option 145 | # multiple time (only on the command line, not in the configuration file where 146 | # it should appear only once). See also the "--disable" option for examples. 147 | enable=c-extension-no-member 148 | 149 | 150 | [REPORTS] 151 | 152 | # Python expression which should return a score less than or equal to 10. You 153 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 154 | # which contain the number of messages in each category, as well as 'statement' 155 | # which is the total number of statements analyzed. This score is used by the 156 | # global evaluation report (RP0004). 157 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 158 | 159 | # Template used to display messages. This is a python new-style format string 160 | # used to format the message information. See doc for all details. 161 | #msg-template= 162 | 163 | # Set the output format. Available formats are text, parseable, colorized, json 164 | # and msvs (visual studio). You can also give a reporter class, e.g. 165 | # mypackage.mymodule.MyReporterClass. 166 | output-format=text 167 | 168 | # Tells whether to display a full report or only the messages. 169 | reports=no 170 | 171 | # Activate the evaluation score. 172 | score=yes 173 | 174 | 175 | [REFACTORING] 176 | 177 | # Maximum number of nested blocks for function / method body 178 | max-nested-blocks=5 179 | 180 | # Complete name of functions that never returns. When checking for 181 | # inconsistent-return-statements if a never returning function is called then 182 | # it will be considered as an explicit return statement and no message will be 183 | # printed. 184 | never-returning-functions=sys.exit 185 | 186 | 187 | [TYPECHECK] 188 | 189 | # List of decorators that produce context managers, such as 190 | # contextlib.contextmanager. Add to this list to register other decorators that 191 | # produce valid context managers. 192 | contextmanager-decorators=contextlib.contextmanager 193 | 194 | # List of members which are set dynamically and missed by pylint inference 195 | # system, and so shouldn't trigger E1101 when accessed. Python regular 196 | # expressions are accepted. 197 | generated-members= 198 | 199 | # Tells whether missing members accessed in mixin class should be ignored. A 200 | # mixin class is detected if its name ends with "mixin" (case insensitive). 201 | ignore-mixin-members=yes 202 | 203 | # Tells whether to warn about missing members when the owner of the attribute 204 | # is inferred to be None. 205 | ignore-none=yes 206 | 207 | # This flag controls whether pylint should warn about no-member and similar 208 | # checks whenever an opaque object is returned when inferring. The inference 209 | # can return multiple potential results while evaluating a Python object, but 210 | # some branches might not be evaluated, which results in partial inference. In 211 | # that case, it might be useful to still emit no-member and other checks for 212 | # the rest of the inferred objects. 213 | ignore-on-opaque-inference=yes 214 | 215 | # List of class names for which member attributes should not be checked (useful 216 | # for classes with dynamically set attributes). This supports the use of 217 | # qualified names. 218 | ignored-classes=optparse.Values,thread._local,_thread._local 219 | 220 | # List of module names for which member attributes should not be checked 221 | # (useful for modules/projects where namespaces are manipulated during runtime 222 | # and thus existing member attributes cannot be deduced by static analysis). It 223 | # supports qualified module names, as well as Unix pattern matching. 224 | ignored-modules= 225 | 226 | # Show a hint with possible names when a member name was not found. The aspect 227 | # of finding the hint is based on edit distance. 228 | missing-member-hint=yes 229 | 230 | # The minimum edit distance a name should have in order to be considered a 231 | # similar match for a missing member name. 232 | missing-member-hint-distance=1 233 | 234 | # The total number of similar names that should be taken in consideration when 235 | # showing a hint for a missing member. 236 | missing-member-max-choices=1 237 | 238 | # List of decorators that change the signature of a decorated function. 239 | signature-mutators= 240 | 241 | 242 | [VARIABLES] 243 | 244 | # List of additional names supposed to be defined in builtins. Remember that 245 | # you should avoid defining new builtins when possible. 246 | additional-builtins= 247 | 248 | # Tells whether unused global variables should be treated as a violation. 249 | allow-global-unused-variables=yes 250 | 251 | # List of strings which can identify a callback function by name. A callback 252 | # name must start or end with one of those strings. 253 | callbacks=cb_, 254 | _cb 255 | 256 | # A regular expression matching the name of dummy variables (i.e. expected to 257 | # not be used). 258 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 259 | 260 | # Argument names that match this expression will be ignored. Default to name 261 | # with leading underscore. 262 | ignored-argument-names=_.*|^ignored_|^unused_ 263 | 264 | # Tells whether we should check for unused import in __init__ files. 265 | init-import=no 266 | 267 | # List of qualified module names which can have objects that can redefine 268 | # builtins. 269 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 270 | 271 | 272 | [LOGGING] 273 | 274 | # Format style used to check logging format string. `old` means using % 275 | # formatting, `new` is for `{}` formatting,and `fstr` is for f-strings. 276 | logging-format-style=old 277 | 278 | # Logging modules to check that the string format arguments are in logging 279 | # function parameter format. 280 | logging-modules=logging 281 | 282 | 283 | [SPELLING] 284 | 285 | # Limits count of emitted suggestions for spelling mistakes. 286 | max-spelling-suggestions=4 287 | 288 | # Spelling dictionary name. Available dictionaries: none. To make it work, 289 | # install the python-enchant package. 290 | spelling-dict= 291 | 292 | # List of comma separated words that should not be checked. 293 | spelling-ignore-words= 294 | 295 | # A path to a file that contains the private dictionary; one word per line. 296 | spelling-private-dict-file= 297 | 298 | # Tells whether to store unknown words to the private dictionary (see the 299 | # --spelling-private-dict-file option) instead of raising a message. 300 | spelling-store-unknown-words=no 301 | 302 | 303 | [BASIC] 304 | 305 | # Naming style matching correct argument names. 306 | argument-naming-style=snake_case 307 | 308 | # Regular expression matching correct argument names. Overrides argument- 309 | # naming-style. 310 | #argument-rgx= 311 | 312 | # Naming style matching correct attribute names. 313 | attr-naming-style=snake_case 314 | 315 | # Regular expression matching correct attribute names. Overrides attr-naming- 316 | # style. 317 | #attr-rgx= 318 | 319 | # Bad variable names which should always be refused, separated by a comma. 320 | bad-names=foo, 321 | bar, 322 | baz, 323 | toto, 324 | tutu, 325 | tata 326 | 327 | # Naming style matching correct class attribute names. 328 | class-attribute-naming-style=any 329 | 330 | # Regular expression matching correct class attribute names. Overrides class- 331 | # attribute-naming-style. 332 | #class-attribute-rgx= 333 | 334 | # Naming style matching correct class names. 335 | class-naming-style=PascalCase 336 | 337 | # Regular expression matching correct class names. Overrides class-naming- 338 | # style. 339 | #class-rgx= 340 | 341 | # Naming style matching correct constant names. 342 | const-naming-style=UPPER_CASE 343 | 344 | # Regular expression matching correct constant names. Overrides const-naming- 345 | # style. 346 | #const-rgx= 347 | 348 | # Minimum line length for functions/classes that require docstrings, shorter 349 | # ones are exempt. 350 | docstring-min-length=-1 351 | 352 | # Naming style matching correct function names. 353 | function-naming-style=snake_case 354 | 355 | # Regular expression matching correct function names. Overrides function- 356 | # naming-style. 357 | #function-rgx= 358 | 359 | # Good variable names which should always be accepted, separated by a comma. 360 | good-names=i, 361 | j, 362 | k, 363 | ex, 364 | Run, 365 | _ 366 | 367 | # Include a hint for the correct naming format with invalid-name. 368 | include-naming-hint=no 369 | 370 | # Naming style matching correct inline iteration names. 371 | inlinevar-naming-style=any 372 | 373 | # Regular expression matching correct inline iteration names. Overrides 374 | # inlinevar-naming-style. 375 | #inlinevar-rgx= 376 | 377 | # Naming style matching correct method names. 378 | method-naming-style=snake_case 379 | 380 | # Regular expression matching correct method names. Overrides method-naming- 381 | # style. 382 | #method-rgx= 383 | 384 | # Naming style matching correct module names. 385 | module-naming-style=snake_case 386 | 387 | # Regular expression matching correct module names. Overrides module-naming- 388 | # style. 389 | #module-rgx= 390 | 391 | # Colon-delimited sets of names that determine each other's naming style when 392 | # the name regexes allow several styles. 393 | name-group= 394 | 395 | # Regular expression which should only match function or class names that do 396 | # not require a docstring. 397 | no-docstring-rgx=^_ 398 | 399 | # List of decorators that produce properties, such as abc.abstractproperty. Add 400 | # to this list to register other decorators that produce valid properties. 401 | # These decorators are taken in consideration only for invalid-name. 402 | property-classes=abc.abstractproperty 403 | 404 | # Naming style matching correct variable names. 405 | variable-naming-style=snake_case 406 | 407 | # Regular expression matching correct variable names. Overrides variable- 408 | # naming-style. 409 | #variable-rgx= 410 | 411 | 412 | [STRING] 413 | 414 | # This flag controls whether the implicit-str-concat-in-sequence should 415 | # generate a warning on implicit string concatenation in sequences defined over 416 | # several lines. 417 | check-str-concat-over-line-jumps=no 418 | 419 | 420 | [FORMAT] 421 | 422 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 423 | expected-line-ending-format= 424 | 425 | # Regexp for a line that is allowed to be longer than the limit. 426 | ignore-long-lines=^\s*(# )??$ 427 | 428 | # Number of spaces of indent required inside a hanging or continued line. 429 | indent-after-paren=4 430 | 431 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 432 | # tab). 433 | indent-string=' ' 434 | 435 | # Maximum number of characters on a single line. 436 | max-line-length=100 437 | 438 | # Maximum number of lines in a module. 439 | max-module-lines=1000 440 | 441 | # List of optional constructs for which whitespace checking is disabled. `dict- 442 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 443 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 444 | # `empty-line` allows space-only lines. 445 | no-space-check=trailing-comma, 446 | dict-separator 447 | 448 | # Allow the body of a class to be on the same line as the declaration if body 449 | # contains single statement. 450 | single-line-class-stmt=no 451 | 452 | # Allow the body of an if to be on the same line as the test if there is no 453 | # else. 454 | single-line-if-stmt=no 455 | 456 | 457 | [MISCELLANEOUS] 458 | 459 | # List of note tags to take in consideration, separated by a comma. 460 | notes=FIXME, 461 | XXX, 462 | TODO 463 | 464 | 465 | [SIMILARITIES] 466 | 467 | # Ignore comments when computing similarities. 468 | ignore-comments=yes 469 | 470 | # Ignore docstrings when computing similarities. 471 | ignore-docstrings=yes 472 | 473 | # Ignore imports when computing similarities. 474 | ignore-imports=no 475 | 476 | # Minimum lines number of a similarity. 477 | min-similarity-lines=4 478 | 479 | 480 | [DESIGN] 481 | 482 | # Maximum number of arguments for function / method. 483 | max-args=5 484 | 485 | # Maximum number of attributes for a class (see R0902). 486 | max-attributes=7 487 | 488 | # Maximum number of boolean expressions in an if statement (see R0916). 489 | max-bool-expr=5 490 | 491 | # Maximum number of branch for function / method body. 492 | max-branches=12 493 | 494 | # Maximum number of locals for function / method body. 495 | max-locals=15 496 | 497 | # Maximum number of parents for a class (see R0901). 498 | max-parents=7 499 | 500 | # Maximum number of public methods for a class (see R0904). 501 | max-public-methods=20 502 | 503 | # Maximum number of return / yield for function / method body. 504 | max-returns=6 505 | 506 | # Maximum number of statements in function / method body. 507 | max-statements=50 508 | 509 | # Minimum number of public methods for a class (see R0903). 510 | min-public-methods=2 511 | 512 | 513 | [CLASSES] 514 | 515 | # List of method names used to declare (i.e. assign) instance attributes. 516 | defining-attr-methods=__init__, 517 | __new__, 518 | setUp, 519 | __post_init__ 520 | 521 | # List of member names, which should be excluded from the protected access 522 | # warning. 523 | exclude-protected=_asdict, 524 | _fields, 525 | _replace, 526 | _source, 527 | _make 528 | 529 | # List of valid names for the first argument in a class method. 530 | valid-classmethod-first-arg=cls 531 | 532 | # List of valid names for the first argument in a metaclass class method. 533 | valid-metaclass-classmethod-first-arg=cls 534 | 535 | 536 | [IMPORTS] 537 | 538 | # List of modules that can be imported at any level, not just the top level 539 | # one. 540 | allow-any-import-level= 541 | 542 | # Allow wildcard imports from modules that define __all__. 543 | allow-wildcard-with-all=no 544 | 545 | # Analyse import fallback blocks. This can be used to support both Python 2 and 546 | # 3 compatible code, which means that the block might have code that exists 547 | # only in one or another interpreter, leading to false positives when analysed. 548 | analyse-fallback-blocks=no 549 | 550 | # Deprecated modules which should not be used, separated by a comma. 551 | deprecated-modules=optparse,tkinter.tix 552 | 553 | # Create a graph of external dependencies in the given file (report RP0402 must 554 | # not be disabled). 555 | ext-import-graph= 556 | 557 | # Create a graph of every (i.e. internal and external) dependencies in the 558 | # given file (report RP0402 must not be disabled). 559 | import-graph= 560 | 561 | # Create a graph of internal dependencies in the given file (report RP0402 must 562 | # not be disabled). 563 | int-import-graph= 564 | 565 | # Force import order to recognize a module as part of the standard 566 | # compatibility libraries. 567 | known-standard-library= 568 | 569 | # Force import order to recognize a module as part of a third party library. 570 | known-third-party=enchant 571 | 572 | # Couples of modules and preferred modules, separated by a comma. 573 | preferred-modules= 574 | 575 | 576 | [EXCEPTIONS] 577 | 578 | # Exceptions that will emit a warning when being caught. Defaults to 579 | # "BaseException, Exception". 580 | overgeneral-exceptions=BaseException, 581 | Exception 582 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.7.7 2 | -------------------------------------------------------------------------------- /.slugignore: -------------------------------------------------------------------------------- 1 | assets 2 | node_modules 3 | public 4 | tests 5 | .bowerrc 6 | nose.cfg 7 | .travis.yml 8 | bower.json 9 | deploy.sh 10 | Gruntfile.js 11 | *.sh -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.7" 4 | 5 | install: 6 | - pip install -q pipenv 7 | - pipenv install -d 8 | 9 | script: 10 | - pipenv run tests 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # PyRegex Changelog 2 | 3 | ## 2014-06-17 4 | 5 | * [bugfix] Fixed broken menu links 6 | * [feature] Incorporated logo into the homepage 7 | 8 | ## 2014-06-14 9 | 10 | * [feature] PyRegex is now running on Python 3.4. Read more at [my blog article](http://rscarvalho.github.io/blog/2014/06/14/pyregex-has-been-updated/) 11 | * [feature] Ported the web framework to Flask. Read more at [my blog article](http://rscarvalho.github.io/blog/2014/06/14/pyregex-has-been-updated/) 12 | 13 | ## 2014-05-22 14 | 15 | * [feature] [Pull Request #12](https://github.com/rscarvalho/pyregex/pull/12) Adds pyRegex favicon and icon source 16 | 17 | ## 2013-08-14 18 | 19 | * [bugfix] [#9](https://github.com/rscarvalho/pyregex/issues/9) Add timeout on regex match to avoid DoS attacks 20 | * Start of this changelog 21 | 22 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | assetPath = (path) -> "assets/" + path 3 | publicPath = (path) -> "public/" + path 4 | 5 | bowerComponent = (name, src) -> 6 | expand: true 7 | cwd: assetPath("bower_components/#{name}") 8 | src: src 9 | dest: assetPath("build/") 10 | 11 | Array::flatten = -> [].concat.apply [], this 12 | Array::clone = -> @slice(0) 13 | Array::insertAfter = (reference, elements...) -> 14 | return @ unless elements.length 15 | idx = @indexOf(reference) 16 | if idx < 0 17 | [@unshift(e) for e in elements] 18 | else 19 | @splice(idx, 0, elements...) 20 | @ 21 | 22 | coffeePaths = ["tests/client", "src/coffee", "assets/src/coffee"].map((e) -> 23 | [e + "/*.coffee", e + "/**/*.coffee"] 24 | ).flatten() 25 | 26 | aws_credentials = undefined 27 | try 28 | aws_credentials = grunt.file.readJSON("aws_credentials.json") 29 | catch e 30 | aws_credentials = {key: "", secret: "", bucket: ""} 31 | 32 | pkg = grunt.file.readJSON("package.json") 33 | 34 | # Configuration 35 | grunt.initConfig 36 | pkg: pkg 37 | aws: aws_credentials 38 | 39 | # Coffee to JS compilation 40 | coffee: 41 | app: 42 | src: [ 43 | assetPath("src/coffee/*.coffee"), 44 | assetPath("src/coffee/**/*.coffee") 45 | ].flatten() 46 | dest: assetPath("build/") 47 | expand: true 48 | flatten: true 49 | ext: ".js" 50 | 51 | markdown: 52 | all: 53 | files: [ 54 | src: 'CHANGELOG.md' 55 | dest: 'public/assets/templates/changelog.html' 56 | ], 57 | options: 58 | template: 'grunt/templates/default_md.jst' 59 | 60 | # Move other files to dist folder 61 | copy: 62 | css: 63 | files: [ 64 | bowerComponent("normalize-css", "normalize.css"), 65 | bowerComponent("select2", ["select2.js", "select2.css"]) 66 | , 67 | expand: true 68 | cwd: assetPath("lib/css") 69 | src: ["**.css"] 70 | dest: assetPath("build/") 71 | , 72 | expand: true 73 | cwd: assetPath("src/css/") 74 | src: ["**.css"] 75 | dest: assetPath("build/") 76 | ] 77 | 78 | js: 79 | files: [ 80 | bowerComponent("jquery/dist", "jquery.js"), 81 | bowerComponent("angular", "angular.js"), 82 | bowerComponent("angular-ui-select2", "src/select2.js"), 83 | bowerComponent("select2", "select2.js"), 84 | bowerComponent("bootstrap-css/js", "bootstrap.js"), 85 | bowerComponent("underscore", "underscore.js"), 86 | bowerComponent("base64", "base64.js"), 87 | bowerComponent("modernizr", "modernizr.js") 88 | , 89 | expand: true 90 | cwd: assetPath("lib/js") 91 | src: ["**.js"] 92 | dest: assetPath("build/") 93 | , 94 | expand: true 95 | cwd: assetPath("src/js/") 96 | src: ["**"] 97 | dest: assetPath("build/") 98 | ] 99 | 100 | html: 101 | files: [ 102 | expand: true 103 | cwd: assetPath("src/") 104 | src: ["*.html"] 105 | dest: publicPath("assets/") 106 | , 107 | expand: true 108 | cwd: assetPath("src/html/") 109 | src: ["*.html", "**/*.html"] 110 | dest: publicPath("assets/") 111 | ] 112 | 113 | images: 114 | files: [ 115 | expand: true 116 | cwd: assetPath("bower_components/bootstrap-css/img") 117 | src: "*.png" 118 | dest: publicPath("assets/") 119 | , 120 | expand: true 121 | cwd: assetPath("bower_components/select2") 122 | src: ["*.png", "*.gif"] 123 | dest: publicPath("assets/") 124 | , 125 | expand: true 126 | cwd: assetPath("lib/images") 127 | src: ["**.png", "**.jpg", "**.gif", "**.webp"] 128 | dest: publicPath("assets/") 129 | ] 130 | 131 | icons: 132 | files: [ 133 | expand: true 134 | cwd: assetPath("src/") 135 | src: "*.ico" 136 | dest: publicPath("/") 137 | ] 138 | 139 | concat: 140 | options: 141 | separator: "\n" 142 | 143 | screen_css: 144 | src: [ 145 | assetPath("build/normalize.css"), 146 | assetPath("build/bootstrap-responsive.css"), 147 | assetPath("build/select2.css"), assetPath("build/**.css") 148 | ] 149 | dest: publicPath("assets/screen.css") 150 | 151 | application_js: 152 | src: [ 153 | assetPath("build/jquery.js"), 154 | assetPath("build/angular.js"), 155 | assetPath("build/select2.js"), 156 | assetPath("build/src/select2.js"), 157 | assetPath("build/base64.js"), assetPath("build/main.js"), 158 | assetPath("build/modernizr.js"), 159 | assetPath("build/**.js"), 160 | assetPath("build/**/*.js") 161 | ] 162 | dest: publicPath("assets/application.js") 163 | 164 | watch: 165 | coffee: 166 | files: coffeePaths 167 | tasks: [ 168 | "coffeelint", 169 | "coffee", 170 | "copy:js", 171 | "concat:application_js", 172 | "karma" 173 | ] 174 | 175 | less: 176 | files: [assetPath("src/less/*.less"), assetPath("src/less/**/*.less")] 177 | tasks: ["less", "copy:css", "concat:screen_css"] 178 | 179 | html: 180 | files: [assetPath("src/*.html"), assetPath("src/**/*.html")] 181 | tasks: ["copy:html", "copy:images"] 182 | 183 | markdown: 184 | files: [assetPath("*.md"), assetPath("**/*.md"), 'grunt/templates/**'] 185 | tasks: ['markdown'] 186 | 187 | coffeelint: 188 | app: [coffeePaths, 'Gruntfile.coffee'].flatten() 189 | 190 | clean: 191 | build: [assetPath("build/**")] 192 | dist: [publicPath("assets/**")] 193 | 194 | less: 195 | dist: 196 | options: 197 | dumpLineNumbers: true 198 | yuicompress: true 199 | paths: ["assets/src/less"] 200 | 201 | files: [ 202 | expand: true 203 | cwd: assetPath("src/less") 204 | src: ["**.less"] 205 | dest: assetPath("build") 206 | ext: ".css" 207 | ] 208 | 209 | karma: 210 | unit: 211 | configFile: "tests/client/karma.conf.js" 212 | 213 | connect: 214 | server: 215 | options: 216 | base: "public/" 217 | port: 8082 218 | keepalive: true 219 | 220 | gen_api: 221 | production: 222 | endpoint: "http://api.pyregex.com/api" 223 | 224 | development: 225 | endpoint: "http://localhost:5000/api" 226 | 227 | "s3-sync": 228 | options: 229 | key: "<%= aws.key %>" 230 | secret: "<%= aws.secret %>" 231 | bucket: "<%= aws.bucket %>" 232 | access: "public-read" 233 | concurrency: 20 234 | gzip: false 235 | 236 | dist: 237 | files: [ 238 | root: "public/" 239 | src: [ 240 | "public/*.html", 241 | "public/**/*.html", 242 | "public/**/*.css", 243 | "public/**/*.js", 244 | "public/**.xml", 245 | "public/favicon.ico" 246 | ] 247 | dest: "/" 248 | gzip: true 249 | , 250 | root: "public/" 251 | src: ['gif', 'png', 'jpg', 'webp'].map((e) -> "public/**/**.#{e}") 252 | dest: "/" 253 | gzip: false 254 | ] 255 | 256 | 257 | # Load plugins 258 | m = (o) -> key for key, value of o 259 | tasks = [m(pkg.dependencies), m(pkg.devDependencies)].flatten() 260 | tasks.filter((e) -> /grunt\-/.test e).forEach(grunt.loadNpmTasks) 261 | 262 | grunt.event.on "watch", (action, filepath, target) -> 263 | grunt.log.writeln "#{target}: #{filepath} has #{action}" 264 | 265 | 266 | grunt.registerMultiTask "gen_api", "Generate server name for api", -> 267 | grunt.log.writeln "#{@target}: #{@data.endpoint}" 268 | contents = "(function(){\n\tthis.PyRegex().value(" + 269 | "\"apiUrl\", \"#{@data.endpoint}\");\n}).call(this);\n" 270 | fs = require("fs") 271 | fs.writeFileSync "assets/build/api.js", contents 272 | 273 | # Custom tasks 274 | 275 | common = ["coffeelint", "coffee", "less", "copy", "concat", "markdown"] 276 | c = (k, args...) -> common.clone().insertAfter(k, args...) 277 | grunt.registerTask "build", c("copy", "gen_api:development") 278 | grunt.registerTask "build:production", c("copy", "gen_api:production") 279 | grunt.registerTask "deploy", ["clean", "build:production", "s3-sync"] 280 | grunt.registerTask "test", ["karma"] 281 | grunt.registerTask "default", ["clean", "build", "watch"] 282 | grunt.registerTask "server", ["build", "connect"] 283 | 284 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | nose = "*" 8 | python-termstyle = "*" 9 | rednose = "*" 10 | honcho = "*" 11 | autopep8 = "*" 12 | isort = "*" 13 | pylint = "*" 14 | pylint-flask = "*" 15 | 16 | [packages] 17 | flask = "*" 18 | uwsgi = "*" 19 | distribute = "*" 20 | webob = "*" 21 | rollbar = "*" 22 | flask-cors = "*" 23 | 24 | [requires] 25 | python_version = "3.7" 26 | 27 | [scripts] 28 | tests = "nosetests -c nose.cfg" 29 | start = "honcho start web" 30 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "bb1694033affbc18d874ab1a2665c51bfa58b647aa2cf479f2193361af7a7afd" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", 22 | "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" 23 | ], 24 | "version": "==2020.4.5.1" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 29 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 30 | ], 31 | "version": "==3.0.4" 32 | }, 33 | "click": { 34 | "hashes": [ 35 | "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", 36 | "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" 37 | ], 38 | "version": "==7.1.1" 39 | }, 40 | "flask": { 41 | "hashes": [ 42 | "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", 43 | "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" 44 | ], 45 | "index": "pypi", 46 | "version": "==1.1.2" 47 | }, 48 | "flask-cors": { 49 | "hashes": [ 50 | "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16", 51 | "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a" 52 | ], 53 | "index": "pypi", 54 | "version": "==3.0.8" 55 | }, 56 | "idna": { 57 | "hashes": [ 58 | "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", 59 | "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" 60 | ], 61 | "version": "==2.9" 62 | }, 63 | "itsdangerous": { 64 | "hashes": [ 65 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 66 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 67 | ], 68 | "version": "==1.1.0" 69 | }, 70 | "jinja2": { 71 | "hashes": [ 72 | "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", 73 | "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" 74 | ], 75 | "version": "==2.11.2" 76 | }, 77 | "markupsafe": { 78 | "hashes": [ 79 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 80 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 81 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 82 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 83 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 84 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 85 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 86 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 87 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 88 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 89 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 90 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 91 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 92 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 93 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 94 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 95 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 96 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 97 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 98 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 99 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 100 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 101 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 102 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 103 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 104 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 105 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 106 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 107 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 108 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 109 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 110 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 111 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" 112 | ], 113 | "version": "==1.1.1" 114 | }, 115 | "requests": { 116 | "hashes": [ 117 | "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", 118 | "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" 119 | ], 120 | "version": "==2.23.0" 121 | }, 122 | "rollbar": { 123 | "hashes": [ 124 | "sha256:75add43e634f3f75e069e1edd2b793b8d1e8800f16419a1d83065b49dd5d1372" 125 | ], 126 | "index": "pypi", 127 | "version": "==0.15.0" 128 | }, 129 | "six": { 130 | "hashes": [ 131 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 132 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 133 | ], 134 | "version": "==1.14.0" 135 | }, 136 | "urllib3": { 137 | "hashes": [ 138 | "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", 139 | "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" 140 | ], 141 | "version": "==1.25.9" 142 | }, 143 | "uwsgi": { 144 | "hashes": [ 145 | "sha256:4972ac538800fb2d421027f49b4a1869b66048839507ccf0aa2fda792d99f583" 146 | ], 147 | "index": "pypi", 148 | "version": "==2.0.18" 149 | }, 150 | "webob": { 151 | "hashes": [ 152 | "sha256:a3c89a8e9ba0aeb17382836cdb73c516d0ecf6630ec40ec28288f3ed459ce87b", 153 | "sha256:aa3a917ed752ba3e0b242234b2a373f9c4e2a75d35291dcbe977649bd21fd108" 154 | ], 155 | "index": "pypi", 156 | "version": "==1.8.6" 157 | }, 158 | "werkzeug": { 159 | "hashes": [ 160 | "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", 161 | "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" 162 | ], 163 | "version": "==1.0.1" 164 | } 165 | }, 166 | "develop": { 167 | "astroid": { 168 | "hashes": [ 169 | "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", 170 | "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" 171 | ], 172 | "version": "==2.3.3" 173 | }, 174 | "autopep8": { 175 | "hashes": [ 176 | "sha256:152fd8fe47d02082be86e05001ec23d6f420086db56b17fc883f3f965fb34954" 177 | ], 178 | "index": "pypi", 179 | "version": "==1.5.2" 180 | }, 181 | "colorama": { 182 | "hashes": [ 183 | "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", 184 | "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" 185 | ], 186 | "version": "==0.4.3" 187 | }, 188 | "honcho": { 189 | "hashes": [ 190 | "sha256:af5806bf13e3b20acdcb9ff8c0beb91eee6fe07393c3448dfad89667e6ac7576", 191 | "sha256:c189402ad2e337777283c6a12d0f4f61dc6dd20c254c9a3a4af5087fc66cea6e" 192 | ], 193 | "index": "pypi", 194 | "version": "==1.0.1" 195 | }, 196 | "isort": { 197 | "hashes": [ 198 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", 199 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" 200 | ], 201 | "index": "pypi", 202 | "version": "==4.3.21" 203 | }, 204 | "lazy-object-proxy": { 205 | "hashes": [ 206 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", 207 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", 208 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", 209 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", 210 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", 211 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", 212 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", 213 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", 214 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", 215 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", 216 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", 217 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", 218 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", 219 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", 220 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", 221 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", 222 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", 223 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", 224 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", 225 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", 226 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" 227 | ], 228 | "version": "==1.4.3" 229 | }, 230 | "mccabe": { 231 | "hashes": [ 232 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 233 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 234 | ], 235 | "version": "==0.6.1" 236 | }, 237 | "nose": { 238 | "hashes": [ 239 | "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", 240 | "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", 241 | "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" 242 | ], 243 | "index": "pypi", 244 | "version": "==1.3.7" 245 | }, 246 | "pycodestyle": { 247 | "hashes": [ 248 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 249 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 250 | ], 251 | "version": "==2.5.0" 252 | }, 253 | "pylint": { 254 | "hashes": [ 255 | "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", 256 | "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" 257 | ], 258 | "index": "pypi", 259 | "version": "==2.4.4" 260 | }, 261 | "pylint-flask": { 262 | "hashes": [ 263 | "sha256:f4d97de2216bf7bfce07c9c08b166e978fe9f2725de2a50a9845a97de7e31517" 264 | ], 265 | "index": "pypi", 266 | "version": "==0.6" 267 | }, 268 | "pylint-plugin-utils": { 269 | "hashes": [ 270 | "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a", 271 | "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a" 272 | ], 273 | "version": "==0.6" 274 | }, 275 | "python-termstyle": { 276 | "hashes": [ 277 | "sha256:6faf42ba42f2826c38cf70dacb3ac51f248a418e48afc0e36593df11cf3ab1d2", 278 | "sha256:f42a6bb16fbfc5e2c66d553e7ad46524ea833872f75ee5d827c15115fafc94e2" 279 | ], 280 | "index": "pypi", 281 | "version": "==0.1.10" 282 | }, 283 | "rednose": { 284 | "hashes": [ 285 | "sha256:6da77917788be277b70259edc0bb92fc6f28fe268b765b4ea88206cc3543a3e1" 286 | ], 287 | "index": "pypi", 288 | "version": "==1.3.0" 289 | }, 290 | "six": { 291 | "hashes": [ 292 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 293 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 294 | ], 295 | "version": "==1.14.0" 296 | }, 297 | "termstyle": { 298 | "hashes": [ 299 | "sha256:ef74b83698ea014112040cf32b1a093c1ab3d91c4dd18ecc03ec178fd99c9f9f" 300 | ], 301 | "version": "==0.1.11" 302 | }, 303 | "typed-ast": { 304 | "hashes": [ 305 | "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", 306 | "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", 307 | "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", 308 | "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", 309 | "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", 310 | "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", 311 | "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", 312 | "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", 313 | "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", 314 | "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", 315 | "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", 316 | "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", 317 | "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", 318 | "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", 319 | "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", 320 | "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", 321 | "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", 322 | "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", 323 | "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", 324 | "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", 325 | "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" 326 | ], 327 | "markers": "implementation_name == 'cpython' and python_version < '3.8'", 328 | "version": "==1.4.1" 329 | }, 330 | "wrapt": { 331 | "hashes": [ 332 | "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" 333 | ], 334 | "version": "==1.11.2" 335 | } 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uwsgi uwsgi.ini 2 | grunt_watch: grunt 3 | grunt_server: grunt connect -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyRegex 2 | 3 | [![Build Status](https://travis-ci.org/rscarvalho/pyregex.png)](https://travis-ci.org/rscarvalho/pyregex) 4 | 5 | PyRegex is an online Regular Expression tester for python dialect. 6 | 7 | ## License 8 | 9 | The code is licensed under the [GNU General Public License v2](LICENSE) 10 | 11 | ## Development Environment 12 | 13 | ### Requirements 14 | 15 | * Server-side language 16 | * [Python](http://www.python.org) 17 | * [Pip](http://www.pip-installer.org) 18 | 19 | * Testing (all of them installable via [pip](http://www.pip-installer.org/)) 20 | * [nose](https://nose.readthedocs.org/en/latest/) 21 | * I also use [rednose](https://pypi.python.org/pypi/rednose) but this one is optional 22 | 23 | * Assets management / generation 24 | * [Node.js](http://nodejs.org/) 25 | * [Npm](https://npmjs.org/) - Usually shipped with Node.js 26 | * [Grunt](http://gruntjs.com/) 27 | * [Bower](http://bower.io/) 28 | 29 | ### Dependency Installation 30 | 31 | * **Node.js** and **Npm** - See their websites ([2](http://nodejs.org/) and [3](http://npmjs.org)) about how to get them installed in your platform 32 | * **Grunt**: `npm install -g grunt-cli` 33 | * **Bower**: `npm install -g bower` 34 | * **Nose** and its companions: `pip install -r requirements.txt` (May require `sudo` or `su`) 35 | * For development, please install **also** `pip install -r requirements-dev.txt` 36 | * Assets dependencies: `./install_assets_deps.sh` 37 | 38 | 39 | ### Running the application 40 | 41 | Just run `honcho start` 42 | 43 | It will start both uWSGI server and grunt. This will watch the filesystem for changes in the source folders and regenerate the client-side target files as needed. 44 | 45 | The API will be available on http://localhost:5000 (The port can be overridden by the `$PORT` environment variable). The web app is available at http://localhost:8082 46 | 47 | ### Testing the application 48 | 49 | To test the application, run `nosetests -c nose.cfg` from a terminal window. 50 | 51 | ## Contributing 52 | 53 | Anyone is more than welcome to [Create an issue](https://github.com/rscarvalho/pyregex/issues), [Fork](https://github.com/rscarvalho/pyregex) the repository and submit a [Pull Request](https://github.com/rscarvalho/pyregex/pulls). 54 | 55 | 56 | ## TODO 57 | 58 | * Social integration (share regex on facebook, twitter, g+, etc.) 59 | * Save/generate regex permalink 60 | * I18n 61 | * Better result visualization 62 | -------------------------------------------------------------------------------- /assets/.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | build/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /assets/src/coffee/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rscarvalho/pyregex/209ad516393cbb903ba6cf9e498bbb89a517d91c/assets/src/coffee/controllers/.gitkeep -------------------------------------------------------------------------------- /assets/src/coffee/controllers/menu_controller.coffee: -------------------------------------------------------------------------------- 1 | @PyRegex().controller 'MenuController', ($scope) -> 2 | $scope.menuItems = [ 3 | id: "home" 4 | title: "Home", 5 | href: "/" 6 | , 7 | id: "changelog" 8 | title: "Changelog", 9 | href: "/changelog" 10 | , 11 | id: "help", 12 | title: "Help", 13 | href: "#", 14 | links: [ 15 | id: "documentation", 16 | title: "Official Python Documentation", 17 | href: "http://docs.python.org/library/re.html" 18 | ] 19 | , 20 | id: "contribute" 21 | title: "Contribute" 22 | href: "#" 23 | links: [ 24 | id: "contribute:develop" 25 | title: "Develop", 26 | href: "https://www.github.com/rscarvalho/pyregex" 27 | , 28 | id: "contribute:donate" 29 | title: "Donate", 30 | href: "https://www.paypal.com/cgi-bin/webscr?cmd=" + 31 | "_donations&business=MU86NLVAYBBQW&lc=GB&" + 32 | "item_name=Donate+To+Help+Funding+PyRegex+Project" + 33 | "&item_number=pyregex&" + 34 | "currency_code=USD&bn=PP%2dDonationsBF%3" + 35 | "abtn_donate_SM%2egif%3aNonHosted" 36 | ] 37 | ] 38 | 39 | $scope.hasLinks = (item) -> 40 | item.links && item.links.length 41 | 42 | $scope.currentUrl = '/' 43 | 44 | $scope.menuChanged = (item) -> 45 | $scope.currentUrl = item.href unless \ 46 | $scope.hasLinks(item) or /^.+:\/\//.test item.href 47 | 48 | @PyRegex().filter 'withLinks', -> 49 | (list) -> list.filter $scope.hasLinks 50 | -------------------------------------------------------------------------------- /assets/src/coffee/controllers/regex_parser.coffee: -------------------------------------------------------------------------------- 1 | ctrl = (_, RegexResource, RegexBuilder, 2 | templateUrl, $scope, $routeParams, $rootScope) -> 3 | 4 | $scope.allFlags = 5 | I: 'Ignore Case' 6 | L: 'Make \\w, \\W, \\b, \\B, \\s and \\S dependent on the current locale.' 7 | M: 'Multi-line Regular Expression' 8 | S: 'Make the "." special character match any character at all, ' + 9 | 'including a newline' 10 | U: 'Make \\w, \\W, \\b, \\B, \\d, \\D, \\s and \\S dependent on the ' + 11 | 'Unicode character properties database.' 12 | X: 'Verbose' 13 | 14 | pickTemplate = (name) -> 15 | $scope.resultTemplateUrl = templateUrl("regex/#{name}.html") 16 | 17 | pickTemplate('start') 18 | $scope.currentResult = {result_type: null} 19 | $scope.processing = false 20 | $scope.permalinkUrl = null 21 | 22 | $scope.re = RegexBuilder.clean() 23 | 24 | regexIsInValid = (regex) -> 25 | regex.source is null || 26 | regex.testString is null || 27 | regex.source is '' || 28 | regex.testString is '' 29 | 30 | $scope.getResults = -> 31 | return if $scope.processing 32 | 33 | if regexIsInValid(@re) 34 | $scope.currentResult.result = null 35 | return 36 | 37 | $scope.processing = true 38 | pickTemplate('start') 39 | 40 | data = $scope.re.data() 41 | 42 | RegexResource.test(data).then (result) -> 43 | $scope.processing = false 44 | $scope.currentResult = result.data 45 | if $scope.isError() 46 | $scope.permalinkUrl = null 47 | else 48 | $scope.permalinkUrl = "/?id=#{$scope.re.encodedData()}" 49 | pickTemplate('result') 50 | 51 | , (result) -> 52 | $scope.processing = false 53 | $scope.currentResult = result.data 54 | $scope.permalinkUrl = null 55 | pickTemplate('error') 56 | 57 | $scope.hasResult = -> 58 | $scope.isResult() and $scope.currentResult.result? 59 | 60 | checkResultType = (type) -> 61 | $scope.isResult() and $scope.currentResult.result_type == type 62 | 63 | $scope.isError = -> 64 | $scope.currentResult?.result_type == 'error' 65 | 66 | $scope.isResult = -> not $scope.isError() 67 | $scope.isFindall = -> checkResultType('findall') 68 | $scope.isSearch = -> checkResultType('search') 69 | $scope.isMatch = -> checkResultType('match') 70 | 71 | if $routeParams.id 72 | $scope.re.fromData $routeParams.id 73 | $scope.getResults() 74 | 75 | @PyRegex().controller('RegexParserController', ctrl) 76 | -------------------------------------------------------------------------------- /assets/src/coffee/directives/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rscarvalho/pyregex/209ad516393cbb903ba6cf9e498bbb89a517d91c/assets/src/coffee/directives/.gitkeep -------------------------------------------------------------------------------- /assets/src/coffee/directives/a.coffee: -------------------------------------------------------------------------------- 1 | @PyRegex().directive 'a', -> 2 | restrict: 'E' 3 | link: (scope, element, attr) -> 4 | update = -> 5 | href = element.attr('href') 6 | return unless /^.+:\/\//.test href 7 | 8 | element.attr('target', '_blank') 9 | 10 | element.bind 'change ngChange', update 11 | scope.$on 'ngRepeatFinished', update 12 | update() 13 | 14 | 15 | @PyRegex().directive 'ngRepeat', ($rootScope) -> 16 | (scope, element, attr) -> 17 | if scope.$last 18 | $rootScope.$broadcast('ngRepeatFinished') 19 | 20 | -------------------------------------------------------------------------------- /assets/src/coffee/directives/auto_grow.coffee: -------------------------------------------------------------------------------- 1 | @PyRegex().directive 'autoGrow', (escapeHtmlFilter) -> 2 | (scope, element, attr) -> 3 | minHeight = element[0].offsetHeight 4 | paddingLeft = parseInt(element.css('paddingLeft')) || 0 5 | paddingRight = parseInt(element.css('paddingRight')) || 0 6 | 7 | $shadow = angular.element('
').css 8 | position: 'absolute' 9 | top: -10000 10 | left: -10000 11 | width: (element[0].offsetWidth - paddingLeft - paddingRight) + 'px' 12 | fontSize: element.css('fontSize') 13 | fontFamily: element.css('fontFamily') 14 | lineHeight: element.css('lineHeight') 15 | resize: 'none' 16 | wordWrap: 'break-word' 17 | 18 | angular.element(document.body).append($shadow) 19 | 20 | update = -> 21 | val = escapeHtmlFilter(element.val()) 22 | $shadow.html(val) 23 | 24 | height = Math.max($shadow[0].offsetHeight + 10, minHeight) + 'px' 25 | 26 | element.css 'height', height 27 | 28 | 29 | element.bind('keyup keydown keypress change', update) 30 | 31 | scope.$on '$destroy', -> $shadow.remove() 32 | update() 33 | 34 | 35 | -------------------------------------------------------------------------------- /assets/src/coffee/directives/py_timer.coffee: -------------------------------------------------------------------------------- 1 | @PyRegex().directive 'pyTimeout', ($timeout) -> 2 | require: 'ngModel' 3 | link: (scope, ele, attrs, controller) -> 4 | promise = null 5 | sleep = if attrs.timeout then parseInt(attrs.timeout) else 3000 6 | controller.$viewChangeListeners.push -> 7 | if promise 8 | $timeout.cancel(promise) 9 | promise = null 10 | 11 | promise = $timeout -> 12 | scope.$eval(attrs.pyTimeout) 13 | , sleep 14 | -------------------------------------------------------------------------------- /assets/src/coffee/filters/escape_html.coffee: -------------------------------------------------------------------------------- 1 | @PyRegex().filter 'escapeHtml', -> 2 | times = (string, number) -> 3 | return '' if number <= 0 4 | if number == 1 then string else times(string, number - 1) 5 | 6 | (text) -> 7 | replacements = [ 8 | [/&/g, '&'], 9 | [//g, '>'], 11 | [/\n$/, '
 '], 12 | [/\n/g, '
'], 13 | [/\s{2,}/g, (space) -> times(' ', space.length - 1) + ' '] 14 | ] 15 | 16 | r = {} 17 | for pair in replacements 18 | r[pair[0]] = pair[1] 19 | 20 | window.REPLACEMENTS = r 21 | 22 | t = text 23 | for pair in replacements 24 | regex = pair[0] 25 | replacement = pair[1] 26 | t = t.replace(regex, replacement) 27 | 28 | return t -------------------------------------------------------------------------------- /assets/src/coffee/filters/length.coffee: -------------------------------------------------------------------------------- 1 | @PyRegex().filter 'length', (_) -> 2 | (value) -> 3 | return 0 if _.isUndefined(value) or _.isNull(value) 4 | 5 | if _.isArray(value) 6 | length = value.length 7 | if _.isObject(value[value.length - 1]) and 8 | !_.isUndefined(value[value.length - 1].$$hashKey) 9 | 10 | length -= 1 11 | 12 | return length 13 | 14 | # subtract 1 because of angular's $$hashKey 15 | if _.isObject(value) 16 | length = _.values(value).length 17 | if !_.isUndefined(value.$$hashKey) 18 | length -= 1 19 | return length 20 | 21 | value.length -------------------------------------------------------------------------------- /assets/src/coffee/main.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | config = ($locationProvider, $routeProvider, $httpProvider) -> 4 | $locationProvider.html5Mode(true) 5 | 6 | $routeProvider.when('/', 7 | templateUrl: '/assets/templates/index.html' 8 | controller: 'RegexParserController'). 9 | when('/changelog', 10 | templateUrl: '/assets/templates/changelog.html'). 11 | otherwise(redirectTo: '/') 12 | 13 | $httpProvider.defaults.useXDomain = true 14 | delete $httpProvider.defaults.headers.common['X-Requested-With'] 15 | 16 | app = angular.module('pyregex', ['ui.select2']).config(config) 17 | 18 | l = location 19 | # app.value('apiUrl', '/api') 20 | app.factory '_', -> window._ 21 | app.factory 'jQuery', -> window.jQuery 22 | app.factory 'window', -> window 23 | app.factory 'templateUrl', -> (name) -> "/assets/templates/#{name}" 24 | 25 | # Base64 encode/decode functions 26 | app.factory 'atob', -> window.atob 27 | app.factory 'btoa', -> window.btoa 28 | 29 | @PyRegex = -> angular.module('pyregex') 30 | -------------------------------------------------------------------------------- /assets/src/coffee/regex_builder.coffee: -------------------------------------------------------------------------------- 1 | @PyRegex().factory 'RegexBuilder', (_, atob, btoa, jQuery) -> 2 | class RegexBuilder 3 | I: 2 4 | L: 4 5 | M: 8 6 | S: 16 7 | U: 32 8 | X: 64 9 | allFlags: ['I', 'L', 'M', 'S', 'U', 'X'] 10 | 11 | constructor: -> 12 | @clean() 13 | 14 | getFlag: => 15 | flagValue = (f) => @[f] || 0 16 | 17 | flag = 0 18 | for key, value of @flags 19 | flag = flag | flagValue(key) if value 20 | return flag 21 | 22 | setFlags: (flags) => 23 | hasFlag = (f, i) => 24 | (f & i) != 0 25 | 26 | if _.isNumber(flags) 27 | _.forEach @allFlags, (el) => 28 | @flags[el] = hasFlag(flags, @[el]) 29 | else 30 | for key, value of flags 31 | @flags[key] = value == true unless _.isUndefined(@flags[key]) 32 | 33 | data: => 34 | regex: @source 35 | flags: @getFlag() 36 | match_type: @matchType 37 | test_string: @testString 38 | 39 | clean: => 40 | @flags = {} 41 | _.map @allFlags, (el) => @flags[el] = false 42 | 43 | @source = null 44 | @testString = null 45 | @matchType = 'match' 46 | 47 | @ 48 | 49 | encodedData: => 50 | json = JSON.stringify(@data()) 51 | encodeURIComponent(btoa(json)) 52 | 53 | fromData: (data) => 54 | decoded = decodeURIComponent(atob(data)) 55 | data = jQuery.parseJSON(decoded) 56 | @clean() 57 | @source = data.regex 58 | @testString = data.test_string 59 | @matchType = data.match_type 60 | @setFlags(data.flags) 61 | 62 | new RegexBuilder() 63 | -------------------------------------------------------------------------------- /assets/src/coffee/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rscarvalho/pyregex/209ad516393cbb903ba6cf9e498bbb89a517d91c/assets/src/coffee/resources/.gitkeep -------------------------------------------------------------------------------- /assets/src/coffee/resources/regex_resource.coffee: -------------------------------------------------------------------------------- 1 | @PyRegex().factory('RegexResource', (apiUrl, $http, jQuery) -> 2 | class RegexResource 3 | constructor: (http, apiUrl) -> 4 | @apiUrl = apiUrl 5 | @http = http 6 | 7 | test: (data) -> 8 | @http.get(@apiUrl + '/regex/test/?' + jQuery.param(data)) 9 | 10 | 11 | new RegexResource($http, apiUrl) 12 | ) 13 | -------------------------------------------------------------------------------- /assets/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rscarvalho/pyregex/209ad516393cbb903ba6cf9e498bbb89a517d91c/assets/src/favicon.ico -------------------------------------------------------------------------------- /assets/src/html/templates/index.html: -------------------------------------------------------------------------------- 1 |   2 |

Your Python Regular Expression's Best Buddy

3 |
4 |
5 |
6 | 9 |
10 | 11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 | 25 |
26 |
27 | 35 |
36 |
37 |
38 |
39 |
40 |
-------------------------------------------------------------------------------- /assets/src/html/templates/regex/error.html: -------------------------------------------------------------------------------- 1 |

2 | Error 3 | {{ currentResult.message }} 4 |

-------------------------------------------------------------------------------- /assets/src/html/templates/regex/result.html: -------------------------------------------------------------------------------- 1 |

2 | Success! Check your results below: 3 |

4 | 5 |
6 |
Groups:    {{currentResult.result|length}}
7 |
    8 |
  • 9 | {{result}} 10 |
  • 11 |
12 |
13 | 14 |
15 |

16 | Group {{currentResult.result.group}} 17 |

18 | 19 |
20 |
Groups:    {{currentResult.result.groups|length}}
21 |
    22 |
  • {{r}}
  • 23 |
24 |
25 |
26 |
Group Dict:   {{currentResult.result.group_dict|length}}
27 |
    28 |
  • 29 | {{name}}: {{group}} 30 |
  • 31 |
32 |
33 |
34 | 35 |

36 | Link to this Regex 37 |

38 | 39 |
40 | Notice No results found. 41 |
-------------------------------------------------------------------------------- /assets/src/html/templates/regex/start.html: -------------------------------------------------------------------------------- 1 |
2 | Info Your results will appear here. 3 |
4 |
5 | Running 6 |
-------------------------------------------------------------------------------- /assets/src/images/pyregex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | .* 7 | 8 | -------------------------------------------------------------------------------- /assets/src/less/main.less: -------------------------------------------------------------------------------- 1 | @footer-height: 120px; 2 | 3 | html, body { 4 | height: 100%; 5 | } 6 | 7 | div.navbar { 8 | h1 { 9 | margin: 0; 10 | padding: 0; 11 | line-height: 0.45em; 12 | } 13 | } 14 | 15 | a.brand span.logo { 16 | color: #ce4213; 17 | background: white; 18 | display: inline-block; 19 | padding: 5px 0 5px 4px; 20 | font-size: 0.9em; 21 | line-height: 0.7em; 22 | border-radius: 2px; 23 | margin-bottom: -5px; 24 | letter-spacing: 3px; 25 | } 26 | 27 | div.regex-parser-container { 28 | input[type='text'], label, span.quotes { 29 | font-family: monospace; 30 | } 31 | 32 | span.quotes { 33 | color: rgb(25, 197, 25); 34 | } 35 | 36 | span.reserved { 37 | color: #bc8600; 38 | } 39 | 40 | textarea { 41 | width: 560px; 42 | max-width: 480px; 43 | min-width: 480px; 44 | resize: none; 45 | 46 | &#test-string { 47 | height: 120px; 48 | min-height: 120px; 49 | } 50 | 51 | &#regex { 52 | height: 12px; 53 | min-height: 12px; 54 | font-family: 'SourceCode Pro', Monaco, Consolas, monospace; 55 | } 56 | } 57 | } 58 | 59 | div.cheat-sheet { 60 | border-top: 1px solid #e5e5e5; 61 | margin-top: 20px; 62 | padding-top: 20px; 63 | } 64 | 65 | footer.main { 66 | text-align: center; 67 | border-top: 1px solid #e5e5e5; 68 | background-color: #f5f5f5; 69 | height: @footer-height - 1px; 70 | 71 | a { 72 | text-decoration: none; 73 | opacity: 0.8; 74 | &:hover { 75 | opacity: 1.0; 76 | } 77 | } 78 | } 79 | 80 | body > .wrap { 81 | min-height: 100%; 82 | height: auto !important; 83 | height: 100%; 84 | margin: 0 auto (-@footer-height); /* the bottom margin is the negative value of the footer's height */ 85 | 86 | .push { 87 | height: @footer-height; 88 | } 89 | 90 | & > .container { 91 | padding-top: 30px; 92 | } 93 | } 94 | 95 | div.container { 96 | div.result { 97 | p.main-group { 98 | overflow: auto; 99 | } 100 | } 101 | } 102 | 103 | /* 104 | The animate-enter prefix is the event name that you 105 | have provided within the ngAnimate attribute. 106 | */ 107 | .animate-enter-setup { 108 | -webkit-transition: 1s linear all; /* Safari/Chrome */ 109 | -moz-transition: 1s linear all; /* Firefox */ 110 | -ms-transition: 1s linear all; /* IE10 */ 111 | -o-transition: 1s linear all; /* Opera */ 112 | transition: 1s linear all; /* Future Browsers */ 113 | 114 | /* The animation preparation code */ 115 | opacity: 0; 116 | } 117 | 118 | /* 119 | Keep in mind that you want to combine both CSS 120 | classes together to avoid any CSS-specificity 121 | conflicts 122 | */ 123 | .animate-enter-setup.animate-enter-start { 124 | /* The animation code itself */ 125 | opacity: 1; 126 | } 127 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assets", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "normalize-css": "~2.1.2", 6 | "angular-ui-select2": "0.0.2", 7 | "select2": "~3.4.1", 8 | "angular": "1.0.7", 9 | "underscore": "~1.5.1", 10 | "angular-mocks": "1.0.7", 11 | "bootstrap-css": "~2.3.2", 12 | "base64": "~0.1.1", 13 | "modernizr": "~2.6.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /grunt/templates/default_md.jst: -------------------------------------------------------------------------------- 1 |   2 | <%= content %> 3 | -------------------------------------------------------------------------------- /install_assets_deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | run_with_retry() { 4 | MAX_RETRIES=$1 5 | shift 6 | NUM_RETRIES=1 7 | 8 | echo "Max Retries: $MAX_RETRIES" 9 | 10 | CMD=$* 11 | 12 | while [ $NUM_RETRIES -le $(( MAX_RETRIES )) ]; do 13 | echo "Running $CMD..." 14 | $CMD 15 | status=$? 16 | 17 | if [ $status -eq 0 ]; then 18 | break 19 | else 20 | NUM_RETRIES=$(( NUM_RETRIES+1 )) 21 | fi 22 | done 23 | 24 | if [ $NUM_RETRIES -eq $(( MAX_RETRIES )) ]; then 25 | echo "npm install failed." 26 | exit -1 27 | fi 28 | } 29 | 30 | run_with_retry 3 npm install -gs karma bower grunt-cli && 31 | 32 | run_with_retry 3 npm install -s && 33 | 34 | run_with_retry 3 bower install -q 35 | 36 | exit $? -------------------------------------------------------------------------------- /nose.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=2 3 | rednose=1 4 | stop=1 5 | nocapture=1 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pyregex-assets", 3 | "version": "0.0.0", 4 | "author": "Rodolfo Carvalho", 5 | "dependencies": { 6 | "grunt": "~0.4.0", 7 | "grunt-contrib-coffee": "~0.7.0", 8 | "grunt-contrib-copy": "~0.4.1", 9 | "grunt-contrib-watch": "~0.5.1", 10 | "grunt-contrib-concat": "~0.3.0", 11 | "grunt-coffeelint": "0.0.7", 12 | "grunt-contrib-clean": "~0.5.0", 13 | "grunt-contrib-less": "~0.7.0", 14 | "grunt-s3-sync": "~0.2.1" 15 | }, 16 | "devDependencies": { 17 | "grunt-notify": "~0.2.4", 18 | "karma-script-launcher": "~0.1.0", 19 | "karma-chrome-launcher": "~0.1.0", 20 | "karma-firefox-launcher": "~0.1.0", 21 | "karma-html2js-preprocessor": "~0.1.0", 22 | "karma-jasmine": "~0.1.0", 23 | "karma-requirejs": "~0.2.0", 24 | "karma-coffee-preprocessor": "~0.1.0", 25 | "karma-phantomjs-launcher": "~0.1.0", 26 | "karma": "~0.10.1", 27 | "grunt-karma": "~0.6.1", 28 | "karma-growl-reporter": "~0.1.0", 29 | "grunt-contrib-connect": "~0.3.0", 30 | "grunt-markdown": "~0.4.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rscarvalho/pyregex/209ad516393cbb903ba6cf9e498bbb89a517d91c/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | PyRegex 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 45 | 46 |
47 |
48 |
49 | 50 |
51 |
52 | Python Regular Expression's Cheat Sheet 53 | (borrowed from pythex) 54 |
55 | 56 |
57 |
58 |
Special Characters
59 |
60 |
    61 |
  • \ escape special characters
  • 62 |
  • . matches any character
  • 63 |
  • ^ matches beginning of string
  • 64 |
  • $ matches end of string
  • 65 |
  • [5b-d] matches any chars '5', 'b', 'c' or 'd'
  • 66 |
  • [^a-c6] matches any char except 'a', 'b', 'c' or '6'
  • 67 |
  • R|S matches either regex R or regex S
  • 68 |
  • () creates a capture group and indicates precedence
  • 69 |
70 |
71 |
72 |
73 |
Quantifiers
74 |
75 |
    76 |
  • * 0 or more (append ? for non-greedy)
  • 77 |
  • + 1 or more (append ? for non-greedy)
  • 78 |
  • ? 0 or 1 (append ? for non-greedy)
  • 79 |
  • {m} exactly mm occurrences
  • 80 |
  • {m, n} from m to n. m defaults to 0, n to infinity
  • 81 |
  • {m, n}? from m to n, as few as possible
  • 82 |
83 |
84 |
85 |
86 |
87 |
88 |
Special sequences
89 |
90 |
    91 |
  • \A start of string
  • 92 |
  • 93 | \b matches empty string at word boundary 94 | (between \w and \W) 95 |
  • 96 |
  • \B matches empty string not at word boundary
  • 97 |
  • \d digit
  • 98 |
  • \D non-digit
  • 99 |
  • \s whitespace: [ \t\n\r\f\v]
  • 100 |
  • \S non-whitespace
  • 101 |
  • \w alphanumeric: [0-9a-zA-Z_]
  • 102 |
  • \W non-alphanumeric
  • 103 |
  • \Z end of string
  • 104 |
  • \g<id> matches a previously defined group
  • 105 |
106 |
107 |
108 |
109 |
Extensions
110 |
111 |
    112 |
  • (?iLmsux) Matches empty string, sets re.X flags
  • 113 |
  • (?:...) Non-capturing version of regular parentheses
  • 114 |
  • (?P<name>...) Creates a named capturing group.
  • 115 |
  • (?P=name) Matches whatever matched previously named group
  • 116 |
  • (?#...) A comment; ignored.
  • 117 |
  • (?=...) Lookahead assertion: Matches without consuming
  • 118 |
  • (?!...) Negative lookahead assertion
  • 119 |
  • (?<=...) Lookbehind assertion: Matches if preceded
  • 120 |
  • (?<!...) Negative lookbehind assertion
  • 121 |
  • (?(id)yes|no) Match 'yes' if group 'id' matched, else 'no'
  • 122 |
123 |
124 |
125 |
126 | 127 |
128 |

For a complete guide on regular expressions and the python re module, please 129 | visit the 130 | official documentation. 131 |

132 |
133 |
134 |
135 | 136 |
 
137 |
138 | 139 | 161 | 162 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | http://www.pyregex.com/ 9 | weekly 10 | 11 | -------------------------------------------------------------------------------- /pyregex/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rscarvalho/pyregex/209ad516393cbb903ba6cf9e498bbb89a517d91c/pyregex/__init__.py -------------------------------------------------------------------------------- /pyregex/api.py: -------------------------------------------------------------------------------- 1 | import os 2 | from base64 import b64decode 3 | from os.path import dirname, realpath 4 | 5 | import rollbar 6 | import rollbar.contrib.flask 7 | from flask import Flask, got_request_exception, request 8 | from flask.json import jsonify 9 | from flask_cors import CORS 10 | 11 | from pyregex.service import (InvalidRegexError, RegexService, 12 | UnprocessibleRegexError) 13 | 14 | # pylint: disable=invalid-name 15 | app = Flask('pyregex') 16 | CORS(app) 17 | 18 | 19 | def api_error(message, *args, **kwargs): 20 | response = jsonify(result_type='error', message=message % args) 21 | response.status_code = int(kwargs.get('status', 400)) 22 | response.headers['Content-type'] = 'application/json' 23 | return response 24 | 25 | 26 | @app.before_first_request 27 | def init_rollbar(): 28 | """Initializes the rollbar module 29 | """ 30 | token = os.environ.get("ROLLBAR_TOKEN") 31 | environment = os.environ.get("ROLLBAR_ENV", "development") 32 | if token: 33 | rollbar.init(token, environment, root=dirname( 34 | realpath(__file__)), allow_logging_basic_config=False) 35 | got_request_exception.connect( 36 | rollbar.contrib.flask.report_exception, app) 37 | else: 38 | logger = app.logger 39 | 40 | # pylint: disable=no-member 41 | logger.info( 42 | "Rollbar token not present. Skipping rollbar setup") 43 | 44 | 45 | if 'FLASK_SECRET_KEY' in os.environ: 46 | app.secret_key = b64decode(os.environ['FLASK_SECRET_KEY']) 47 | else: 48 | app.secret_key = '\x0f0%T\xd3\xd5\x11\xca\xaa\xf5,\x02Zp,"\x83\x94\x1b\x9e|6\xd7<' 49 | 50 | 51 | @app.route('/api/regex/test/', methods=['GET']) 52 | def test_regex(): 53 | match_type = request.values.get('match_type', 'findall') 54 | regex = request.values.get('regex', '') 55 | test_string = request.values.get('test_string', '') 56 | 57 | try: 58 | flags = int(request.values.get('flags', '0')) 59 | except TypeError: 60 | flags = 0 61 | 62 | try: 63 | service = RegexService(regex, match_type, flags) 64 | except ValueError as error: 65 | fmt = 'Invalid value for {}: "{}"' 66 | if len(error.args) > 2: 67 | fmt += ". Acceptable values are {}" 68 | 69 | args = [", ".join(a) if isinstance(a, tuple) 70 | else a for a in error.args] 71 | return api_error(fmt.format(*args)) 72 | except InvalidRegexError: 73 | return api_error('Invalid regular expression: %s' % regex) 74 | 75 | try: 76 | result = service.test(test_string) 77 | except UnprocessibleRegexError: 78 | return api_error('This regular expression is unprocessible', status=422) 79 | 80 | return jsonify(result_type=match_type, result=result) 81 | -------------------------------------------------------------------------------- /pyregex/service.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sre_constants 3 | from multiprocessing import Process, Queue 4 | 5 | from .util import Value, dict_from_object 6 | 7 | SINGLE_PROCESS = False 8 | 9 | 10 | class InvalidRegexError(Exception): 11 | def __init__(self, error, *args, **kwargs): 12 | super(InvalidRegexError, self).__init__(*args, **kwargs) 13 | self.error = error 14 | 15 | 16 | class UnprocessibleRegexError(Exception): 17 | def __init__(self, *args, **kwargs): 18 | super(UnprocessibleRegexError, self).__init__(*args, **kwargs) 19 | 20 | 21 | class RegexService(Value): 22 | VALID_MATCH_TYPES = ('findall', 'match', 'search',) 23 | REGEX_TIMEOUT = 2 # seconds 24 | 25 | def __init__(self, regex, match_type, flags): 26 | if regex is None: 27 | raise ValueError('regex', regex) 28 | 29 | if match_type not in self.VALID_MATCH_TYPES: 30 | raise ValueError('match_type', match_type, self.VALID_MATCH_TYPES) 31 | 32 | if not isinstance(flags, int) or flags < 0: 33 | raise ValueError('flags', flags) 34 | 35 | try: 36 | re.compile(regex, flags) 37 | except sre_constants.error as error: 38 | raise InvalidRegexError(error) 39 | 40 | super(RegexService, self).__init__( 41 | pattern=regex, match_type=match_type, flags=flags) 42 | 43 | def test(self, test_string): 44 | def run_regex_test(pattern, match_type, flags, test_string, queue): 45 | regex = re.compile(pattern, flags) 46 | callback = getattr(regex, match_type) 47 | queue.put(dict_from_object(callback(test_string))) 48 | 49 | queue = Queue() 50 | # TODO; Let us just remove those immutable types because they're not very useful 51 | # pylint: disable=no-member 52 | args = (self.pattern, self.match_type, self.flags, test_string, queue) 53 | 54 | if SINGLE_PROCESS: 55 | run_regex_test(*args) 56 | else: 57 | process = Process(target=run_regex_test, args=args) 58 | process.start() 59 | process.join(self.REGEX_TIMEOUT) 60 | if process.is_alive(): 61 | process.terminate() 62 | raise UnprocessibleRegexError() 63 | return queue.get() 64 | -------------------------------------------------------------------------------- /pyregex/util.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | class ValueMeta(type): 4 | def __new__(cls, name, bases, dct): 5 | obj = type.__new__(cls, name, bases, dct) 6 | 7 | old_setattr = obj.__setattr__ 8 | 9 | def _setattr(self, *args): 10 | if hasattr(self, '__immutable__') and self.__immutable__: 11 | raise TypeError("can't modify immutable instance") 12 | old_setattr(self, *args) 13 | 14 | obj.__setattr__ = _setattr 15 | obj.__delattr__ = _setattr 16 | 17 | return obj 18 | 19 | def __call__(cls, *args, **kwargs): 20 | obj = type.__call__(cls, *args, **kwargs) 21 | obj.__immutable__ = True 22 | return obj 23 | 24 | 25 | class Value(object, metaclass=ValueMeta): 26 | def __init__(self, **kwargs): 27 | for key in kwargs: 28 | setattr(self, key, kwargs[key]) 29 | 30 | 31 | def dict_from_object(obj): 32 | if obj and hasattr(obj, 'groupdict') and callable(getattr(obj, 'groupdict')): 33 | return dict( 34 | group=obj.group(), 35 | groups=obj.groups(), 36 | group_dict=obj.groupdict(), 37 | end=obj.end(), start=obj.start(), pos=obj.pos, 38 | span=obj.span(), 39 | regs=obj.regs, 40 | last_group=obj.lastgroup, 41 | last_index=obj.lastindex 42 | ) 43 | elif not obj: 44 | return None 45 | return obj 46 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.7.6 2 | -------------------------------------------------------------------------------- /tests/api_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from urllib.parse import urlencode 3 | from pyregex.api import app 4 | import re 5 | import json 6 | import signal 7 | from webob import Request 8 | 9 | 10 | def regex_params(regex, test_string, flags=0, match_type='match'): 11 | return dict(flags=int(flags), regex=regex, test_string=test_string, match_type=match_type) 12 | 13 | 14 | def build_request(url, query_dict=None, *args, **kwargs): 15 | if query_dict: 16 | url = "%s?%s" % (url, urlencode(query_dict)) 17 | r = Request.blank(url, *args, **kwargs) 18 | r.headers['Accept'] = '*/*' 19 | return r 20 | 21 | 22 | class RegexHandlerTest(unittest.TestCase): 23 | def test_index(self): 24 | request = Request.blank('/api/regex/') 25 | response = request.get_response(app) 26 | self.assertEqual(404, response.status_int) 27 | 28 | def test_regexTestFindall(self): 29 | params = regex_params(r'\w+', u'Hello, World!', 30 | re.I | re.M, 'findall') 31 | request = build_request('/api/regex/test/', params) 32 | response = request.get_response(app) 33 | 34 | json_body = self.get_json_response(response) 35 | self.assertEqual('findall', json_body['result_type']) 36 | self.assertEqual(['Hello', 'World'], json_body['result']) 37 | 38 | def test_regexTestFindall2(self): 39 | params = regex_params( 40 | r'[\w\']+', u'Hey, you - what are you doing here!?', 0, 'findall') 41 | request = build_request('/api/regex/test/', params) 42 | response = request.get_response(app) 43 | 44 | json_body = self.get_json_response(response) 45 | self.assertEqual('findall', json_body['result_type']) 46 | self.assertEqual(['Hey', 'you', 'what', 'are', 'you', 47 | 'doing', 'here'], json_body['result']) 48 | 49 | def test_regexTestFindallNotAMatch(self): 50 | params = regex_params(r'\d+', u'Hello, World!', 51 | re.I | re.M, 'findall') 52 | request = build_request('/api/regex/test/', params) 53 | response = request.get_response(app) 54 | 55 | json_body = self.get_json_response(response) 56 | self.assertEqual('findall', json_body['result_type']) 57 | self.assertEqual(None, json_body['result']) 58 | 59 | def test_regexTestMatch(self): 60 | params = regex_params( 61 | r'\[(\d{4,}-\d{2,}-\d{2})\] Testing beginning on server (.+)$', 62 | u'[2013-07-23] Testing beginning on server example.com') 63 | 64 | expected = dict( 65 | group='[2013-07-23] Testing beginning on server example.com', 66 | groups=['2013-07-23', 'example.com'], 67 | group_dict={}, 68 | end=52, start=0, pos=0, 69 | span=[0, 52], 70 | regs=[[0, 52], [1, 11], [41, 52]], 71 | last_group=None, 72 | last_index=2 73 | ) 74 | 75 | request = build_request('/api/regex/test/', params) 76 | response = request.get_response(app) 77 | json_body = self.get_json_response(response) 78 | self.assertEqual('match', json_body['result_type']) 79 | self.assertEqual(expected, json_body['result']) 80 | 81 | def test_regexTestMatchNotAMatch(self): 82 | params = regex_params( 83 | r'\[(\d{4,}-\d{2,}-\d{2})\] Testing beginning on server (.+)$', 84 | u'Not Matching [2013-07-23] Testing beginning on server example.com') 85 | 86 | expected = None 87 | 88 | request = Request.blank( 89 | '/api/regex/test/?%s' % urlencode(params)) 90 | response = request.get_response(app) 91 | json_body = self.get_json_response(response) 92 | self.assertEqual('match', json_body['result_type']) 93 | self.assertEqual(expected, json_body['result']) 94 | 95 | def test_regexTestSearch(self): 96 | params = regex_params( 97 | r'^\[(\d{4,}-\d{2,}-\d{2})\] Testing beginning on server (.+)$', 98 | u'[2013-07-23] Testing beginning on server example.com', 99 | match_type='search') 100 | 101 | expected = dict( 102 | group='[2013-07-23] Testing beginning on server example.com', 103 | groups=['2013-07-23', 'example.com'], 104 | group_dict={}, 105 | end=52, start=0, pos=0, 106 | span=[0, 52], 107 | regs=[[0, 52], [1, 11], [41, 52]], 108 | last_group=None, 109 | last_index=2 110 | ) 111 | 112 | request = build_request('/api/regex/test/', params) 113 | response = request.get_response(app) 114 | json_body = self.get_json_response(response) 115 | self.assertEqual('search', json_body['result_type']) 116 | self.assertEqual(expected, json_body['result']) 117 | 118 | def test_regexTestInvalidRegex(self): 119 | params = regex_params('[Not balanced', '[Not balanced') 120 | 121 | request = build_request('/api/regex/test/', params) 122 | response = request.get_response(app) 123 | json_body = self.get_json_response(response, 400) 124 | self.assertEqual('error', json_body['result_type']) 125 | self.assertEqual('Invalid regular expression: %s' % 126 | params['regex'], json_body['message']) 127 | 128 | def test_regexTestUnknown(self): 129 | params = regex_params(r'\w+', 'Hello, World!', 130 | re.I | re.M, 'not_really_sure_about_it') 131 | 132 | request = build_request('/api/regex/test/', params) 133 | response = request.get_response(app) 134 | 135 | json_body = self.get_json_response(response, 400) 136 | self.assertEqual('error', json_body['result_type']) 137 | expected = r'Invalid value for match_type: "not_really_sure_about_it"\. Acceptable values are .+$' 138 | self.assertRegexpMatches(json_body['message'], expected) 139 | 140 | def test_regexTestMultilineFindallNoFlags(self): 141 | params = regex_params(r"\w+\n\d+", "Hi 2013", match_type="findall") 142 | 143 | request = build_request('/api/regex/test/', params) 144 | response = request.get_response(app) 145 | json_body = self.get_json_response(response) 146 | self.assertEqual('findall', json_body['result_type']) 147 | self.assertIsNone(json_body['result']) 148 | 149 | def test_regexTestMultilineFindallWithFlags(self): 150 | regex = '\\w+\n\\.\n\\d+' 151 | params = regex_params(regex, "Hi\n.\n2013", 152 | match_type="findall", flags=re.X) 153 | 154 | request = build_request('/api/regex/test/', params) 155 | response = request.get_response(app) 156 | json_body = self.get_json_response(response) 157 | self.assertEqual('findall', json_body['result_type']) 158 | self.assertIsNone(json_body['result']) 159 | 160 | params.update(test_string="Hi.2013") 161 | request = build_request('/api/regex/test/', params) 162 | response = request.get_response(app) 163 | json_body = self.get_json_response(response) 164 | self.assertEqual('findall', json_body['result_type']) 165 | self.assertEqual(['Hi.2013'], json_body['result']) 166 | 167 | def test_testRegexCatastrophicBacktrace(self): 168 | test_string = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + \ 169 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 170 | regex = r"(a?a)+b" 171 | 172 | params = regex_params(regex, test_string) 173 | 174 | TimeoutException = type('TimeoutException', (Exception,), {}) 175 | 176 | def timeout_cb(signum, frame): 177 | raise TimeoutException() 178 | 179 | old_handler = signal.signal(signal.SIGALRM, timeout_cb) 180 | signal.alarm(5) 181 | 182 | try: 183 | request = build_request('/api/regex/test/', params) 184 | response = request.get_response(app) 185 | json_body = self.get_json_response(response, 422) 186 | self.assertEqual('error', json_body['result_type']) 187 | self.assertEqual( 188 | 'This regular expression is unprocessible', json_body['message']) 189 | except TimeoutException as e: 190 | self.fail("Response took more than 5 seconds to execute") 191 | finally: 192 | signal.alarm(0) 193 | signal.signal(signal.SIGALRM, old_handler) 194 | 195 | def get_json_response(self, response, *acceptable_statuses): 196 | if not acceptable_statuses: 197 | acceptable_statuses = [200] 198 | 199 | self.assertIn(response.status_int, acceptable_statuses) 200 | self.assertEqual('application/json', response.content_type) 201 | return json.loads(response.body.decode('utf-8')) 202 | -------------------------------------------------------------------------------- /tests/client/controllers/regex_parser_controller_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "RegexParserController", -> 2 | $scope = null 3 | $controller = null 4 | 5 | beforeEach module('pyregex') 6 | beforeEach( 7 | inject ($rootScope, _, _$controller_, RegexBuilder, RegexResource) -> 8 | $scope = $rootScope.$new() 9 | $controller = _$controller_('RegexParserController', 10 | _: _ 11 | RegexResource: RegexResource 12 | $scope: $scope 13 | RegexBuilder: RegexBuilder 14 | ) 15 | ) 16 | 17 | describe "$scope", -> 18 | it "should contain valid scope variables after initialization", -> 19 | inject ($rootScope, _, $controller, RegexBuilder) -> 20 | $scope = $rootScope.$new() 21 | $controller('RegexParserController', 22 | _: _ 23 | RegexResource: {} 24 | $scope: $scope 25 | RegexBuilder: RegexBuilder 26 | ) 27 | templateUrl = '/assets/templates/regex/start.html' 28 | expect($scope.reFlags).toBe(undefined) 29 | expect($scope.re).toBe(RegexBuilder) 30 | expect($scope.re.matchType).toBe('match') 31 | expect($scope.resultTemplateUrl).toBe(templateUrl) 32 | expect($scope.currentResult).toEqual(result_type: null) 33 | expect($scope.processing).toBe(false) 34 | expect($scope.re.getFlag()).toBe(0) 35 | 36 | describe "result processing", -> 37 | it "should reflect webserver result on $scope", inject (jQuery) -> 38 | $httpBackend = null 39 | $scope.re.setFlags(I: true) 40 | $scope.re.source = "Hello, (.+?)$" 41 | $scope.re.testString = "Hello, World!" 42 | $scope.re.matchType = "search" 43 | 44 | data = jQuery.param($scope.re.data()) 45 | 46 | inject (_$httpBackend_) -> 47 | $httpBackend = _$httpBackend_ 48 | baseUrl = 'http://localhost:5000/api' 49 | $httpBackend.expectGET(baseUrl + '/regex/test/?' + data). 50 | respond(result_type: 'search') 51 | 52 | expect($scope.processing).toBe false 53 | $scope.getResults() 54 | expect($scope.processing).toBe true 55 | 56 | $httpBackend.flush() 57 | expect($scope.processing).toBe false 58 | 59 | result = $scope.currentResult 60 | expect(result.result_type).toBe 'search' 61 | 62 | $httpBackend.verifyNoOutstandingExpectation() 63 | $httpBackend.verifyNoOutstandingRequest() 64 | 65 | it "should have correct values for status functions", -> 66 | result = $scope.currentResult 67 | result.result_type = 'match' 68 | 69 | expect($scope.isError()).toBe(false) 70 | expect($scope.isResult()).toBe(true) 71 | expect($scope.isMatch()).toBe(true) 72 | 73 | -------------------------------------------------------------------------------- /tests/client/controllers/regex_parser_controller_spec.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | (function() { 3 | describe("RegexParserController", function() { 4 | beforeEach(module('pyregex')); 5 | return describe("$scope", function() { 6 | return it("should contain valid scope variables after initialization", function() { 7 | return inject(function($rootScope, _, $controller, RegexBuilder) { 8 | var $scope; 9 | $scope = $rootScope.$new(); 10 | $controller('RegexParserController', { 11 | _: _, 12 | RegexResource: {}, 13 | $scope: $scope, 14 | RegexBuilder: RegexBuilder 15 | }); 16 | expect($scope.reFlags).toBe(void 0); 17 | expect($scope.re).toBe(RegexBuilder); 18 | expect($scope.re.matchType).toBe('match'); 19 | expect($scope.resultTemplateUrl).toBe('/assets/templates/regex/start.html'); 20 | expect($scope.currentResult).toEqual({ 21 | result_type: null 22 | }); 23 | expect($scope.processing).toBe(false); 24 | return expect($scope.re.getFlag()).toBe(0); 25 | }); 26 | }); 27 | }); 28 | }); 29 | 30 | }).call(this); 31 | -------------------------------------------------------------------------------- /tests/client/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Thu Aug 08 2013 14:03:22 GMT-0300 (BRT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | frameworks: ['jasmine'], 13 | 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | '../../public/assets/*.js', 18 | '../../public/assets/**/*.js', 19 | '../../assets/bower_components/angular-mocks/angular-mocks.js', 20 | './**/*_spec.coffee', 21 | ], 22 | 23 | 24 | // list of files to exclude 25 | exclude: [ 26 | 27 | ], 28 | 29 | 30 | // test results reporter to use 31 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 32 | reporters: ['dots', 'growl'], 33 | 34 | 35 | // web server port 36 | port: 9876, 37 | 38 | 39 | // enable / disable colors in the output (reporters and logs) 40 | colors: true, 41 | 42 | 43 | // level of logging 44 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 45 | logLevel: config.LOG_INFO, 46 | 47 | 48 | // enable / disable watching file and executing tests whenever any file changes 49 | autoWatch: true, 50 | 51 | 52 | // Start these browsers, currently available: 53 | // - Chrome 54 | // - ChromeCanary 55 | // - Firefox 56 | // - Opera 57 | // - Safari (only Mac) 58 | // - PhantomJS 59 | // - IE (only Windows) 60 | browsers: ['PhantomJS'], 61 | 62 | 63 | // If browser does not capture in given timeout [ms], kill it 64 | captureTimeout: 60000, 65 | 66 | 67 | // Continuous Integration mode 68 | // if true, it capture browsers, run tests and exit 69 | singleRun: true, 70 | 71 | preprocessors: { 72 | './**/*_spec.coffee': ['coffee'] 73 | } 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /tests/client/module_config_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "App Configuration", -> 2 | beforeEach module('pyregex') 3 | 4 | it "should have a value for API endpoint", inject (apiUrl) -> 5 | expect(apiUrl).toBe('http://localhost:5000/api') 6 | 7 | describe "factories", -> 8 | it "should reflect jQuery global object", inject (jQuery) -> 9 | expect(jQuery).toBe(window.jQuery) 10 | 11 | it "should reflect underscore (_) global object", inject (_) -> 12 | expect(_).toBe(window._) 13 | 14 | it "should reflect global window object", inject (_window_) -> 15 | expect(_window_).toBe(window) 16 | 17 | describe "filters", -> 18 | it "should return the number of elements of a given array or object", 19 | inject (lengthFilter) -> 20 | expect(lengthFilter(a: 1, b: 2, c: 3, d: 4)).toBe(4) 21 | expect(lengthFilter([1, 2, 3])).toBe(3) 22 | expect(lengthFilter([1, 2, 3, $$hashKey: {}])).toBe(3) 23 | expect(lengthFilter(a: 1, b: 2, $$hashKey: {})).toBe(2) 24 | 25 | it "should return a text with escaped html elements", 26 | inject (escapeHtmlFilter) -> 27 | original = "\n" 28 | expected = "<Hello, world!
This is some extra & " + 29 | "html chars</>
 " 30 | expect(escapeHtmlFilter(original)).toEqual expected 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/client/regex_builder_spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'RegexBuilder', -> 2 | $builder = null 3 | 4 | beforeEach module('pyregex') 5 | beforeEach inject (RegexBuilder) -> 6 | $builder = RegexBuilder 7 | 8 | describe 'initialization', -> 9 | it 'should initialize re flags to false', -> 10 | expect($builder.flags.I).toBe false 11 | expect($builder.flags.L).toBe false 12 | expect($builder.flags.M).toBe false 13 | expect($builder.flags.S).toBe false 14 | expect($builder.flags.U).toBe false 15 | expect($builder.flags.X).toBe false 16 | 17 | it 'should initialize matchType to "match"', -> 18 | expect($builder.matchType).toBe 'match' 19 | 20 | it 'should calculate builder flags according to Python api', -> 21 | $builder.flags.I = true 22 | expect($builder.getFlag()).toBe 2 23 | 24 | $builder.flags.L = true 25 | expect($builder.getFlag()).toBe 6 26 | 27 | $builder.flags.I = false 28 | expect($builder.getFlag()).toBe 4 29 | 30 | $builder.flags.X = true 31 | expect($builder.getFlag()).toBe 68 32 | 33 | it 'should calculate builder flags according to an integer number', -> 34 | $builder.clean() 35 | expect($builder.flags.I).toBe false 36 | $builder.setFlags(2) 37 | expect($builder.flags.I).toBe true 38 | $builder.setFlags(4) 39 | expect($builder.flags.I).toBe false 40 | expect($builder.flags.L).toBe true 41 | 42 | 43 | 44 | it 'should generate form data to send to the API', -> 45 | $builder.setFlags I: true, L: true, X: false 46 | 47 | $builder.source = "(\\w+)" 48 | $builder.testString = "Hello, World!" 49 | $builder.matchType = 'search' 50 | 51 | data = $builder.data() 52 | 53 | expect(_.keys(data)).toEqual ['regex', 'flags', 'match_type', 'test_string'] 54 | expect(data.regex).toBe "(\\w+)" 55 | expect(data.test_string).toBe "Hello, World!" 56 | expect(data.match_type).toBe "search" 57 | expect(data.flags).toBe 6 58 | 59 | it 'should generate a Base64-encoded value of the regex data', -> 60 | expected = 'eyJyZWdleCI6IkhlbGxvLCAoXFx3KykhIiwiZmxhZ3MiOj' + 61 | 'YsIm1hdGNoX3R5cGUiOiJtYXRjaCIsInRlc3Rfc3RyaW5n' + 62 | 'IjoiSGVsbG8sIFdvcmxkISJ9' 63 | $builder.setFlags 6 64 | $builder.source = "Hello, (\\w+)!" 65 | $builder.testString = "Hello, World!" 66 | $builder.matchType = 'match' 67 | 68 | data = $builder.data() 69 | expect(data.flags).toBe 6 70 | 71 | expect(expected).toEqual $builder.encodedData() 72 | 73 | it 'should parse a Base64-encoded string and reconstruct regex data', -> 74 | data = 'eyJyZWdleCI6IkhlbGxvLCAoXFx3KykhIiwiZmxhZ3MiOj' + 75 | 'YsIm1hdGNoX3R5cGUiOiJtYXRjaCIsInRlc3Rfc3RyaW5n' + 76 | 'IjoiSGVsbG8sIFdvcmxkISJ9' 77 | 78 | $builder.fromData data 79 | 80 | data = $builder.data() 81 | expect(6).toBe data.flags 82 | expect("Hello, (\\w+)!").toBe data.regex 83 | expect("match").toBe data.match_type 84 | expect("Hello, World!").toBe data.test_string 85 | 86 | it 'should recreate the data from a previously encoded string', -> 87 | $builder.setFlags 6 88 | $builder.source = "Hello, (\\w+)!" 89 | $builder.testString = "Hello, World!" 90 | $builder.matchType = 'match' 91 | expected = $builder.data() 92 | encoded_data = $builder.encodedData() 93 | 94 | $builder.clean() 95 | 96 | expect(expected).not.toEqual $builder.data() 97 | 98 | $builder.fromData(encoded_data) 99 | expect(expected).toEqual $builder.data() 100 | 101 | -------------------------------------------------------------------------------- /tests/service_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import re 3 | import signal 4 | 5 | from pyregex.service import RegexService, InvalidRegexError, UnprocessibleRegexError 6 | 7 | 8 | class TimeoutException(Exception): 9 | pass 10 | 11 | 12 | class ServiceTests(unittest.TestCase): 13 | def test_initialize(self): 14 | svc = RegexService(r'\d+', 'match', int(re.I)) 15 | self.assertIsNotNone(svc) 16 | 17 | def test_invalidArgs(self): 18 | self.assertRaises(ValueError, lambda: RegexService(None, 'match', 2)) 19 | self.assertRaises(ValueError, lambda: RegexService(r'\d+', None, 2)) 20 | self.assertRaises( 21 | ValueError, lambda: RegexService(r'\d+', 'findall', -1)) 22 | self.assertRaises( 23 | ValueError, lambda: RegexService(r'\d+', 'invalid', 2)) 24 | self.assertRaises(InvalidRegexError, 25 | lambda: RegexService('\\d+[', 'match', 2)) 26 | 27 | def test_immutability(self): 28 | svc = RegexService(r'\d+', 'match', 0) 29 | with self.assertRaises(TypeError): 30 | svc.match_type = 'findall' 31 | 32 | def test_match(self): 33 | svc = RegexService(r'\d+', 'match', 0) 34 | result = svc.test("1984") 35 | self.assertIsNotNone(result) 36 | self.assertEqual(result['group'], "1984") 37 | 38 | result = svc.test("x1984") 39 | self.assertIsNone(result) 40 | 41 | def test_findall(self): 42 | svc = RegexService(r'\d+', 'findall', 0) 43 | result = svc.test("1984 2013 2020") 44 | self.assertIsNotNone(result) 45 | self.assertEqual(result, ['1984', '2013', '2020']) 46 | 47 | result = svc.test("testing") 48 | self.assertIsNone(result) 49 | 50 | def test_findall2(self): 51 | svc = RegexService(r'[\w\']+', 'findall', 0) 52 | result = svc.test('Hey, you - what are you doing here!?') 53 | self.assertIsNotNone(result) 54 | self.assertEqual( 55 | result, ['Hey', 'you', 'what', 'are', 'you', 'doing', 'here']) 56 | 57 | def test_search(self): 58 | svc = RegexService(r'\d+', 'search', 0) 59 | result = svc.test("1984") 60 | self.assertIsNotNone(result) 61 | self.assertEqual(result['group'], "1984") 62 | 63 | result = svc.test("x1984") 64 | self.assertIsNotNone(result) 65 | self.assertEqual(result['group'], "1984") 66 | 67 | def test_catastrophicBacktrace(self): 68 | input = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + \ 69 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 70 | 71 | def timeout_cb(signum, frame): 72 | raise TimeoutException("Timeout!") 73 | 74 | old_handler = signal.signal(signal.SIGALRM, timeout_cb) 75 | signal.alarm(5) 76 | 77 | try: 78 | svc = RegexService(r'(a?a)+b', 'match', 0) 79 | with self.assertRaises(UnprocessibleRegexError): 80 | svc.test(input) 81 | except TimeoutException as e: 82 | self.fail("Response took more than 5 seconds to execute") 83 | finally: 84 | signal.alarm(0) 85 | signal.signal(signal.SIGALRM, old_handler) 86 | -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http-socket = :$(PORT) 3 | master = true 4 | processes = 4 5 | die-on-term = true 6 | module = pyregex.api:app 7 | memory-report = true 8 | enable-threads = true 9 | 10 | touch-reload = . 11 | --------------------------------------------------------------------------------