├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── .pylintrc ├── LICENSE ├── MANIFEST.in ├── README.md ├── brythonserver ├── __init__.py ├── __version__.py ├── definitions.py ├── main.py ├── reverseproxied.py ├── static │ ├── brythonserver │ │ ├── __init__.py │ │ └── turtle.py │ ├── bs.js │ ├── bssite.css │ ├── drive_16dp.png │ ├── drive_24dp.png │ ├── drive_32dp.png │ ├── drive_48dp.png │ ├── drive_64dp.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ └── favicon.ico ├── templates │ ├── _rawcookies.html │ ├── _rawprivacy.html │ ├── _rawtermsofservice.html │ ├── console.html │ ├── cookiebanner.html │ ├── cookies.html │ ├── exec.html │ ├── index.html │ ├── layout.html │ ├── legalese.html │ ├── privacy.html │ └── termsofservice.html └── utility.py ├── docs ├── Design.md └── Functionality.md ├── requirements.txt ├── scripts ├── buildrelease.sh ├── run_js_tests.sh ├── run_tests.sh ├── testuploadrelease.sh └── uploadrelease.sh ├── setup.py └── wsgi.py /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build and test 2 | on: [push] 3 | jobs: 4 | test-build-and-linters: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: actions/setup-node@v3 9 | with: 10 | node-version: '14' 11 | - run: npm install standard --global 12 | - run: ./scripts/run_js_tests.sh 13 | - uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.11' 16 | architecture: 'x64' 17 | - run: python3 -m venv ./env 18 | - run: ./scripts/buildrelease.sh 19 | - run: ./scripts/run_tests.sh 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | pip-selfcheck.json 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # PyBuilder 55 | target/ 56 | 57 | # Redis 58 | dump.rdb 59 | 60 | # Cloud 9 61 | .c9/* 62 | 63 | # virtualenv 64 | bin/ 65 | include/ 66 | local/ 67 | share/ 68 | 69 | # brython 70 | brythonserver/static/brython 71 | brythonserver/static/brython/* 72 | 73 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Clear in-memory caches upon conclusion of linting. Useful if running pylint 9 | # in a server-like mode. 10 | clear-cache-post-run=no 11 | 12 | # Load and enable all available extensions. Use --list-extensions to see a list 13 | # all available extensions. 14 | #enable-all-extensions= 15 | 16 | # In error mode, messages with a category besides ERROR or FATAL are 17 | # suppressed, and no reports are done by default. Error mode is compatible with 18 | # disabling specific errors. 19 | #errors-only= 20 | 21 | # Always return a 0 (non-error) status code, even if lint errors are found. 22 | # This is primarily useful in continuous integration scripts. 23 | #exit-zero= 24 | 25 | # A comma-separated list of package or module names from where C extensions may 26 | # be loaded. Extensions are loading into the active Python interpreter and may 27 | # run arbitrary code. 28 | extension-pkg-allow-list= 29 | 30 | # A comma-separated list of package or module names from where C extensions may 31 | # be loaded. Extensions are loading into the active Python interpreter and may 32 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 33 | # for backward compatibility.) 34 | extension-pkg-whitelist= 35 | 36 | # Return non-zero exit code if any of these messages/categories are detected, 37 | # even if score is above --fail-under value. Syntax same as enable. Messages 38 | # specified are enabled, while categories only check already-enabled messages. 39 | fail-on= 40 | 41 | # Specify a score threshold under which the program will exit with error. 42 | fail-under=10 43 | 44 | # Interpret the stdin as a python script, whose filename needs to be passed as 45 | # the module_or_package argument. 46 | #from-stdin= 47 | 48 | # Files or directories to be skipped. They should be base names, not paths. 49 | ignore=CVS 50 | 51 | # Add files or directories matching the regular expressions patterns to the 52 | # ignore-list. The regex matches against paths and can be in Posix or Windows 53 | # format. Because '\\' represents the directory delimiter on Windows systems, 54 | # it can't be used as an escape character. 55 | ignore-paths= 56 | 57 | # Files or directories matching the regular expression patterns are skipped. 58 | # The regex matches against base names, not paths. The default value ignores 59 | # Emacs file locks 60 | ignore-patterns=^\.# 61 | 62 | # List of module names for which member attributes should not be checked 63 | # (useful for modules/projects where namespaces are manipulated during runtime 64 | # and thus existing member attributes cannot be deduced by static analysis). It 65 | # supports qualified module names, as well as Unix pattern matching. 66 | ignored-modules= 67 | 68 | # Python code to execute, usually for sys.path manipulation such as 69 | # pygtk.require(). 70 | #init-hook= 71 | 72 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 73 | # number of processors available to use, and will cap the count on Windows to 74 | # avoid hangs. 75 | jobs=1 76 | 77 | # Control the amount of potential inferred values when inferring a single 78 | # object. This can help the performance when dealing with large functions or 79 | # complex, nested conditions. 80 | limit-inference-results=100 81 | 82 | # List of plugins (as comma separated values of python module names) to load, 83 | # usually to register additional checkers. 84 | load-plugins= 85 | 86 | # Pickle collected data for later comparisons. 87 | persistent=yes 88 | 89 | # Minimum Python version to use for version dependent checks. Will default to 90 | # the version used to run pylint. 91 | py-version=3.11 92 | 93 | # Discover python modules and packages in the file system subtree. 94 | recursive=no 95 | 96 | # Add paths to the list of the source roots. Supports globbing patterns. The 97 | # source root is an absolute path or a path relative to the current working 98 | # directory used to determine a package namespace for modules located under the 99 | # source root. 100 | source-roots= 101 | 102 | # When enabled, pylint would attempt to guess common misconfiguration and emit 103 | # user-friendly hints instead of false-positive error messages. 104 | suggestion-mode=yes 105 | 106 | # Allow loading of arbitrary C extensions. Extensions are imported into the 107 | # active Python interpreter and may run arbitrary code. 108 | unsafe-load-any-extension=no 109 | 110 | # In verbose mode, extra non-checker-related info will be displayed. 111 | #verbose= 112 | 113 | 114 | [BASIC] 115 | 116 | # Naming style matching correct argument names. 117 | argument-naming-style=snake_case 118 | 119 | # Regular expression matching correct argument names. Overrides argument- 120 | # naming-style. If left empty, argument names will be checked with the set 121 | # naming style. 122 | #argument-rgx= 123 | 124 | # Naming style matching correct attribute names. 125 | attr-naming-style=snake_case 126 | 127 | # Regular expression matching correct attribute names. Overrides attr-naming- 128 | # style. If left empty, attribute names will be checked with the set naming 129 | # style. 130 | #attr-rgx= 131 | 132 | # Bad variable names which should always be refused, separated by a comma. 133 | bad-names=foo, 134 | bar, 135 | baz, 136 | toto, 137 | tutu, 138 | tata 139 | 140 | # Bad variable names regexes, separated by a comma. If names match any regex, 141 | # they will always be refused 142 | bad-names-rgxs= 143 | 144 | # Naming style matching correct class attribute names. 145 | class-attribute-naming-style=any 146 | 147 | # Regular expression matching correct class attribute names. Overrides class- 148 | # attribute-naming-style. If left empty, class attribute names will be checked 149 | # with the set naming style. 150 | #class-attribute-rgx= 151 | 152 | # Naming style matching correct class constant names. 153 | class-const-naming-style=UPPER_CASE 154 | 155 | # Regular expression matching correct class constant names. Overrides class- 156 | # const-naming-style. If left empty, class constant names will be checked with 157 | # the set naming style. 158 | #class-const-rgx= 159 | 160 | # Naming style matching correct class names. 161 | class-naming-style=PascalCase 162 | 163 | # Regular expression matching correct class names. Overrides class-naming- 164 | # style. If left empty, class names will be checked with the set naming style. 165 | #class-rgx= 166 | 167 | # Naming style matching correct constant names. 168 | const-naming-style=UPPER_CASE 169 | 170 | # Regular expression matching correct constant names. Overrides const-naming- 171 | # style. If left empty, constant names will be checked with the set naming 172 | # style. 173 | #const-rgx= 174 | 175 | # Minimum line length for functions/classes that require docstrings, shorter 176 | # ones are exempt. 177 | docstring-min-length=-1 178 | 179 | # Naming style matching correct function names. 180 | function-naming-style=snake_case 181 | 182 | # Regular expression matching correct function names. Overrides function- 183 | # naming-style. If left empty, function names will be checked with the set 184 | # naming style. 185 | #function-rgx= 186 | 187 | # Good variable names which should always be accepted, separated by a comma. 188 | good-names=i, 189 | j, 190 | k, 191 | ex, 192 | Run, 193 | _ 194 | 195 | # Good variable names regexes, separated by a comma. If names match any regex, 196 | # they will always be accepted 197 | good-names-rgxs= 198 | 199 | # Include a hint for the correct naming format with invalid-name. 200 | include-naming-hint=no 201 | 202 | # Naming style matching correct inline iteration names. 203 | inlinevar-naming-style=any 204 | 205 | # Regular expression matching correct inline iteration names. Overrides 206 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 207 | # with the set naming style. 208 | #inlinevar-rgx= 209 | 210 | # Naming style matching correct method names. 211 | method-naming-style=snake_case 212 | 213 | # Regular expression matching correct method names. Overrides method-naming- 214 | # style. If left empty, method names will be checked with the set naming style. 215 | #method-rgx= 216 | 217 | # Naming style matching correct module names. 218 | module-naming-style=snake_case 219 | 220 | # Regular expression matching correct module names. Overrides module-naming- 221 | # style. If left empty, module names will be checked with the set naming style. 222 | #module-rgx= 223 | 224 | # Colon-delimited sets of names that determine each other's naming style when 225 | # the name regexes allow several styles. 226 | name-group= 227 | 228 | # Regular expression which should only match function or class names that do 229 | # not require a docstring. 230 | no-docstring-rgx=^_ 231 | 232 | # List of decorators that produce properties, such as abc.abstractproperty. Add 233 | # to this list to register other decorators that produce valid properties. 234 | # These decorators are taken in consideration only for invalid-name. 235 | property-classes=abc.abstractproperty 236 | 237 | # Regular expression matching correct type alias names. If left empty, type 238 | # alias names will be checked with the set naming style. 239 | #typealias-rgx= 240 | 241 | # Regular expression matching correct type variable names. If left empty, type 242 | # variable names will be checked with the set naming style. 243 | #typevar-rgx= 244 | 245 | # Naming style matching correct variable names. 246 | variable-naming-style=snake_case 247 | 248 | # Regular expression matching correct variable names. Overrides variable- 249 | # naming-style. If left empty, variable names will be checked with the set 250 | # naming style. 251 | #variable-rgx= 252 | 253 | 254 | [CLASSES] 255 | 256 | # Warn about protected attribute access inside special methods 257 | check-protected-access-in-special-methods=no 258 | 259 | # List of method names used to declare (i.e. assign) instance attributes. 260 | defining-attr-methods=__init__, 261 | __new__, 262 | setUp, 263 | asyncSetUp, 264 | __post_init__ 265 | 266 | # List of member names, which should be excluded from the protected access 267 | # warning. 268 | exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit 269 | 270 | # List of valid names for the first argument in a class method. 271 | valid-classmethod-first-arg=cls 272 | 273 | # List of valid names for the first argument in a metaclass class method. 274 | valid-metaclass-classmethod-first-arg=mcs 275 | 276 | 277 | [DESIGN] 278 | 279 | # List of regular expressions of class ancestor names to ignore when counting 280 | # public methods (see R0903) 281 | exclude-too-few-public-methods= 282 | 283 | # List of qualified class names to ignore when counting class parents (see 284 | # R0901) 285 | ignored-parents= 286 | 287 | # Maximum number of arguments for function / method. 288 | max-args=5 289 | 290 | # Maximum number of attributes for a class (see R0902). 291 | max-attributes=7 292 | 293 | # Maximum number of boolean expressions in an if statement (see R0916). 294 | max-bool-expr=5 295 | 296 | # Maximum number of branch for function / method body. 297 | max-branches=12 298 | 299 | # Maximum number of locals for function / method body. 300 | max-locals=15 301 | 302 | # Maximum number of parents for a class (see R0901). 303 | max-parents=7 304 | 305 | # Maximum number of public methods for a class (see R0904). 306 | max-public-methods=20 307 | 308 | # Maximum number of return / yield for function / method body. 309 | max-returns=6 310 | 311 | # Maximum number of statements in function / method body. 312 | max-statements=50 313 | 314 | # Minimum number of public methods for a class (see R0903). 315 | min-public-methods=2 316 | 317 | 318 | [EXCEPTIONS] 319 | 320 | # Exceptions that will emit a warning when caught. 321 | overgeneral-exceptions=builtins.BaseException,builtins.Exception 322 | 323 | 324 | [FORMAT] 325 | 326 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 327 | expected-line-ending-format= 328 | 329 | # Regexp for a line that is allowed to be longer than the limit. 330 | ignore-long-lines=^\s*(# )??$ 331 | 332 | # Number of spaces of indent required inside a hanging or continued line. 333 | indent-after-paren=4 334 | 335 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 336 | # tab). 337 | indent-string=' ' 338 | 339 | # Maximum number of characters on a single line. 340 | max-line-length=100 341 | 342 | # Maximum number of lines in a module. 343 | max-module-lines=1000 344 | 345 | # Allow the body of a class to be on the same line as the declaration if body 346 | # contains single statement. 347 | single-line-class-stmt=no 348 | 349 | # Allow the body of an if to be on the same line as the test if there is no 350 | # else. 351 | single-line-if-stmt=no 352 | 353 | 354 | [IMPORTS] 355 | 356 | # List of modules that can be imported at any level, not just the top level 357 | # one. 358 | allow-any-import-level= 359 | 360 | # Allow explicit reexports by alias from a package __init__. 361 | allow-reexport-from-package=no 362 | 363 | # Allow wildcard imports from modules that define __all__. 364 | allow-wildcard-with-all=no 365 | 366 | # Deprecated modules which should not be used, separated by a comma. 367 | deprecated-modules= 368 | 369 | # Output a graph (.gv or any supported image format) of external dependencies 370 | # to the given file (report RP0402 must not be disabled). 371 | ext-import-graph= 372 | 373 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 374 | # external) dependencies to the given file (report RP0402 must not be 375 | # disabled). 376 | import-graph= 377 | 378 | # Output a graph (.gv or any supported image format) of internal dependencies 379 | # to the given file (report RP0402 must not be disabled). 380 | int-import-graph= 381 | 382 | # Force import order to recognize a module as part of the standard 383 | # compatibility libraries. 384 | known-standard-library= 385 | 386 | # Force import order to recognize a module as part of a third party library. 387 | known-third-party=enchant 388 | 389 | # Couples of modules and preferred modules, separated by a comma. 390 | preferred-modules= 391 | 392 | 393 | [LOGGING] 394 | 395 | # The type of string formatting that logging methods do. `old` means using % 396 | # formatting, `new` is for `{}` formatting. 397 | logging-format-style=old 398 | 399 | # Logging modules to check that the string format arguments are in logging 400 | # function parameter format. 401 | logging-modules=logging 402 | 403 | 404 | [MESSAGES CONTROL] 405 | 406 | # Only show warnings with the listed confidence levels. Leave empty to show 407 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 408 | # UNDEFINED. 409 | confidence=HIGH, 410 | CONTROL_FLOW, 411 | INFERENCE, 412 | INFERENCE_FAILURE, 413 | UNDEFINED 414 | 415 | # Disable the message, report, category or checker with the given id(s). You 416 | # can either give multiple identifiers separated by comma (,) or put this 417 | # option multiple times (only on the command line, not in the configuration 418 | # file where it should appear only once). You can also use "--disable=all" to 419 | # disable everything first and then re-enable specific checks. For example, if 420 | # you want to run only the similarities checker, you can use "--disable=all 421 | # --enable=similarities". If you want to run only the classes checker, but have 422 | # no Warning level messages displayed, use "--disable=all --enable=classes 423 | # --disable=W". 424 | disable=raw-checker-failed, 425 | bad-inline-option, 426 | locally-disabled, 427 | file-ignored, 428 | suppressed-message, 429 | useless-suppression, 430 | deprecated-pragma, 431 | use-symbolic-message-instead, 432 | use-implicit-booleaness-not-comparison-to-string, 433 | use-implicit-booleaness-not-comparison-to-zero, 434 | invalid-name, 435 | too-many-instance-attributes, 436 | duplicate-code, 437 | too-few-public-methods, 438 | unused-argument, 439 | too-many-arguments, 440 | broad-exception-caught, 441 | too-many-locals, 442 | too-many-statements 443 | 444 | 445 | # Enable the message, report, category or checker with the given id(s). You can 446 | # either give multiple identifier separated by comma (,) or put this option 447 | # multiple time (only on the command line, not in the configuration file where 448 | # it should appear only once). See also the "--disable" option for examples. 449 | enable= 450 | 451 | 452 | [METHOD_ARGS] 453 | 454 | # List of qualified names (i.e., library.method) which require a timeout 455 | # parameter e.g. 'requests.api.get,requests.api.post' 456 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 457 | 458 | 459 | [MISCELLANEOUS] 460 | 461 | # List of note tags to take in consideration, separated by a comma. 462 | notes=FIXME, 463 | XXX, 464 | TODO 465 | 466 | # Regular expression of note tags to take in consideration. 467 | notes-rgx= 468 | 469 | 470 | [REFACTORING] 471 | 472 | # Maximum number of nested blocks for function / method body 473 | max-nested-blocks=5 474 | 475 | # Complete name of functions that never returns. When checking for 476 | # inconsistent-return-statements if a never returning function is called then 477 | # it will be considered as an explicit return statement and no message will be 478 | # printed. 479 | never-returning-functions=sys.exit,argparse.parse_error 480 | 481 | 482 | [REPORTS] 483 | 484 | # Python expression which should return a score less than or equal to 10. You 485 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 486 | # 'convention', and 'info' which contain the number of messages in each 487 | # category, as well as 'statement' which is the total number of statements 488 | # analyzed. This score is used by the global evaluation report (RP0004). 489 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 490 | 491 | # Template used to display messages. This is a python new-style format string 492 | # used to format the message information. See doc for all details. 493 | msg-template= 494 | 495 | # Set the output format. Available formats are: text, parseable, colorized, 496 | # json2 (improved json format), json (old json format) and msvs (visual 497 | # studio). You can also give a reporter class, e.g. 498 | # mypackage.mymodule.MyReporterClass. 499 | #output-format= 500 | 501 | # Tells whether to display a full report or only the messages. 502 | reports=no 503 | 504 | # Activate the evaluation score. 505 | score=yes 506 | 507 | 508 | [SIMILARITIES] 509 | 510 | # Comments are removed from the similarity computation 511 | ignore-comments=yes 512 | 513 | # Docstrings are removed from the similarity computation 514 | ignore-docstrings=yes 515 | 516 | # Imports are removed from the similarity computation 517 | ignore-imports=yes 518 | 519 | # Signatures are removed from the similarity computation 520 | ignore-signatures=yes 521 | 522 | # Minimum lines number of a similarity. 523 | min-similarity-lines=4 524 | 525 | 526 | [SPELLING] 527 | 528 | # Limits count of emitted suggestions for spelling mistakes. 529 | max-spelling-suggestions=4 530 | 531 | # Spelling dictionary name. No available dictionaries : You need to install 532 | # both the python package and the system dependency for enchant to work. 533 | spelling-dict= 534 | 535 | # List of comma separated words that should be considered directives if they 536 | # appear at the beginning of a comment and should not be checked. 537 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 538 | 539 | # List of comma separated words that should not be checked. 540 | spelling-ignore-words= 541 | 542 | # A path to a file that contains the private dictionary; one word per line. 543 | spelling-private-dict-file= 544 | 545 | # Tells whether to store unknown words to the private dictionary (see the 546 | # --spelling-private-dict-file option) instead of raising a message. 547 | spelling-store-unknown-words=no 548 | 549 | 550 | [STRING] 551 | 552 | # This flag controls whether inconsistent-quotes generates a warning when the 553 | # character used as a quote delimiter is used inconsistently within a module. 554 | check-quote-consistency=no 555 | 556 | # This flag controls whether the implicit-str-concat should generate a warning 557 | # on implicit string concatenation in sequences defined over several lines. 558 | check-str-concat-over-line-jumps=no 559 | 560 | 561 | [TYPECHECK] 562 | 563 | # List of decorators that produce context managers, such as 564 | # contextlib.contextmanager. Add to this list to register other decorators that 565 | # produce valid context managers. 566 | contextmanager-decorators=contextlib.contextmanager 567 | 568 | # List of members which are set dynamically and missed by pylint inference 569 | # system, and so shouldn't trigger E1101 when accessed. Python regular 570 | # expressions are accepted. 571 | generated-members= 572 | 573 | # Tells whether to warn about missing members when the owner of the attribute 574 | # is inferred to be None. 575 | ignore-none=yes 576 | 577 | # This flag controls whether pylint should warn about no-member and similar 578 | # checks whenever an opaque object is returned when inferring. The inference 579 | # can return multiple potential results while evaluating a Python object, but 580 | # some branches might not be evaluated, which results in partial inference. In 581 | # that case, it might be useful to still emit no-member and other checks for 582 | # the rest of the inferred objects. 583 | ignore-on-opaque-inference=yes 584 | 585 | # List of symbolic message names to ignore for Mixin members. 586 | ignored-checks-for-mixins=no-member, 587 | not-async-context-manager, 588 | not-context-manager, 589 | attribute-defined-outside-init 590 | 591 | # List of class names for which member attributes should not be checked (useful 592 | # for classes with dynamically set attributes). This supports the use of 593 | # qualified names. 594 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 595 | 596 | # Show a hint with possible names when a member name was not found. The aspect 597 | # of finding the hint is based on edit distance. 598 | missing-member-hint=yes 599 | 600 | # The minimum edit distance a name should have in order to be considered a 601 | # similar match for a missing member name. 602 | missing-member-hint-distance=1 603 | 604 | # The total number of similar names that should be taken in consideration when 605 | # showing a hint for a missing member. 606 | missing-member-max-choices=1 607 | 608 | # Regex pattern to define which classes are considered mixins. 609 | mixin-class-rgx=.*[Mm]ixin 610 | 611 | # List of decorators that change the signature of a decorated function. 612 | signature-mutators= 613 | 614 | 615 | [VARIABLES] 616 | 617 | # List of additional names supposed to be defined in builtins. Remember that 618 | # you should avoid defining new builtins when possible. 619 | additional-builtins= 620 | 621 | # Tells whether unused global variables should be treated as a violation. 622 | allow-global-unused-variables=yes 623 | 624 | # List of names allowed to shadow builtins 625 | allowed-redefined-builtins= 626 | 627 | # List of strings which can identify a callback function by name. A callback 628 | # name must start or end with one of those strings. 629 | callbacks=cb_, 630 | _cb 631 | 632 | # A regular expression matching the name of dummy variables (i.e. expected to 633 | # not be used). 634 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 635 | 636 | # Argument names that match this expression will be ignored. 637 | ignored-argument-names=_.*|^ignored_|^unused_ 638 | 639 | # Tells whether we should check for unused import in __init__ files. 640 | init-import=no 641 | 642 | # List of qualified module names which can have objects that can redefine 643 | # builtins. 644 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 645 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Eric Dennison 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include brythonserver/* 2 | include brythonserver/static/* 3 | include brythonserver/templates/* 4 | include brythonserver/static/brython 5 | include brythonserver/static/brython/* 6 | include brythonserver/static/brythonserver/* 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![build passing](https://github.com/BrythonServer/brython-server/actions/workflows/build-and-test.yml/badge.svg?event=push) 2 | 3 | # Brython-Server 4 | 5 | **Brython-Server** is a Flask-based web application focused on providing a simple 6 | Python 3 development environment where source files are hosted on Github. 7 | 8 | You can try [Brython-Server](http://runpython.org) 9 | to get a feel for how it works. 10 | 11 | ## Brief Instructions 12 | 13 | When the page loads, you can begin writing Python 3 code right away. To 14 | execute your code, press the **run** button. 15 | 16 | ### Github Support 17 | To load Python 3 source code hosted on Github, you must should first log in to 18 | Github with the **login** button. Github will ask you to authorize Brython-Server 19 | in the next page. 20 | 21 | To load your source, paste the Github URL of your source file or repository 22 | into the text control at the top of the page. Press `` or the **refresh** 23 | button to retrieve the source from Github. 24 | 25 | You may make any changes you want to the source code and re-run it. If you would 26 | like to save your work back to Github, just press the **commit** button. 27 | 28 | ### Google Drive Support 29 | The Google Drive **load** and **save** buttons will activate Google Drive 30 | authentication and authorization dialog where you may confirm your desire to let 31 | Brython-Server access your Google Drive. This confirmation is required periodically. 32 | 33 | The Google Drive **load** button directs you to a standard Google Drive file picking 34 | screen. Only compatible text files are available to pick. Once you have selected a file, 35 | the URL for the file will be displayed in the upper left edit window. 36 | 37 | The Google Drive **save** button will upload any changes you have made to a file since 38 | you downloaded it, but only if you own or have edit priveleges on the file. If you didn't 39 | download a file first, the **save** button will prompt you for a new file name. 40 | In this case, Brython-Server will create a new file with your chosen name in the root 41 | of your Google Drive. 42 | 43 | If you previously **load**-ed or refreshed an existing file from Google Drive then the 44 | **save** button will simply udate your file with any changes you have made since then. 45 | 46 | Authorizing Google Drive will also add the Brython-Server app to your Google Drive. 47 | This will give you a custom **new** file type in Google Drive, and a custom option 48 | under the Google Drive **Open with** context menu. 49 | 50 | Note: files that were not created by Brython-Server may not be opened from the **load** 51 | button unless you previously opened them with the Google Drive **Open with** context 52 | menu. 53 | 54 | Note: working with any Github repository or Google Drive source files will require you 55 | to have an account with these services. If you use these services to access files 56 | that are not modifiable by you, you will be able to edit them locally in the 57 | Brython-Server page but will not be able to commit or save any changes back to their 58 | original source unless you have the priveleges to do so. 59 | 60 | ### Turtle 61 | 62 | Brython-Server supports the Python turtle to the extent that it is supported by 63 | the underlying Brython interpreter. Its usage is simple, but slightly non-standard. 64 | For example: 65 | 66 | ```python 67 | from brythonserver import turtle 68 | t = turtle.Turtle() 69 | t.forward(100) 70 | t.right(90) 71 | t.forward(100) 72 | turtle.done() 73 | ``` 74 | 75 | ### Ggame 76 | 77 | Brython-Server includes built-in support for the Ggame graphics engine. For example, 78 | a trivial program from the 79 | [Ggame documentation](https://ggame.readthedocs.io/en/latest/introduction.html): 80 | 81 | ```python 82 | from ggame import App, ImageAsset, Sprite 83 | 84 | # Create a displayed object at 100,100 using an image asset 85 | Sprite(ImageAsset("bunny.png"), (100, 100)) 86 | # Create the app, with a default stage 87 | APP = App() 88 | # Run the app 89 | APP.run() 90 | ``` 91 | 92 | ## Deployment 93 | 94 | The best way to install Brython-Server is with pip and virtualenv, using Python 3.11+. 95 | Create and activate your virtual environment then install Brython-Server with: 96 | 97 | 98 | ```python 99 | python3.11 -m pip install brython-server 100 | ``` 101 | 102 | ### Requirements 103 | 104 | The essential requirements for Brython-Server are met when you install with pip. 105 | In addition, for a production install you will need 106 | [gunicorn](http://docs.gunicorn.org/en/stable/install.html). 107 | 108 | Brython-Server will use [Brython](https://github.com/brython-dev/brython) as 109 | its Python interpreter and and [Ggame](https://github.com/BrythonServer/ggame) 110 | as its graphics engine. The correct versions of each will automatically be used 111 | when you install Brython-Server using pip. 112 | 113 | ### Environment Variables 114 | 115 | A full Brython-Server installation that is capable of interacting with Github 116 | should have several environment variables set for production use: 117 | 118 | Required for Github functionality: 119 | * githubtoken (an optional Github personal access token) 120 | * githubsecret (Github oauth secret) 121 | * githubclientid (Github oauth client id) 122 | 123 | Required for Google Drive functionality: 124 | * googleclientid (Google Client ID) 125 | * googleapikey (Google API Key. Brython Server requires the drive/files 126 | and filePicker APIs) 127 | * googleappid (Google Application ID) 128 | 129 | Required for creating a "personalized" Brython-Server instance: 130 | * sitetitle (A string that will be displayed as the "name of the site") 131 | * sitecontact (An e-mail address to use for contact) 132 | * siteurl (A full URL to the website) 133 | * flasksecret (A Flask application secret key) 134 | 135 | Note: to generate a unique, random Flask secret key, enter the following in 136 | a Python console: 137 | 138 | 139 | ```python 140 | >>> import os 141 | >>> os.urandom(24) 142 | ``` 143 | 144 | Use the string that results as the value of the flasksecret environment 145 | variable. 146 | 147 | ### Execution 148 | 149 | To run the server in stand-alone development mode (never in production!) 150 | execute (for example) from the Python 3 shell: 151 | 152 | ```python 153 | Python 3.11.7 (main, Dec 8 2023, 18:56:58) [GCC 11.4.0] on linux 154 | Type "help", "copyright", "credits" or "license" for more information. 155 | >>> from brythonserver.main import APP 156 | >>> APP.run(host="0.0.0.0", port=8080) 157 | * Serving Flask app 'brythonserver.main' (lazy loading) 158 | * Environment: production 159 | WARNING: This is a development server. Do not use it in a production deployment. 160 | Use a production WSGI server instead. 161 | * Debug mode: off 162 | * Running on all addresses (0.0.0.0) 163 | WARNING: This is a development server. Do not use it in a production deployment. 164 | * Running on http://127.0.0.1:8080 165 | * Running on http://192.168.111.50:8080 (Press CTRL+C to quit) 166 | ``` 167 | 168 | To run the server in a production environment, use gunicorn: 169 | 170 | ```bash 171 | $ gunicorn -b 0.0.0.0:3000 -w 4 brythonserver.main:APP 172 | ``` 173 | ## Development Environment 174 | 175 | To begin working with Brython Server in development environment: 176 | 177 | * Clone this repository and cd into it. 178 | * Create a virtual environment: `python3.11 -m venv env` 179 | * Activate the virtual environment: `source env/bin/activate` 180 | * Install the dependencies: `python3.11 -m pip install -r requirements.txt` 181 | 182 | ### Other Dependencies 183 | 184 | Your development environment will need black and standardjs to 185 | execute the `run_tests` and `run_js_tests` scripts (in the scripts folder). 186 | 187 | ### Execution 188 | 189 | Prior to executing the server in your development environment you will have to 190 | perform the following manual steps to populate the Brython distribution files 191 | where Brython Server can access them: 192 | 193 | ```bash 194 | cd ~/workspace/brython-server 195 | mkdir -p brythonserver/static/brython 196 | cd brythonserver/static/brython 197 | python3.11 -m brython --update 198 | 199 | ``` 200 | 201 | Now you should be able to run Brython Server in your development environment 202 | using a script similar to this: 203 | 204 | ```bash 205 | export githubclientid= 206 | export githubsecret= 207 | export githubtoken= 208 | export googleclientid='.apps.googleusercontent.com' 209 | export googleapikey='' 210 | export googleappid='' 211 | export sitetitle="" 212 | export sitecontact= 213 | export siteurl= 214 | export PORT= 215 | source brython-server/env/bin/activate 216 | cd brython-server 217 | python3.11 -m pip install -r requirements.txt 218 | pushd brythonserver/static/brython 219 | brython-cli install 220 | popd 221 | python3.11 wsgi.py 222 | ``` 223 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /brythonserver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrythonServer/brython-server/7fd1a73b14c1b4843f430a1d2263524083ce850a/brythonserver/__init__.py -------------------------------------------------------------------------------- /brythonserver/__version__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Brython-Server version definition. 3 | Author: E Dennison 4 | """ 5 | 6 | VERSION = "2.3.3" 7 | -------------------------------------------------------------------------------- /brythonserver/definitions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Brython-Server string constant definitions. 3 | Author: E Dennison 4 | """ 5 | 6 | from collections import namedtuple 7 | 8 | ENV_GITHUBCLIENTID = "githubclientid" 9 | ENV_GITHUBSECRET = "githubsecret" 10 | ENV_GOOGLECLIENTID = "googleclientid" 11 | ENV_GOOGLEAPIKEY = "googleapikey" 12 | ENV_GOOGLEAPPID = "googleappid" 13 | ENV_FLASKSECRET = "flasksecret" 14 | ENV_SITETITLE = "sitetitle" 15 | ENV_SITECONTACT = "sitecontact" 16 | ENV_SITEURL = "siteurl" 17 | ENV_DEBUG = "debug" 18 | 19 | SESSION_GITHUBSTATE = "githubstate" 20 | SESSION_ACCESSTOKEN = "accesstoken" 21 | SESSION_MAINFILE = "mainfile" 22 | SESSION_MAINSHA = "mainsha" 23 | SESSION_GITHUBCONTEXT = "githubcontext" 24 | SESSION_GITHUBREPO = "githubrepo" 25 | SESSION_GITHUBPATH = "githubpath" 26 | SESSION_METADATA = "metadata" 27 | 28 | Context = namedtuple("Context", ["user", "repo", "path"]) 29 | 30 | RUN_EDIT = "run_edit" 31 | AUTH_REQUEST = "auth_request" 32 | AUTH_FORGET = "auth_forget" 33 | GITHUB_COMMIT = "github_commit" 34 | IMPORTNAME = "brythonserver" 35 | 36 | URL_GITHUBAUTHORIZE = "https://github.com/login/oauth/authorize" 37 | URL_GITHUBRETRIEVETOKEN = "https://github.com/login/oauth/access_token" 38 | 39 | INIT_CONTENT = 'print("Hello, world.")' 40 | 41 | BRYTHON_FOLDER = "static/brython" 42 | BRYTHON_JS = "brython.js" 43 | -------------------------------------------------------------------------------- /brythonserver/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Brython-Server main module with Flask route points. 3 | Author: E Dennison 4 | """ 5 | import os 6 | import os.path 7 | import urllib.request 8 | import urllib.parse 9 | import json 10 | import base64 11 | import re 12 | from flask import ( 13 | Flask, 14 | render_template, 15 | session, 16 | request, 17 | redirect, 18 | url_for, 19 | abort, 20 | Response, 21 | ) 22 | import ggame.__version__ 23 | from ggame.__version__ import VERSION as GGVERSION, BUZZ_VERSION, PIXI_VERSION 24 | from .reverseproxied import ReverseProxied 25 | from .definitions import ( 26 | ENV_FLASKSECRET, 27 | ENV_DEBUG, 28 | ENV_GOOGLECLIENTID, 29 | ENV_GOOGLEAPIKEY, 30 | ENV_GOOGLEAPPID, 31 | BRYTHON_FOLDER, 32 | BRYTHON_JS, 33 | ENV_SITETITLE, 34 | ENV_SITECONTACT, 35 | ENV_SITEURL, 36 | INIT_CONTENT, 37 | RUN_EDIT, 38 | AUTH_REQUEST, 39 | AUTH_FORGET, 40 | IMPORTNAME, 41 | SESSION_GITHUBCONTEXT, 42 | SESSION_MAINSHA, 43 | SESSION_MAINFILE, 44 | SESSION_METADATA, 45 | Context, 46 | ) 47 | from .utility import ( 48 | githubloggedin, 49 | checkgithubstate, 50 | gistrequest, 51 | githubretrievetoken, 52 | githubretrievefile, 53 | githubauthurl, 54 | githubforgetauth, 55 | githubgetmainfile, 56 | githubretrievegist, 57 | githubpath, 58 | githubrequest, 59 | ) 60 | from .__version__ import VERSION 61 | 62 | APP = Flask(__name__, static_url_path="/__static") 63 | APP.wsgi_app = ReverseProxied(APP.wsgi_app) 64 | 65 | APP.secret_key = os.environ.get(ENV_FLASKSECRET, "A0Zr98j/3yX R~XHH!jmN]LWX/,?RT") 66 | APP.debug = os.environ.get(ENV_DEBUG, False) 67 | 68 | # Retrieve Brython Version 69 | with open( 70 | os.path.join( 71 | os.path.dirname(os.path.abspath(__file__)), BRYTHON_FOLDER, BRYTHON_JS 72 | ), 73 | encoding="utf-8", 74 | ) as bjs: 75 | BRYTHON_VERSION = ( 76 | re.search(r"// implementation \[([0-9, ]+)", bjs.read()) 77 | .group(1) 78 | .replace(", ", ".") 79 | ) 80 | 81 | # Locate the ggame directory 82 | GGAME_PATH = os.path.dirname(os.path.abspath(ggame.__version__.__file__)) 83 | 84 | SITETITLE = os.environ.get(ENV_SITETITLE, "Brython Server") 85 | SITECONTACT = os.environ.get(ENV_SITECONTACT, "noone@nowhere.net") 86 | SITEURL = os.environ.get(ENV_SITEURL, "https://runpython.org") 87 | G_CLIENTID = os.environ.get(ENV_GOOGLECLIENTID, "") 88 | G_APIKEY = os.environ.get(ENV_GOOGLEAPIKEY, "") 89 | G_APPID = os.environ.get(ENV_GOOGLEAPPID, "") 90 | 91 | 92 | @APP.route("/", methods=["POST", "GET"]) 93 | def root(): 94 | """Root server URL. 95 | 96 | This default path for the web site is used for a variety of things, 97 | via voth POST and GET methods. 98 | 99 | With GET, the URL may include an argument list, which is used to load 100 | a particularl Github repository/file. This is useful for sharing code 101 | via e-mail or hyperlink. This URL/method is also used as a return 102 | URL from Github when the user authorizes the application. In this 103 | case the argument list includes a STATE and access TOKEN. 104 | 105 | With POST, the user has requested a switch to EDIT mode (from exec.html), 106 | or to login at Github or forget login at Github. 107 | 108 | Returns one of the following: 109 | index.html -- render template 110 | exec.html -- render template 111 | redirect -- to / or github 112 | """ 113 | cookieconsent = request.cookies.get("cookie_consent") == "true" 114 | 115 | github_loggedin = githubloggedin() 116 | 117 | returnedhtml = None 118 | 119 | if request.method == "GET": 120 | if "user" in request.args or "gist" in request.args or "fileid" in request.args: 121 | # Executing an existing file 122 | user = request.args.get("user", "") 123 | repo = request.args.get("repo", "") 124 | branch = request.args.get("branch", "") 125 | name = request.args.get("name", request.args.get("gist", "")) 126 | path = request.args.get("path", "") 127 | fileid = request.args.get("fileid", "") 128 | returnedhtml = render_template( 129 | "exec.html", 130 | user=user, 131 | repo=repo, 132 | branch=branch, 133 | name=name, 134 | path=path, 135 | fileid=fileid, 136 | title=SITETITLE, 137 | contact=SITECONTACT, 138 | brythonversion=BRYTHON_VERSION, 139 | buzzversion=BUZZ_VERSION, 140 | pixiversion=PIXI_VERSION, 141 | bsversion=VERSION, 142 | ggversion=GGVERSION, 143 | cookieconsent=cookieconsent, 144 | g_clientid=G_CLIENTID, 145 | g_apikey=G_APIKEY, 146 | g_appid=G_APPID, 147 | ) 148 | elif "code" in request.args and "state" in request.args: 149 | # Github authorization response - check if valid 150 | if checkgithubstate(request.args.get("state")): 151 | githubretrievetoken(request.args.get("code")) 152 | returnedhtml = redirect(url_for("root")) 153 | elif "gui_edit" in request.args: 154 | # Drive UI integration: Edit 155 | returnedhtml = render_template( 156 | "index.html", 157 | edit=request.args.get("gui_edit", ""), 158 | new="", 159 | title=SITETITLE, 160 | contact=SITECONTACT, 161 | consolesite=SITETITLE + " Console", 162 | editcontent="", 163 | github=github_loggedin, 164 | brythonversion=BRYTHON_VERSION, 165 | buzzversion=BUZZ_VERSION, 166 | pixiversion=PIXI_VERSION, 167 | bsversion=VERSION, 168 | ggversion=GGVERSION, 169 | cookieconsent=cookieconsent, 170 | g_clientid=G_CLIENTID, 171 | g_apikey=G_APIKEY, 172 | g_appid=G_APPID, 173 | ) 174 | elif "gui_new" in request.args: 175 | # Drive UI integration: New 176 | returnedhtml = render_template( 177 | "index.html", 178 | edit="", 179 | new=request.args.get("gui_new", ""), 180 | title=SITETITLE, 181 | contact=SITECONTACT, 182 | consolesite=SITETITLE + " Console", 183 | editcontent="", 184 | github=github_loggedin, 185 | brythonversion=BRYTHON_VERSION, 186 | buzzversion=BUZZ_VERSION, 187 | pixiversion=PIXI_VERSION, 188 | bsversion=VERSION, 189 | ggversion=GGVERSION, 190 | cookieconsent=cookieconsent, 191 | g_clientid=G_CLIENTID, 192 | g_apikey=G_APIKEY, 193 | g_appid=G_APPID, 194 | ) 195 | else: 196 | # Nothing special going on 197 | returnedhtml = render_template( 198 | "index.html", 199 | github=github_loggedin, 200 | title=SITETITLE, 201 | contact=SITECONTACT, 202 | consolesite=SITETITLE + " Console", 203 | edit="", 204 | new="", 205 | editcontent=INIT_CONTENT, 206 | brythonversion=BRYTHON_VERSION, 207 | buzzversion=BUZZ_VERSION, 208 | pixiversion=PIXI_VERSION, 209 | bsversion=VERSION, 210 | ggversion=GGVERSION, 211 | cookieconsent=cookieconsent, 212 | g_clientid=G_CLIENTID, 213 | g_apikey=G_APIKEY, 214 | g_appid=G_APPID, 215 | ) 216 | elif request.method == "POST": 217 | if RUN_EDIT in request.form: 218 | # user is requesting to open a new page with editor 219 | returnedhtml = render_template( 220 | "index.html", 221 | edit=request.form[RUN_EDIT], 222 | new="", 223 | title=SITETITLE, 224 | contact=SITECONTACT, 225 | consolesite=SITETITLE + " Console", 226 | editcontent="", 227 | github=github_loggedin, 228 | brythonversion=BRYTHON_VERSION, 229 | buzzversion=BUZZ_VERSION, 230 | pixiversion=PIXI_VERSION, 231 | bsversion=VERSION, 232 | ggversion=GGVERSION, 233 | cookieconsent=cookieconsent, 234 | g_clientid=G_CLIENTID, 235 | g_apikey=G_APIKEY, 236 | g_appid=G_APPID, 237 | ) 238 | elif AUTH_REQUEST in request.form: 239 | # user is requesting authorization from github 240 | returnedhtml = redirect(githubauthurl()) 241 | elif AUTH_FORGET in request.form: 242 | # user is requesting to forget our authorization 243 | githubforgetauth() 244 | returnedhtml = redirect(url_for("root")) 245 | if returnedhtml: 246 | return returnedhtml 247 | abort(404) 248 | return "You should never see this!" 249 | 250 | 251 | @APP.route("/favicon.ico") 252 | def favicon(): 253 | """Return favicon.ico. 254 | 255 | Since web browsers are inclined to request the favicon.ico from the root 256 | of the web server, we should be able to provide it. Note that this will 257 | cause a problem if the Github python app has a resource called favicon.ico. 258 | """ 259 | return APP.send_static_file("favicon.ico") 260 | 261 | 262 | @APP.route("/brythonconsole") 263 | def brythonconsole(): 264 | """Return template for python/brython console.""" 265 | cookieconsent = request.cookies.get("cookie_consent") == "true" 266 | return render_template( 267 | "console.html", 268 | title=SITETITLE, 269 | contact=SITECONTACT, 270 | consolesite=SITETITLE + " Console", 271 | buzzversion=BUZZ_VERSION, 272 | pixiversion=PIXI_VERSION, 273 | brythonversion=BRYTHON_VERSION, 274 | bsversion=VERSION, 275 | cookieconsent=cookieconsent, 276 | g_clientid=G_CLIENTID, 277 | g_apikey=G_APIKEY, 278 | g_appid=G_APPID, 279 | ) 280 | 281 | 282 | @APP.route("/" + IMPORTNAME + "/") 283 | def brythonimport(filename): 284 | """Return static import file 285 | 286 | Add custom importable modules under the static/IMPORTNAME folder. 287 | """ 288 | return APP.send_static_file(os.path.join(IMPORTNAME, filename)) 289 | 290 | 291 | @APP.route("/legalnotices/") 292 | def legalnotices(filename): 293 | """Return legal notice html""" 294 | return render_template( 295 | filename + ".html", title=SITETITLE, contact=SITECONTACT, url=SITEURL 296 | ) 297 | 298 | 299 | @APP.route("/ggame/") 300 | def ggameimport(filename): 301 | """Return content from the ggame file tree.""" 302 | try: 303 | with open(os.path.join(GGAME_PATH, filename), "rb") as thefile: 304 | content = thefile.read() 305 | if isinstance(content, bytes): 306 | return Response(content, mimetype="application/octet-stream") 307 | return Response(content) 308 | except FileNotFoundError as err: 309 | print(err) 310 | abort(404) 311 | return "You should never see this!" 312 | 313 | 314 | @APP.route("/ggame.py") 315 | def ggame_py(): 316 | """Return a 404 on any attempt to load ggame.py. This is will 317 | avoid 'wasting time' before searching for modules in the 318 | ggame package.""" 319 | # ggame is an available package, no ggame.py will be possible 320 | abort(404) 321 | 322 | 323 | @APP.route("/") 324 | def file(filename): 325 | """Return (possibly cached) file for the current Github repo. 326 | Will look for png match in the local ggame installation as well! 327 | """ 328 | filename = urllib.request.pathname2url(filename) 329 | try: 330 | cx = Context(*session[SESSION_GITHUBCONTEXT]) 331 | content, _sha = githubretrievefile(cx.user, cx.repo, cx.path + "/" + filename) 332 | except (FileNotFoundError, KeyError, urllib.error.HTTPError) as err: 333 | try: 334 | if not filename.endswith((".png", ".PNG")): 335 | raise FileNotFoundError from err 336 | return ggameimport(filename) 337 | except FileNotFoundError as err1: 338 | print(err1) 339 | abort(404) 340 | if isinstance(content, bytes): 341 | return Response(content, mimetype="application/octet-stream") 342 | return Response(content) 343 | 344 | 345 | ## API routes 346 | 347 | 348 | @APP.route("/api/v1/commit", methods=["PUT"]) 349 | def v1_commit(): 350 | """Commit changes in editor to the current main file on Github. 351 | 352 | JSON arguments: 353 | user -- Github user name 354 | repo -- Github user's repo name 355 | path -- path (fragment) to a specific file 356 | name -- specific file name 357 | editcontent -- contents of editor on web page 358 | commitmsg -- commit message 359 | 360 | JSON return: 361 | success -- True/False 362 | """ 363 | content = request.json 364 | user = content.get("user") 365 | repo = content.get("repo") 366 | path = content.get("path", "") 367 | name = content.get("name", "") 368 | editcontent = content.get("editcontent", "") 369 | msg = content.get("commitmsg", "") 370 | if path and not path.endswith(name): 371 | path += "/" + name 372 | else: 373 | path += name 374 | try: 375 | metadata = session.get(SESSION_METADATA, "") # previously loaded a gist? 376 | if metadata == "": # default - ordinary repository 377 | gitrequest, token = githubrequest(user, repo, path, "PUT") 378 | parameters = { 379 | "message": msg, 380 | "content": base64.b64encode(editcontent.encode("utf-8")).decode( 381 | "utf-8" 382 | ), 383 | "sha": session[SESSION_MAINSHA], 384 | } 385 | else: # this is a gist file name 386 | gitrequest, token = gistrequest(name, "PATCH") 387 | parameters = {"files": {metadata: {"content": editcontent}}} 388 | # pylint: disable=bare-except 389 | except: 390 | print("Session expired.") 391 | return ( 392 | json.dumps( 393 | {"success": False, "message": "Session expired - reload to continue"} 394 | ), 395 | 440, 396 | {"ContentType": "application/json"}, 397 | ) 398 | data = json.dumps(parameters).encode("utf-8") 399 | gitrequest.add_header("Content-Type", "application/json; charset=utf-8") 400 | gitrequest.add_header("Accept", "application/json") 401 | gitrequest.add_header("Content-Length", len(data)) 402 | try: 403 | with urllib.request.urlopen(gitrequest, data) as response: 404 | jsresponse = json.loads(response.read().decode("utf-8")) 405 | session[SESSION_MAINSHA] = jsresponse.get("content", {}).get("sha", "") 406 | return (json.dumps({"success": True}), 200, {"ContentType": "application/json"}) 407 | except urllib.error.HTTPError as err: 408 | error = err.msg + " " + str(err.code) 409 | print( 410 | "Github commit error: " + error + ", token was ", 411 | token, 412 | ", path was ", 413 | user, 414 | repo, 415 | path, 416 | ) 417 | return ( 418 | json.dumps({"success": False, "message": error}), 419 | 404, 420 | {"ContentType": "application/json"}, 421 | ) 422 | 423 | 424 | @APP.route("/api/v1/load", methods=["PUT"]) 425 | def v1_load(): 426 | """Load source code and resources from Github 427 | 428 | JSON arguments: 429 | user -- Github user name (blank for gist) 430 | repo -- Github user's repo name (blank for gist) 431 | branch -- optional branch identifier 432 | path -- optional path (fragment) to a specific file 433 | name -- optional specific file name or gist ID 434 | 435 | JSON return: 436 | success -- True/False 437 | name -- name of main file to execute 438 | path -- path to main file 439 | content -- content of main file 440 | """ 441 | content = request.json 442 | user = content.get("user", "") 443 | repo = content.get("repo", "") 444 | branch = content.get("branch", "") 445 | path = content.get("path", "") 446 | name = content.get("name", "") 447 | mainfile = name 448 | mainsha = "" 449 | try: 450 | if mainfile == "": 451 | mainfile = githubgetmainfile(user, repo, path) 452 | else: 453 | if user != "" and repo != "": # user, repo, path and mainfile 454 | maincontent, mainsha = githubretrievefile( 455 | user, repo, path + "/" + mainfile 456 | ) 457 | elif user == "" or repo == "": # missing user or repo -> must be gist? 458 | maincontent, mainsha = githubretrievegist(mainfile) 459 | else: 460 | raise FileNotFoundError 461 | # All files read, save primary name and sha 462 | session[SESSION_MAINFILE] = mainfile 463 | session[SESSION_MAINSHA] = mainsha 464 | session[SESSION_GITHUBCONTEXT] = Context(user, repo, path) 465 | # All files read, return 466 | return ( 467 | json.dumps( 468 | { 469 | "success": True, 470 | "name": mainfile, 471 | "path": githubpath(user, repo, branch, path, mainfile), 472 | "content": maincontent, 473 | } 474 | ), 475 | 200, 476 | {"ContentType": "application/json"}, 477 | ) 478 | except (urllib.error.HTTPError, FileNotFoundError) as err: 479 | print("Github error: " + err.msg + ", path was ", user, repo, path) 480 | if err.msg == "Unauthorized": 481 | return ( 482 | json.dumps( 483 | { 484 | "success": False, 485 | "message": "Unauthorized: please log in to Github.", 486 | } 487 | ), 488 | 401, 489 | {"ContentType": "application/json"}, 490 | ) 491 | return ( 492 | json.dumps({"success": False, "message": err.msg}), 493 | 404, 494 | {"ContentType": "application/json"}, 495 | ) 496 | return ( 497 | json.dumps({"success": False, "message": "You should not see this error."}), 498 | 404, 499 | {"ContentType": "application/json"}, 500 | ) 501 | -------------------------------------------------------------------------------- /brythonserver/reverseproxied.py: -------------------------------------------------------------------------------- 1 | """ 2 | From http://flask.pocoo.org/snippets/35/ 3 | """ 4 | 5 | 6 | class ReverseProxied: 7 | """Wrap the application in this middleware and configure the 8 | front-end server to add these headers, to let you quietly bind 9 | this to a URL other than / and to an HTTP scheme that is 10 | different than what is used locally. 11 | 12 | In nginx: 13 | location /myprefix { 14 | proxy_pass http://192.168.0.1:5001; 15 | proxy_set_header Host $host; 16 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 17 | proxy_set_header X-Scheme $scheme; 18 | proxy_set_header X-Script-Name /myprefix; 19 | } 20 | 21 | :param app: the WSGI application 22 | """ 23 | 24 | def __init__(self, app): 25 | self.app = app 26 | 27 | def __call__(self, environ, start_response): 28 | script_name = environ.get("HTTP_X_SCRIPT_NAME", "") 29 | if script_name: 30 | environ["SCRIPT_NAME"] = script_name 31 | path_info = environ["PATH_INFO"] 32 | if path_info.startswith(script_name): 33 | environ["PATH_INFO"] = path_info[len(script_name) :] 34 | 35 | scheme = environ.get("HTTP_X_SCHEME", "") 36 | if scheme: 37 | environ["wsgi.url_scheme"] = scheme 38 | return self.app(environ, start_response) 39 | -------------------------------------------------------------------------------- /brythonserver/static/brythonserver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrythonServer/brython-server/7fd1a73b14c1b4843f430a1d2263524083ce850a/brythonserver/static/brythonserver/__init__.py -------------------------------------------------------------------------------- /brythonserver/static/brythonserver/turtle.py: -------------------------------------------------------------------------------- 1 | """ 2 | Brython-Server turtle import wrapper 3 | Author: E Dennison 4 | """ 5 | from browser import document 6 | from turtle import * 7 | from turtle import set_defaults, FormattedTuple 8 | 9 | set_defaults(turtle_canvas_wrapper=document["graphics-column"]) 10 | 11 | # Redefine done 12 | _done = done 13 | 14 | 15 | def done(): 16 | _done() 17 | Screen().reset() 18 | Turtle._pen = None 19 | 20 | 21 | # Redefine FormattedTuple to support abs() 22 | _FormattedTuple = FormattedTuple 23 | 24 | 25 | class FormattedTuple(_FormattedTuple): 26 | def __abs__(self): 27 | return (self[0] ** 2 + self[1] ** 2) ** 0.5 28 | -------------------------------------------------------------------------------- /brythonserver/static/bs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Brython-server default javascript 3 | * Author: E Dennison 4 | */ 5 | 6 | /* eslint-env jquery */ 7 | /* global ace */ 8 | /* global brython */ 9 | /* global gapi */ 10 | /* global google */ 11 | /* global XMLHttpRequest */ 12 | /* global Blob */ 13 | /* global alert */ 14 | /* global __EXECUTE__BRYTHON__ */ 15 | /* eslint-disable no-unused-vars, no-var */ 16 | 17 | /* 18 | * bsConsole 19 | * 20 | * Console Proxy routes alert and prompt calls to our own handler. 21 | */ 22 | var bsConsole = (function () { 23 | // var consolequeue = []; 24 | let consolecontext = '' 25 | const CONSOLEID = '#console' 26 | const OLDPROMPT = window.prompt 27 | const CONSOLECONTEXTSIZE = 24 // size of a typical old school CRT (VT100) 28 | 29 | // handle console printing 30 | function PrintConsole (text) { 31 | const textarea = $(CONSOLEID) 32 | textarea.val(textarea.val() + text) 33 | textarea.scrollTop(textarea[0].scrollHeight) 34 | consolecontext += text 35 | } 36 | 37 | function Initialize () { 38 | // take over the prompt dialog so we can display prompt text in the console 39 | window.prompt = function (text, defvalue) { 40 | PrintConsole(text) // put prompt 41 | const truncatedcontext = consolecontext.split('\n').slice(-CONSOLECONTEXTSIZE).join('\n') 42 | const returnedValue = OLDPROMPT(truncatedcontext, defvalue) 43 | consolecontext = truncatedcontext 44 | PrintConsole(returnedValue + '\n') 45 | return returnedValue 46 | } 47 | // now seize the output 48 | takeOverConsole() 49 | } 50 | 51 | // Console hijacker - http://tobyho.com/2012/07/27/taking-over-console-log/ 52 | // target is ID of alternate destination 53 | function takeOverConsole () { 54 | const console = window.console 55 | if (!console) return 56 | 57 | function intercept (method) { 58 | const original = console[method] 59 | console[method] = function () { 60 | for (let i = 0; i < arguments.length; i++) { 61 | if (typeof arguments[i] === 'string' && 62 | arguments[i].indexOf('Error 404 means that Python module') === -1 && 63 | arguments[i].indexOf('using indexedDB for stdlib modules cache') === -1) { 64 | PrintConsole(arguments[i]) 65 | } 66 | } 67 | if (original.apply) { 68 | // Do this for normal browsers 69 | original.apply(console, arguments) 70 | } else { 71 | // Do this for IE 72 | const message = Array.prototype.slice.apply(arguments).join(' ') 73 | original(message) 74 | } 75 | } 76 | } 77 | const methods = ['log', 'warn', 'error'] 78 | for (let i = 0; i < methods.length; i++) { intercept(methods[i]) } 79 | } 80 | 81 | // clear the console output 82 | function clearConsole () { 83 | $(CONSOLEID).val('') 84 | consolecontext = '' 85 | } 86 | 87 | // public API 88 | return { 89 | init: Initialize, 90 | clear: clearConsole 91 | } 92 | }()) 93 | 94 | /* END bsConsole */ 95 | 96 | /* 97 | * bsUI 98 | * 99 | * User Interface Functionality 100 | */ 101 | var bsUI = (function () { 102 | const GOOGLE_AUTHORIZE = '#googleloginbutton' 103 | const GOOGLE_LOAD = '#googleloadbutton' 104 | const GOOGLE_LOGOUT = '#googlelogoutbutton' 105 | const GOOGLE_SAVE = '#googlesavebutton' 106 | const FILE_NAME = '#source_filename' 107 | const GITHUB_URL = '#github_url' 108 | const SHARE_URL = '#share_url' 109 | const URL_SUBMIT = '#url_submit' 110 | const URL_INPUT = '#url_input' 111 | const RUN_EDIT = '#run_edit' 112 | const RUN_EDIT_FORM = '#run_edit_form' 113 | const GRAPHICS_COL_NAME = '#graphics-column' // graphics-column 114 | const CANVAS_NAME = '#ggame-canvas' // ggame-canvas 115 | const TURTLE_CANVAS_NAME = '#turtle-canvas' 116 | const TEXT_COLUMNS = ['col-md-8', 'col-md-4', 'col-md-0'] 117 | const CONSOLE_COLUMNS = ['col-md-0', 'col-md-12', 'col-md-0'] 118 | const GRAPHICS_COLUMNS = ['col-md-0', 'col-md-4', 'col-md-8'] 119 | 120 | let editor = null 121 | let ingraphics = false 122 | let hidedepth = 0 123 | 124 | // hide buttons and show the working indicator 125 | function showWorking () { 126 | hidedepth += 1 127 | $('#loading').show() 128 | $('#navigation').hide() 129 | } 130 | 131 | // show buttons and hide working indicator 132 | function hideWorking () { 133 | if (hidedepth > 0) { 134 | hidedepth -= 1 135 | } 136 | if (hidedepth === 0) { 137 | $('#loading').hide() 138 | $('#navigation').show() 139 | } 140 | } 141 | 142 | function Initialize () { 143 | // Github link is not visible by default 144 | $(GITHUB_URL).hide() 145 | // Share link is not visible by default 146 | const shareLink = $(SHARE_URL) 147 | if (shareLink) { 148 | shareLink.hide() 149 | } 150 | // Capture key on GITHUB_URL and direct to URL_SUBMIT 151 | $(URL_INPUT).keyup(function (event) { 152 | if (event.keyCode === 13) { 153 | event.preventDefault() 154 | $(URL_SUBMIT).click() 155 | } 156 | }) 157 | // showWorking() 158 | } 159 | 160 | function setConsoleMode () { 161 | $('#editor-column').attr('class', CONSOLE_COLUMNS[0]) 162 | $('#output-column').attr('class', CONSOLE_COLUMNS[1]) 163 | $(GRAPHICS_COL_NAME).attr('class', CONSOLE_COLUMNS[2]) 164 | $(GRAPHICS_COL_NAME).hide() 165 | $('#editor-column').hide() 166 | $('#haltbutton').prop('disabled', true) 167 | if (ingraphics && (typeof window.ggame_quit === 'function')) { 168 | window.ggame_quit() 169 | } 170 | } 171 | 172 | function setEditMode () { 173 | $('#editor-column').attr('class', TEXT_COLUMNS[0]) 174 | $('#output-column').attr('class', TEXT_COLUMNS[1]) 175 | $(GRAPHICS_COL_NAME).attr('class', TEXT_COLUMNS[2]) 176 | $(GRAPHICS_COL_NAME).attr('hidden', true) 177 | $(TURTLE_CANVAS_NAME).empty() 178 | $('#editor-column').show() 179 | $('#haltbutton').prop('disabled', true) 180 | $('#gobutton').prop('disabled', false) 181 | if (ingraphics && (typeof window.ggame_quit === 'function')) { 182 | window.ggame_quit() 183 | } 184 | ingraphics = false 185 | } 186 | 187 | function setTurtleMode () { 188 | $('#editor-column').attr('class', GRAPHICS_COLUMNS[0]) 189 | $('#output-column').attr('class', GRAPHICS_COLUMNS[1]) 190 | $(GRAPHICS_COL_NAME).attr('class', GRAPHICS_COLUMNS[2]) 191 | $(GRAPHICS_COL_NAME).attr('hidden', false) 192 | $(CANVAS_NAME).hide() 193 | $(TURTLE_CANVAS_NAME).remove() 194 | $('#haltbutton').prop('disabled', false) 195 | $('#gobutton').prop('disabled', true) 196 | $('#editor-column').hide() 197 | } 198 | 199 | function setGraphicsMode () { 200 | $('#editor-column').attr('class', GRAPHICS_COLUMNS[0]) 201 | $('#output-column').attr('class', GRAPHICS_COLUMNS[1]) 202 | $(GRAPHICS_COL_NAME).attr('class', GRAPHICS_COLUMNS[2]) 203 | $(CANVAS_NAME).show() 204 | $(TURTLE_CANVAS_NAME).remove() 205 | $(CANVAS_NAME).height($(GRAPHICS_COL_NAME).clientHeight) 206 | $(CANVAS_NAME).width($(GRAPHICS_COL_NAME).clientWidth) 207 | $('#haltbutton').prop('disabled', false) 208 | $('#gobutton').prop('disabled', true) 209 | $('#editor-column').hide() 210 | $(GRAPHICS_COL_NAME).attr('hidden', false) 211 | ingraphics = true 212 | } 213 | 214 | // Show github or google link 215 | function showLink (path) { 216 | const element = $(GITHUB_URL) 217 | element.attr('href', path) 218 | element.show() 219 | } 220 | 221 | // Show share link 222 | function showShareURL (data) { 223 | const element = $(SHARE_URL) 224 | if (data.user === '' && data.repo === '') { 225 | element.attr('href', '?gist=' + data.name) 226 | } else { 227 | const baseargs = '?user=' + data.user + '&repo=' + data.repo + '&branch=' + data.branch + '&name=' + data.name 228 | if (data.path === '') { 229 | element.attr('href', baseargs) 230 | } else { 231 | element.attr('href', baseargs + '&path=' + data.path) 232 | } 233 | } 234 | element.show() 235 | } 236 | 237 | // show GOOGLE share link 238 | function showGoogleShareURL (fileId) { 239 | const element = $(SHARE_URL) 240 | if (element) { 241 | element.attr('href', '?fileid=' + fileId) 242 | element.show() 243 | } 244 | } 245 | 246 | // Execute the EDIT button 247 | function runEdit () { 248 | $(RUN_EDIT).val($(GITHUB_URL).attr('href')) 249 | $(RUN_EDIT_FORM).submit() 250 | } 251 | 252 | // Create editor 253 | function startEditor () { 254 | editor = ace.edit('editorace') 255 | // editor.setTheme("ace/theme/eclipse"); 256 | editor.setShowPrintMargin(true) 257 | editor.setDisplayIndentGuides(true) 258 | editor.getSession().setMode('ace/mode/python') 259 | editor.$blockScrolling = Infinity 260 | const textarea = $('textarea[name="editorcache"]').hide() 261 | if (textarea.val().length !== 0) { 262 | editor.getSession().setValue(textarea.val()) 263 | } 264 | editor.getSession().on('change', function () { 265 | textarea.val(editor.getSession().getValue()) 266 | }) 267 | } 268 | 269 | // Get editor content 270 | function getEditor () { 271 | if (editor) { 272 | return editor.getValue() 273 | } 274 | } 275 | 276 | // Set editor content 277 | function setEditor (text) { 278 | if (editor) { 279 | editor.setValue(text) 280 | } 281 | } 282 | 283 | // Clear editor selection 284 | function clearEditorSelection () { 285 | if (editor) { 286 | editor.selection.clearSelection() 287 | } 288 | } 289 | 290 | // Show controls appropriate when signed in to Google 291 | function showGoogle () { 292 | $(GOOGLE_LOGOUT).show() 293 | $(GOOGLE_AUTHORIZE).hide() 294 | $(GOOGLE_LOAD).show() 295 | $(GOOGLE_SAVE).show() 296 | } 297 | 298 | // Hide controls appropriate when not signed in to Google 299 | function hideGoogle () { 300 | $(GOOGLE_AUTHORIZE).show() 301 | $(GOOGLE_LOAD).hide() 302 | $(GOOGLE_LOGOUT).hide() 303 | $(GOOGLE_SAVE).hide() 304 | } 305 | 306 | // Public API 307 | return { 308 | URL_INPUT, 309 | FILE_NAME, 310 | init: Initialize, 311 | showlink: showLink, 312 | showshareurl: showShareURL, 313 | showgoogleshareurl: showGoogleShareURL, 314 | runedit: runEdit, 315 | starteditor: startEditor, 316 | geteditor: getEditor, 317 | seteditor: setEditor, 318 | clearselect: clearEditorSelection, 319 | editmode: setEditMode, 320 | turtlemode: setTurtleMode, 321 | graphicsmode: setGraphicsMode, 322 | consolemode: setConsoleMode, 323 | executemode: setTurtleMode, // setExecMode 324 | showgoogle: showGoogle, 325 | hidegoogle: hideGoogle, 326 | showworking: showWorking, 327 | hideworking: hideWorking 328 | } 329 | }()) 330 | /* END bsUI */ 331 | 332 | /* 333 | * bsGithubUtil 334 | * 335 | * Github Utilities 336 | */ 337 | var bsGithubUtil = (function () { 338 | // create a Github file path 339 | function createGithubPath (data) { 340 | let path = data.path 341 | if (path.length > 0 && path.substr(path.length - 1) !== '/') { 342 | path += '/' 343 | } 344 | path += data.name 345 | return path 346 | } 347 | 348 | // create a Github URL text for specific file 349 | function createGithubURL (data) { 350 | let url = 'not found...' 351 | if (data.user !== '' && data.repo !== '') { 352 | url = 'https://github.com/' + data.user + '/' + data.repo 353 | url += '/blob/' + data.branch + '/' + createGithubPath(data) 354 | } else if (data.name !== '') { 355 | url = 'https://gist.github.com/' + data.name 356 | } 357 | return url 358 | } 359 | 360 | // parse Github URL text 361 | function parseGithubURL (urlInput) { 362 | let data = { 363 | user: '', 364 | repo: '', 365 | branch: '', 366 | path: '', 367 | name: '' 368 | } 369 | if (urlInput == null) { 370 | return data 371 | } 372 | // attempt a single file match 373 | // example: https://github.com/tiggerntatie/brython-student-test/blob/master/hello.py 374 | // example: https://github.com/tiggerntatie/hhs-cp-site/blob/master/hhscp/static/exemplars/c02hello.py 375 | // example subtree: https://github.com/tiggerntatie/hhs-cp-site/tree/master/hhscp/static/exemplars 376 | const gittreematch = urlInput.match(/.*github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/) 377 | const gitfilematch = urlInput.match(/.*github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/([^/]+)/) 378 | const gittreefilematch = urlInput.match(/.*github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)\/([^/]+)/) 379 | const gitrepomatch = urlInput.match(/.*github\.com\/([^/]+)\/([^/]+).*/) 380 | const gisturlmatch = urlInput.match(/.*gist\.github\.com(\/[^/]+)?\/([0-9,a-f]+)/) 381 | const gistmatch = urlInput.match(/^[0-9,a-f]+$/) 382 | if (gisturlmatch || gistmatch) { 383 | const gist = gisturlmatch ? gisturlmatch[2] : gistmatch[0] 384 | data = { 385 | user: '', 386 | repo: '', 387 | branch: '', 388 | path: '', 389 | name: gist 390 | } 391 | } else if (gitrepomatch) { 392 | data = { 393 | user: gitrepomatch[1], 394 | repo: gitrepomatch[2], 395 | branch: '', 396 | path: '', 397 | name: '' 398 | } 399 | if (gittreematch) { 400 | data.branch = gittreematch[3] 401 | data.path = gittreematch[4] 402 | } 403 | if (gittreefilematch) { 404 | data.path = gittreefilematch[4] 405 | data.name = gittreefilematch[5] 406 | } else if (gitfilematch) { 407 | data.branch = gitfilematch[3] 408 | data.name = gitfilematch[4] 409 | } 410 | } 411 | return data 412 | } 413 | 414 | // parse the urlInput and return structure with user, repo, path, name 415 | function parseGithub (UI) { 416 | return parseGithubURL($(UI.URL_INPUT).val()) 417 | } 418 | 419 | return { 420 | parse: parseGithub, 421 | parseurl: parseGithubURL, 422 | createurl: createGithubURL 423 | } 424 | }()) 425 | /* END bsGithubUtil */ 426 | 427 | /* 428 | * bsGoogleUtil 429 | * 430 | * Google Utilities 431 | */ 432 | const bsGoogleUtil = (function () { 433 | // create a Google URL text for specific file 434 | function createGoogleURL (id) { 435 | return 'https://drive.google.com/file/d/' + id + '/view' 436 | } 437 | 438 | // parse Google URL text 439 | // e.g. https://drive.google.com/a/hanovernorwichschools.org/file/d/1OGC1fguuXR_-PKraS9vYbYVunVYjKNFY/view?usp=drive_web 440 | function parseGoogleURL (urlInput) { 441 | if (urlInput == null) { 442 | return null 443 | } 444 | // rule out an obvious github url 445 | if (urlInput.match(/github/)) { 446 | return false 447 | } else { 448 | return urlInput.match(/[-_\w]{25,}/) 449 | } 450 | } 451 | 452 | // parse the urlInput and return id 453 | function parseGoogle (UI) { 454 | return parseGoogleURL($(UI.URL_INPUT).val()) 455 | } 456 | 457 | return { 458 | parse: parseGoogle, 459 | parseurl: parseGoogleURL, 460 | createurl: createGoogleURL 461 | } 462 | }()) 463 | /* END bsGoogleUtil */ 464 | 465 | /* 466 | * bsController 467 | * 468 | * Miscellaneous actions and network transactions 469 | */ 470 | var bsController = (function () { 471 | let maincontent = '' 472 | let mainscript = null 473 | const __MAIN__ = '__main__' 474 | let initialized = false 475 | let gdriveFileidLoaded = null 476 | 477 | // Initialize the brython interpreter 478 | function initBrython () { 479 | if (!initialized) { 480 | brython(1) 481 | initialized = true 482 | } 483 | } 484 | 485 | // Execute the brython interpreter 486 | function runBrython (console) { 487 | console.clear() 488 | __EXECUTE__BRYTHON__() 489 | } 490 | 491 | function removeMainScript () { 492 | if (mainscript) { 493 | document.head.removeChild(mainscript) 494 | } 495 | } 496 | 497 | function initMainScript () { 498 | removeMainScript() 499 | mainscript = document.createElement('script') 500 | mainscript.type = 'text/python' 501 | mainscript.async = false 502 | mainscript.id = __MAIN__ 503 | } 504 | 505 | // Set main python script as inline 506 | function setMainValue (txt) { 507 | initMainScript() 508 | mainscript.innerHTML = txt 509 | document.head.appendChild(mainscript) 510 | } 511 | 512 | // load script from github, embed in html and execute 513 | // data dictionary input includes user, repo, name and path (fragment) 514 | function runGithub (Console, UI, data) { 515 | initBrython() 516 | loadGithubtoScript(UI, data, function (result) { 517 | setMainValue(maincontent) 518 | if (mainscript) { 519 | UI.hideworking() 520 | runBrython(Console) 521 | } 522 | }) 523 | } 524 | 525 | // load script from google and execute 526 | function runGoogle (Console, UI, fileId) { 527 | initBrython() 528 | loadGoogletoScript(UI, fileId, function () { 529 | setMainValue(maincontent) 530 | if (mainscript) { 531 | runBrython(Console) 532 | } 533 | }) 534 | } 535 | 536 | // re-execute current mainscript 537 | function runCurrent (Console) { 538 | if (mainscript) { 539 | runBrython(Console) 540 | } 541 | } 542 | 543 | // execute contents of editor and update server with new content 544 | function runEditor (UI, Console) { 545 | setMainValue(UI.geteditor()) 546 | runCurrent(Console) 547 | } 548 | 549 | // send request to login to github 550 | function loginGithub (UI) { 551 | $('#run_auth_request').submit() 552 | } 553 | 554 | // send request to logout of github 555 | // only FORGETS out github authorization - does not log out of github, per se 556 | function logoutGithub () { 557 | $('#run_auth_forget').submit() 558 | } 559 | 560 | // send request to commit/save to github 561 | function commitGithub (GH, UI) { 562 | const d = new Date() 563 | const ds = d.toLocaleDateString() 564 | const ts = d.toLocaleTimeString() 565 | const data = GH.parse(UI) 566 | data.editcontent = UI.geteditor() 567 | data.commitmsg = 'Updated from Brython Server: ' + ds + ' ' + ts 568 | UI.showworking() 569 | $.ajax({ 570 | url: 'api/v1/commit', 571 | contentType: 'application/json; charset=UTF-8', 572 | dataType: 'json', 573 | data: JSON.stringify(data), 574 | type: 'PUT', 575 | complete: function () { 576 | UI.hideworking() 577 | }, 578 | success: function (data) {}, 579 | error: function (err) { 580 | window.alert(err.responseJSON.message) 581 | } 582 | }) 583 | } 584 | 585 | // read main github script name and content 586 | // return file name 587 | function loadGithubtoScript (UI, data, callback) { 588 | if (data) { 589 | UI.showworking() 590 | $.ajax({ 591 | url: 'api/v1/load', 592 | contentType: 'application/json; charset=UTF-8', 593 | dataType: 'json', 594 | data: JSON.stringify(data), 595 | type: 'PUT', 596 | complete: function () { 597 | UI.hideworking() 598 | }, 599 | success: function (data) { 600 | maincontent = data.content 601 | UI.showlink(data.path) 602 | callback(data) 603 | }, 604 | error: function (err) { 605 | window.alert(err.responseJSON.message) 606 | } 607 | 608 | }) 609 | } 610 | } 611 | 612 | // Handling Google Drive and Oauth2 613 | 614 | // Required for Google OAuth2 615 | const googleScope = 'https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/drive.install' 616 | let googleApiKey 617 | let googleAppId 618 | 619 | // GAPI init with callback https://developers.google.com/identity/oauth2/web/guides/migration-to-gis#gapi-callback 620 | let tokenClient 621 | let gapiInited 622 | let gisInited 623 | 624 | function checkBeforeStart () { 625 | if (gapiInited && gisInited) { 626 | // Start only when both gapi and gis are initialized 627 | const UI = bsUI 628 | UI.hideworking() 629 | UI.showgoogle() 630 | } 631 | } 632 | 633 | function gapiInit () { 634 | gapi.client.init({ 635 | // NOTE: OAuth2 'scope' and 'client_id' parameters have moved to initTokenClient(). 636 | }) 637 | .then(function () { // Load the API discovery document 638 | gapi.client.load('https://www.googleapis.com/discovery/v1/apis/drive/v3/rest') 639 | gapiInited = true 640 | checkBeforeStart() 641 | }) 642 | } 643 | 644 | function gapiLoad () { 645 | gapi.load('client', gapiInit) 646 | } 647 | 648 | function gisInit (clientid) { 649 | tokenClient = google.accounts.oauth2.initTokenClient({ 650 | client_id: clientid, 651 | scope: googleScope, 652 | callback: '' // defined at request time 653 | }) 654 | gisInited = true 655 | checkBeforeStart() 656 | } 657 | 658 | // Google OAuth2 handleClientLoad 659 | // also record whether exec or index 660 | function handleClientLoad (apikey, appid) { 661 | googleApiKey = apikey 662 | googleAppId = appid 663 | } 664 | 665 | // read script from google 666 | function loadGoogletoScript (UI, fileId, callback) { 667 | tokenClient.callback = (resp) => { 668 | if (resp.error !== undefined) { 669 | throw (resp) 670 | } 671 | 672 | const GU = bsGoogleUtil 673 | UI.showworking() 674 | const request = gapi.client.drive.files.get({ 675 | fileId 676 | }) 677 | request.then(function (response) { 678 | // first, get the filename 679 | $(UI.FILE_NAME).val(response.result.name) 680 | // set up for another "get" for the data 681 | const request = gapi.client.drive.files.get({ 682 | fileId, 683 | alt: 'media' 684 | }) 685 | request.then( 686 | function (response) { 687 | $(UI.URL_INPUT).val(GU.createurl(fileId)) 688 | maincontent = response.body 689 | UI.showlink(GU.createurl(fileId)) 690 | UI.seteditor(maincontent) 691 | UI.clearselect() 692 | UI.showgoogleshareurl(fileId) 693 | gdriveFileidLoaded = fileId 694 | UI.hideworking() 695 | callback() 696 | }, 697 | function (error) { 698 | UI.hideworking() 699 | gdriveFileidLoaded = null 700 | window.alert('Something went wrong loading your file from Google Drive!') 701 | console.error(error) 702 | }) 703 | }, 704 | function (error) { 705 | UI.hideworking() 706 | gdriveFileidLoaded = null 707 | window.alert('Not Found') 708 | console.error(error) 709 | }) 710 | } 711 | // Conditionally ask users to select the Google Account they'd like to use, 712 | // and explicitly obtrain their consent to open the file picker. 713 | // NOTE: To request an access token a user gesture is necessary. 714 | if (gapi.client.getToken() === null) { 715 | // Prompt the user to select a Google Account and ask for for consent to access their data 716 | // when establishing a new session. 717 | tokenClient.requestAccessToken({ prompt: 'consent' }) 718 | } else { 719 | // Skip display of account chooser and consent dialog for an existing session. 720 | tokenClient.requestAccessToken({ prompt: '' }) 721 | } 722 | } 723 | 724 | // from https://stackoverflow.com/questions/39381563/get-file-content-of-google-docs-using-google-drive-api-v3 725 | function pickerLoadFile (data) { 726 | if (data.action === google.picker.Action.PICKED) { 727 | const file = data.docs[0] 728 | const fileId = file.id 729 | const fileName = file.name 730 | const fileUrl = file.url 731 | const UI = bsUI 732 | const request = gapi.client.drive.files.get({ 733 | fields: ['url', 'title'], 734 | fileId, 735 | alt: 'media' 736 | }) 737 | UI.showworking() 738 | request.then(function (response) { 739 | UI.seteditor(response.body) 740 | UI.clearselect() 741 | $(UI.FILE_NAME).val(fileName) 742 | $(UI.URL_INPUT).val(fileUrl) 743 | gdriveFileidLoaded = fileId 744 | UI.hideworking() 745 | UI.showgoogleshareurl(fileId) 746 | }, function (error) { 747 | UI.hideworking() 748 | console.error(error) 749 | alert(error.result.error.message) 750 | }) 751 | return request 752 | } 753 | } 754 | 755 | // Create and render a Picker object for finding python sources. 756 | function loadPicker () { 757 | tokenClient.callback = (resp) => { 758 | if (resp.error !== undefined) { 759 | throw (resp) 760 | } 761 | 762 | const oauthToken = gapi.auth.getToken().access_token 763 | gapi.load('picker', { 764 | callback: function () { 765 | if (oauthToken) { 766 | const view = new google.picker.DocsView() 767 | view.setParent('root') 768 | view.setIncludeFolders(true) 769 | view.setMimeTypes('text/plain,text/x-python') 770 | const picker = new google.picker.PickerBuilder() 771 | .enableFeature(google.picker.Feature.NAV_HIDDEN) 772 | .enableFeature(google.picker.Feature.MULTISELECT_DISABLED) 773 | .setAppId(googleAppId) 774 | .setOAuthToken(oauthToken) 775 | .addView(view) 776 | .addView(new google.picker.DocsUploadView()) 777 | .setDeveloperKey(googleApiKey) 778 | .setCallback(pickerLoadFile) 779 | .build() 780 | 781 | picker.setVisible(true) 782 | } 783 | } 784 | }) 785 | } 786 | // Conditionally ask users to select the Google Account they'd like to use, 787 | // and explicitly obtrain their consent to open the file picker. 788 | // NOTE: To request an access token a user gesture is necessary. 789 | if (gapi.client.getToken() === null) { 790 | // Prompt the user to select a Google Account and ask for for consent to access their data 791 | // when establishing a new session. 792 | tokenClient.requestAccessToken({ prompt: 'consent' }) 793 | } else { 794 | // Skip display of account chooser and consent dialog for an existing session. 795 | tokenClient.requestAccessToken({ prompt: '' }) 796 | } 797 | } 798 | 799 | // Save content to Google Drive with ID (already authenticated) 800 | // File ID must be in gdriveFileidLoaded 801 | function gdriveSaveFile () { 802 | const UI = bsUI 803 | const fileId = gdriveFileidLoaded 804 | const content = UI.geteditor() 805 | const blob = new Blob([content], { 806 | type: 'text/x-python;charset=utf8' 807 | }) 808 | UI.showworking() 809 | const xhr = new XMLHttpRequest() 810 | xhr.responseType = 'json' 811 | xhr.onreadystatechange = function () { 812 | if (xhr.readyState !== XMLHttpRequest.DONE) { 813 | return 814 | } 815 | bsUI.hideworking() // why didn't it know about UI here? 816 | switch (xhr.status) { 817 | case 200: 818 | $(bsUI.URL_INPUT).val(bsGoogleUtil.createurl(gdriveFileidLoaded)) 819 | break 820 | default: 821 | window.alert('Unable to save code to Google Drive.') 822 | break 823 | } 824 | } 825 | xhr.open('PATCH', 'https://www.googleapis.com/upload/drive/v3/files/' + fileId + '?uploadType=media') 826 | xhr.setRequestHeader('Authorization', 'Bearer ' + gapi.auth.getToken().access_token) 827 | xhr.send(blob) 828 | } 829 | 830 | // Handle the Google Drive "save" button 831 | function saveGoogle () { 832 | tokenClient.callback = (resp) => { 833 | if (resp.error !== undefined) { 834 | throw (resp) 835 | } 836 | const oauthToken = gapi.auth.getToken().access_token 837 | if (oauthToken) { 838 | if (gdriveFileidLoaded) { 839 | gdriveSaveFile() 840 | } else { 841 | $('#newfileModal').modal() 842 | } 843 | } 844 | } 845 | // Conditionally ask users to select the Google Account they'd like to use, 846 | // and explicitly obtrain their consent to open the file picker. 847 | // NOTE: To request an access token a user gesture is necessary. 848 | if (gapi.client.getToken() === null) { 849 | // Prompt the user to select a Google Account and ask for for consent to access their data 850 | // when establishing a new session. 851 | tokenClient.requestAccessToken({ prompt: 'consent' }) 852 | } else { 853 | // Skip display of account chooser and consent dialog for an existing session. 854 | tokenClient.requestAccessToken({ prompt: '' }) 855 | } 856 | } 857 | 858 | // select a newfile directory from google drive 859 | function directoryPicker (newfile) { 860 | const oauthToken = gapi.auth.getToken().access_token 861 | gapi.load('picker', { 862 | callback: function () { 863 | if (oauthToken) { 864 | const view = new google.picker.DocsView(google.picker.ViewId.FOLDERS) 865 | .setParent('root') 866 | .setIncludeFolders(true) 867 | .setMimeTypes('application/vnd.google-apps.folder') 868 | .setSelectFolderEnabled(true) 869 | const picker = new google.picker.PickerBuilder() 870 | .enableFeature(google.picker.Feature.NAV_HIDDEN) 871 | .enableFeature(google.picker.Feature.MULTISELECT_DISABLED) 872 | .setAppId(googleAppId) 873 | .setOAuthToken(oauthToken) 874 | .addView(view) 875 | .setTitle('Select Destination Folder') 876 | .setDeveloperKey(googleApiKey) 877 | .setCallback(function (data) { 878 | if (data.action === google.picker.Action.PICKED) { 879 | const dir = data.docs[0] 880 | if (dir.id) { 881 | saveGoogleWithName(newfile, dir.id) 882 | } 883 | } 884 | }) 885 | .build() 886 | picker.setVisible(true) 887 | } 888 | } 889 | }) 890 | } 891 | 892 | // function called following successful processing of new file modal 893 | function saveGoogleWithName (newfilename, folderid) { 894 | if (newfilename != null) { 895 | // if folderid is empty, user must pick one 896 | if (!folderid) { 897 | directoryPicker(newfilename) 898 | } 899 | const fileMetadata = { 900 | name: newfilename, 901 | mimeType: 'text/x-python', 902 | alt: 'media', 903 | parents: [folderid], 904 | useContentAsIndexableText: true 905 | } 906 | gapi.client.drive.files.create({ 907 | resource: fileMetadata 908 | }).then(function (response) { 909 | switch (response.status) { 910 | case 200: 911 | $(bsUI.FILE_NAME).val(response.result.name) 912 | gdriveFileidLoaded = response.result.id 913 | gdriveSaveFile() 914 | break 915 | default: 916 | window.alert('Unable to create file in Google Drive') 917 | break 918 | } 919 | }) 920 | } 921 | } 922 | 923 | // revoke google authorization 924 | function revokeAccess () { 925 | const GoogleAuth = gapi.auth2.getAuthInstance() 926 | GoogleAuth.disconnect() 927 | setSigninStatus() 928 | gdriveFileidLoaded = null 929 | } 930 | 931 | // update display according to whether user is logged in to google 932 | function setSigninStatus () { 933 | const UI = bsUI 934 | const GoogleAuth = gapi.auth2.getAuthInstance() 935 | const user = GoogleAuth.currentUser.get() 936 | const isAuthorized = user.hasGrantedScopes(googleScope) 937 | if (isAuthorized) { 938 | UI.showgoogle() 939 | } else { 940 | UI.hidegoogle() 941 | } 942 | } 943 | 944 | // examine the Url and attempt to parse as Google (1st) or Github (2nd) 945 | function loadCloud (GH, GU, UI) { 946 | const fileId = GU.parse(UI) 947 | if (fileId) { 948 | loadGoogletoScript(UI, fileId, function () {}) 949 | } else { 950 | let data = GH.parse(UI) 951 | loadGithubtoScript(UI, data, 952 | function (result) { 953 | data = GH.parseurl(result.path) 954 | $(UI.URL_INPUT).val(GH.createurl(data)) 955 | $(UI.FILE_NAME).val(data.name) 956 | UI.showshareurl(data) 957 | UI.seteditor(maincontent) 958 | UI.clearselect() 959 | UI.hideworking() 960 | }) 961 | } 962 | } 963 | 964 | // log in to Google and grant basic permissions 965 | function loginGoogle () { 966 | const GoogleAuth = gapi.auth2.getAuthInstance() 967 | if (!GoogleAuth.isSignedIn.get()) { 968 | // User is not signed in. Start Google auth flow. 969 | GoogleAuth.signIn() 970 | } 971 | } 972 | 973 | return { 974 | rungithub: runGithub, 975 | run: runCurrent, 976 | login: loginGithub, 977 | logout: logoutGithub, 978 | commit: commitGithub, 979 | load: loadCloud, 980 | runeditor: runEditor, 981 | init: initBrython, 982 | googleclientload: handleClientLoad, 983 | googleloadclick: loadPicker, 984 | googlesaveclick: saveGoogle, 985 | googlerevoke: revokeAccess, 986 | googlesetstatus: setSigninStatus, 987 | googlelogout: revokeAccess, 988 | googlelogin: loginGoogle, 989 | googlesavename: saveGoogleWithName, 990 | googleapiload: gapiLoad, 991 | googlegisinit: gisInit, 992 | rungoogle: runGoogle 993 | } 994 | }()) 995 | /* END bsController */ 996 | -------------------------------------------------------------------------------- /brythonserver/static/bssite.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | margin:0; 7 | padding:0; 8 | height:100%; 9 | background-color:#FAFAEC; 10 | } 11 | 12 | .row #output-column, #editor-column, #graphics-column { 13 | height:100%; 14 | } 15 | 16 | .container-fluid { 17 | height:100%; 18 | } 19 | 20 | span.hidden 21 | { 22 | display: none; 23 | } 24 | 25 | #nav-row { 26 | padding-top:5px; 27 | padding-left:0; 28 | padding-right:0; 29 | } 30 | 31 | #console-nav-row { 32 | padding-top:5px; 33 | padding-left:0; 34 | padding-right:0; 35 | text-align:right; 36 | } 37 | 38 | #body-row { 39 | height:84%; 40 | padding-left:10px; 41 | } 42 | 43 | #output-column, #editor-column, #graphics-column, #legal-column { 44 | padding-left:10x; 45 | padding-right:10px; 46 | padding-bottom:10px; 47 | } 48 | 49 | 50 | #loading { 51 | color:red; 52 | } 53 | 54 | #source_filename { 55 | width:150px; 56 | } 57 | 58 | #bottom-row { 59 | padding-left:10px; 60 | padding-right:10px; 61 | } 62 | 63 | #footer-row, #legalese-row { 64 | padding:10px; 65 | } 66 | 67 | #footer-row { 68 | text-align:right; 69 | } 70 | 71 | #legalese-row { 72 | text-align:left; 73 | } 74 | 75 | #console, #code { 76 | width:100%; 77 | height:100%; 78 | box-sizing: border-box; 79 | margin-top:15px; 80 | margin-left:-10px; 81 | font-family: "Lucida Console", Monaco, monospace; 82 | } 83 | 84 | #console { 85 | font-size:10pt; 86 | } 87 | 88 | #code { 89 | background-color:#ffffff; 90 | font-size:12pt; 91 | } 92 | 93 | #ggame-canvas { 94 | width:100%; 95 | height:auto; 96 | box-sizing: border-box; 97 | margin-top:15px; 98 | background-color:#ffffff; 99 | border: 1px solid #DDD; 100 | } 101 | 102 | 103 | /* ACE EDITOR */ 104 | 105 | #editorace { 106 | position:relative; 107 | top:0; 108 | right:0; 109 | bottom:0; 110 | left:0; 111 | height:100%; 112 | width:100%; 113 | border: 1px solid #DDD; 114 | border-radius: 4px; 115 | border-bottom-right-radius: 0px; 116 | margin-top:15px; 117 | margin-left:-10px; 118 | font-family: "Lucida Console", Monaco, monospace; 119 | font-size:10pt; 120 | } 121 | 122 | 123 | -------------------------------------------------------------------------------- /brythonserver/static/drive_16dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrythonServer/brython-server/7fd1a73b14c1b4843f430a1d2263524083ce850a/brythonserver/static/drive_16dp.png -------------------------------------------------------------------------------- /brythonserver/static/drive_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrythonServer/brython-server/7fd1a73b14c1b4843f430a1d2263524083ce850a/brythonserver/static/drive_24dp.png -------------------------------------------------------------------------------- /brythonserver/static/drive_32dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrythonServer/brython-server/7fd1a73b14c1b4843f430a1d2263524083ce850a/brythonserver/static/drive_32dp.png -------------------------------------------------------------------------------- /brythonserver/static/drive_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrythonServer/brython-server/7fd1a73b14c1b4843f430a1d2263524083ce850a/brythonserver/static/drive_48dp.png -------------------------------------------------------------------------------- /brythonserver/static/drive_64dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrythonServer/brython-server/7fd1a73b14c1b4843f430a1d2263524083ce850a/brythonserver/static/drive_64dp.png -------------------------------------------------------------------------------- /brythonserver/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrythonServer/brython-server/7fd1a73b14c1b4843f430a1d2263524083ce850a/brythonserver/static/favicon-16x16.png -------------------------------------------------------------------------------- /brythonserver/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrythonServer/brython-server/7fd1a73b14c1b4843f430a1d2263524083ce850a/brythonserver/static/favicon-32x32.png -------------------------------------------------------------------------------- /brythonserver/static/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrythonServer/brython-server/7fd1a73b14c1b4843f430a1d2263524083ce850a/brythonserver/static/favicon-96x96.png -------------------------------------------------------------------------------- /brythonserver/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrythonServer/brython-server/7fd1a73b14c1b4843f430a1d2263524083ce850a/brythonserver/static/favicon.ico -------------------------------------------------------------------------------- /brythonserver/templates/_rawcookies.html: -------------------------------------------------------------------------------- 1 |

Last updated: July 08, 2019

2 | 3 |

{{ title }} ("us", "we", or "our") uses cookies on the {{ url }} website (the "Service"). By using the Service, you consent to the use of cookies.

4 | 5 |

Our Cookies Policy explains what cookies are, how we use cookies, how third-parties we may partner with may use cookies on the Service, your choices regarding cookies and further information about cookies.

6 | 7 |

What are cookies

8 | 9 |

Cookies are small pieces of text sent by your web browser by a website you visit. A cookie file is stored in your web browser and allows the Service or a third-party to recognize you and make your next visit easier and the Service more useful to you.

10 | 11 |

Cookies can be "persistent" or "session" cookies. Persistent cookies remain on your personal computer or mobile device when you go offline, while session cookies are deleted as soon as you close your web browser.

12 | 13 |

How {{ title }} uses cookies

14 | 15 |

When you use and access the Service, we may place a number of cookies files in your web browser.

16 | 17 |

We use cookies for the following purposes:

18 | 19 |
    20 |
  • 21 |

    To enable certain functions of the Service

    22 | 23 |

    We use both session and persistent cookies on the Service and we use different types of cookies to run the Service:

    24 | 25 |

    Essential cookies. We may use essential cookies to authenticate users and prevent fraudulent use of user accounts.

    26 |
  • 27 |
28 | 29 |

What are your choices regarding cookies

30 | 31 |

If you'd like to delete cookies or instruct your web browser to delete or refuse cookies, please visit the help pages of your web browser. As an European citizen, under GDPR, you have certain individual rights. You can learn more about these rights in the GDPR Guide.

32 | 33 |

Please note, however, that if you delete cookies or refuse to accept them, you might not be able to use all of the features we offer, you may not be able to store your preferences, and some of our pages might not display properly.

34 | 35 | 52 | 53 |

Where can you find more information about cookies

54 | 55 |

You can learn more about cookies and the following third-party websites:

56 | 57 | -------------------------------------------------------------------------------- /brythonserver/templates/_rawprivacy.html: -------------------------------------------------------------------------------- 1 | 2 |

Effective date: July 09, 2019

3 | 4 | 5 |

{{ title }} ("us", "we", or "our") operates the {{ url }} website (hereinafter referred to as the "Service").

6 | 7 |

This page informs you of our policies regarding the collection, use and disclosure of personal data when you use our Service and the choices you have associated with that data.

8 | 9 |

We use your data to provide and improve the Service. By using the Service, you agree to the collection and use of information in accordance with this policy. Unless otherwise defined in this Privacy Policy, the terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, accessible from {{ url }}

10 | 11 |

Definitions

12 |
    13 |
  • 14 |

    Service

    15 |

    Service is the {{ url }} website operated by {{ title }}

    16 |
  • 17 |
  • 18 |

    Personal Data

    19 |

    Personal Data means data about a living individual who can be identified from those data (or from those and other information either in our possession or likely to come into our possession).

    20 |
  • 21 |
  • 22 |

    Usage Data

    23 |

    Usage Data is data collected automatically either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).

    24 |
  • 25 |
  • 26 |

    Cookies

    27 |

    Cookies are small files stored on your device (computer or mobile device).

    28 |
  • 29 |
  • 30 |

    Data Controller

    31 |

    Data Controller means the natural or legal person who (either alone or jointly or in common with other persons) determines the purposes for which and the manner in which any personal information are, or are to be, processed.

    32 |

    For the purpose of this Privacy Policy, we are a Data Controller of your Personal Data.

    33 |
  • 34 |
  • 35 |

    Data Processors (or Service Providers)

    36 |

    Data Processor (or Service Provider) means any natural or legal person who processes the data on behalf of the Data Controller.

    37 |

    We may use the services of various Service Providers in order to process your data more effectively.

    38 |
  • 39 |
  • 40 |

    Data Subject (or User)

    41 |

    Data Subject is any living individual who is using our Service and is the subject of Personal Data.

    42 |
  • 43 |
44 | 45 | 46 |

Information Collection and Use

47 |

We collect several different types of information for various purposes to provide and improve our Service to you.

48 | 49 |

Types of Data Collected

50 | 51 |

Personal Data

52 |

While using our Service, we may ask you to provide us with certain personally identifiable information that can be used to contact or identify you ("Personal Data"). Personally identifiable information may include, but is not limited to:

53 | 54 |
    55 |
  • Cookies and Usage Data
  • 56 |
57 | 58 | 59 |

Usage Data

60 | 61 |

We may also collect information on how the Service is accessed and used ("Usage Data"). This Usage Data may include information such as your computer's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that you visit, the time and date of your visit, the time spent on those pages, unique device identifiers and other diagnostic data.

62 | 63 | 64 |

Tracking & Cookies Data

65 |

We use cookies and similar tracking technologies to track the activity on our Service and we hold certain information.

66 |

Cookies are files with a small amount of data which may include an anonymous unique identifier. Cookies are sent to your browser from a website and stored on your device. Other tracking technologies are also used such as beacons, tags and scripts to collect and track information and to improve and analyse our Service.

67 |

You can instruct your browser to refuse all cookies or to indicate when a cookie is being sent. However, if you do not accept cookies, you may not be able to use some portions of our Service.

68 |

Examples of Cookies we use:

69 |
    70 |
  • Session Cookies. We use Session Cookies to operate our Service.
  • 71 |
  • Preference Cookies. We use Preference Cookies to remember your preferences and various settings.
  • 72 |
  • Security Cookies. We use Security Cookies for security purposes.
  • 73 |
74 | 75 |

Use of Data

76 |

{{ title }} uses the collected data for various purposes:

77 |
    78 |
  • To provide and maintain our Service
  • 79 |
  • To notify you about changes to our Service
  • 80 |
  • To allow you to participate in interactive features of our Service when you choose to do so
  • 81 |
  • To provide customer support
  • 82 |
  • To gather analysis or valuable information so that we can improve our Service
  • 83 |
  • To monitor the usage of our Service
  • 84 |
  • To detect, prevent and address technical issues
  • 85 |
86 | 87 | 88 |

Legal Basis for Processing Personal Data under the General Data Protection Regulation (GDPR)

89 |

If you are from the European Economic Area (EEA), {{ title }} legal basis for collecting and using the personal information described in this Privacy Policy depends on the Personal Data we collect and the specific context in which we collect it.

90 |

{{ title }} may process your Personal Data because:

91 |
    92 |
  • We need to perform a contract with you
  • 93 |
  • You have given us permission to do so
  • 94 |
  • The processing is in our legitimate interests and it is not overridden by your rights
  • 95 |
  • To comply with the law
  • 96 |
97 | 98 | 99 |

Retention of Data

100 |

{{ title }} will retain your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes and enforce our legal agreements and policies.

101 |

{{ title }} will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of our Service, or we are legally obligated to retain this data for longer periods.

102 | 103 |

Transfer of Data

104 |

Your information, including Personal Data, may be transferred to — and maintained on — computers located outside of your state, province, country or other governmental jurisdiction where the data protection laws may differ from those of your jurisdiction.

105 |

If you are located outside United States and choose to provide information to us, please note that we transfer the data, including Personal Data, to United States and process it there.

106 |

Your consent to this Privacy Policy followed by your submission of such information represents your agreement to that transfer.

107 |

{{ title }} will take all the steps reasonably necessary to ensure that your data is treated securely and in accordance with this Privacy Policy and no transfer of your Personal Data will take place to an organisation or a country unless there are adequate controls in place including the security of your data and other personal information.

108 | 109 |

Disclosure of Data

110 | 111 | 112 |

Legal Requirements

113 |

{{ title }} may disclose your Personal Data in the good faith belief that such action is necessary to:

114 |
    115 |
  • To comply with a legal obligation
  • 116 |
  • To protect and defend the rights or property of {{ title }}
  • 117 |
  • To prevent or investigate possible wrongdoing in connection with the Service
  • 118 |
  • To protect the personal safety of users of the Service or the public
  • 119 |
  • To protect against legal liability
  • 120 |
121 | 122 |

Security of Data

123 |

The security of your data is important to us but remember that no method of transmission over the Internet or method of electronic storage is 100% secure. While we strive to use commercially acceptable means to protect your Personal Data, we cannot guarantee its absolute security.

124 | 125 | 126 |

Your Data Protection Rights under the General Data Protection Regulation (GDPR)

127 |

If you are a resident of the European Economic Area (EEA), you have certain data protection rights. {{ title }} aims to take reasonable steps to allow you to correct, amend, delete or limit the use of your Personal Data.

128 |

If you wish to be informed about what Personal Data we hold about you and if you want it to be removed from our systems, please contact us.

129 |

In certain circumstances, you have the following data protection rights:

130 |
    131 |
  • 132 |

    The right to access, update or delete the information we have on you. Whenever made possible, you can access, update or request deletion of your Personal Data directly within your account settings section. If you are unable to perform these actions yourself, please contact us to assist you.

    133 |
  • 134 |
  • 135 |

    The right of rectification. You have the right to have your information rectified if that information is inaccurate or incomplete.

    136 |
  • 137 |
  • 138 |

    The right to object. You have the right to object to our processing of your Personal Data.

    139 |
  • 140 |
  • 141 |

    The right of restriction. You have the right to request that we restrict the processing of your personal information.

    142 |
  • 143 |
  • 144 |

    The right to data portability. You have the right to be provided with a copy of the information we have on you in a structured, machine-readable and commonly used format.

    145 |
  • 146 |
  • 147 |

    The right to withdraw consent. You also have the right to withdraw your consent at any time where {{ title }} relied on your consent to process your personal information.

    148 |
  • 149 |
150 |

Please note that we may ask you to verify your identity before responding to such requests.

151 | 152 |

You have the right to complain to a Data Protection Authority about our collection and use of your Personal Data. For more information, please contact your local data protection authority in the European Economic Area (EEA).

153 | 154 |

Service Providers

155 |

We may employ third party companies and individuals to facilitate our Service ("Service Providers"), provide the Service on our behalf, perform Service-related services or assist us in analysing how our Service is used.

156 |

These third parties have access to your Personal Data only to perform these tasks on our behalf and are obligated not to disclose or use it for any other purpose.

157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 |

Links to Other Sites

165 |

Our Service may contain links to other sites that are not operated by us. If you click a third party link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit.

166 |

We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.

167 | 168 | 173 | 174 |

Changes to This Privacy Policy

175 |

We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page.

176 |

We will let you know via email and/or a prominent notice on our Service, prior to the change becoming effective and update the "effective date" at the top of this Privacy Policy.

177 |

You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.

178 | 179 | 180 |

Contact Us

181 |

If you have any questions about this Privacy Policy, please contact us:

182 |
    183 |
  • By email: {{ contact }}
  • 184 | 185 |
-------------------------------------------------------------------------------- /brythonserver/templates/_rawtermsofservice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Last updated: July 09, 2019

4 | 5 | 6 |

These Terms and Conditions ("Terms", "Terms and Conditions") govern your relationship with {{ url }} website (the "Service") operated by {{ title }} ("us", "we", or "our").

7 | 8 |

Please read these Terms and Conditions carefully before using the Service.

9 | 10 |

Your access to and use of the Service is conditioned on your acceptance of and compliance with these Terms. These Terms apply to all visitors, users and others who access or use the Service.

11 | 12 |

By accessing or using the Service you agree to be bound by these Terms. If you disagree with any part of the terms then you may not access the Service.

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |

Links To Other Web Sites

28 | 29 |

Our Service may contain links to third-party web sites or services that are not owned or controlled by {{ title }}.

30 | 31 |

{{ title }} has no control over, and assumes no responsibility for, the content, privacy policies, or practices of any third party web sites or services. You further acknowledge and agree that {{ title }} shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such web sites or services.

32 | 33 |

We strongly advise you to read the terms and conditions and privacy policies of any third-party web sites or services that you visit.

34 | 35 | 36 |

Termination

37 | 38 |

We may terminate or suspend your access immediately, without prior notice or liability, for any reason whatsoever, including without limitation if you breach the Terms.

39 | 40 |

Upon termination, your right to use the Service will immediately cease.

41 | 42 | 43 |

Limitation Of Liability

44 | 45 |

In no event shall {{ title }}, nor its directors, employees, partners, agents, suppliers, or affiliates, be liable for any indirect, incidental, special, consequential or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from (i) your access to or use of or inability to access or use the Service; (ii) any conduct or content of any third party on the Service; (iii) any content obtained from the Service; and (iv) unauthorized access, use or alteration of your transmissions or content, whether based on warranty, contract, tort (including negligence) or any other legal theory, whether or not we have been informed of the possibility of such damage, and even if a remedy set forth herein is found to have failed of its essential purpose.

46 | 47 | 48 |

Disclaimer

49 | 50 |

Your use of the Service is at your sole risk. The Service is provided on an "AS IS" and "AS AVAILABLE" basis. The Service is provided without warranties of any kind, whether express or implied, including, but not limited to, implied warranties of merchantability, fitness for a particular purpose, non-infringement or course of performance.

51 | 52 |

{{ title }} its subsidiaries, affiliates, and its licensors do not warrant that a) the Service will function uninterrupted, secure or available at any particular time or location; b) any errors or defects will be corrected; c) the Service is free of viruses or other harmful components; or d) the results of using the Service will meet your requirements.

53 | 54 | 55 |

Governing Law

56 | 57 |

These Terms shall be governed and construed in accordance with the laws of Vermont, United States, without regard to its conflict of law provisions.

58 | 59 |

Our failure to enforce any right or provision of these Terms will not be considered a waiver of those rights. If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining provisions of these Terms will remain in effect. These Terms constitute the entire agreement between us regarding our Service, and supersede and replace any prior agreements we might have between us regarding the Service.

60 | 61 | 62 |

Changes

63 | 64 |

We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material we will try to provide at least 30 days notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion.

65 | 66 |

By continuing to access or use our Service after those revisions become effective, you agree to be bound by the revised terms. If you do not agree to the new terms, please stop using the Service.

67 | 68 | 69 |

Contact Us

70 | 71 |

If you have any questions about these Terms, please contact us.

-------------------------------------------------------------------------------- /brythonserver/templates/console.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 | 5 | 14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 | 24 |
25 | 26 | 27 |
28 | {% include 'legalese.html' %} 29 | 33 |
34 | 35 | 36 | 37 |
38 | 39 |
40 | 41 | {% endblock %} 42 | 43 | {% block foot %} 44 | {{ super() }} 45 | 53 | 57 | {% endblock %} 58 | 59 | -------------------------------------------------------------------------------- /brythonserver/templates/cookiebanner.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /brythonserver/templates/cookies.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %} 3 | {{ title }} Cookies Policy 4 | {% endblock %} 5 | {% block brythonjs %} 6 | {% endblock %} 7 | {% block body %} 8 | 11 |
12 | 15 |
16 |
17 | {% include 'legalese.html' %} 18 | 24 |
25 | {% endblock %} 26 | {% block foot %} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /brythonserver/templates/exec.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 | 5 | {% if cookieconsent %} 6 | 28 | {% else %} 29 | {% include 'cookiebanner.html' %} 30 | {% endif %} 31 | 32 | 33 | 34 |
35 | {% if cookieconsent %} 36 |
37 |
38 | {% endif %} 39 |
40 | 41 |
42 | {% if cookieconsent %} 43 | 46 | {% endif %} 47 |
48 | 49 | 50 |
51 | {% include 'legalese.html' %} 52 | 58 |
59 | 60 | 61 |
62 | 63 |
64 | 65 | 66 | 67 | {% endblock %} 68 | 69 | {% block foot %} 70 | {{ super() }} 71 | 99 | {% endblock %} -------------------------------------------------------------------------------- /brythonserver/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | 6 | 7 | {% endblock %} 8 | 9 | {% block body %} 10 | 11 | {% if cookieconsent %} 12 | 82 | {% else %} 83 | {% include 'cookiebanner.html' %} 84 | {% endif %} 85 | 86 | 87 | 88 |
89 |
90 | 91 |
{{ editcontent }}
92 |
93 |
94 | 95 |
96 | 99 |
100 | 101 | 102 |
103 | {% include 'legalese.html' %} 104 | 113 |
114 | 115 | 116 | 152 | 153 | 154 |
155 | 156 |
157 |
158 | 159 |
160 | 161 | 162 | 163 | 164 | {% endblock %} 165 | 166 | {% block foot %} 167 | {{ super() }} 168 | {% if cookieconsent %} 169 | 213 | {% endif %} 214 | {% endblock %} 215 | -------------------------------------------------------------------------------- /brythonserver/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %} 5 | {{ title }} 6 | {% endblock %} 7 | {% block head %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% endblock %} 28 | {% block brythonjs %} 29 | {% if cookieconsent %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 50 | {% endif %} 51 | {% endblock %} 52 | 53 |
54 | {% block body %} 55 | {% endblock %} 56 |
57 | {% block foot %} 58 | {% if cookieconsent %} 59 | 60 | 61 | 62 | {% endif %} 63 | {% endblock %} 64 | 65 | 66 | -------------------------------------------------------------------------------- /brythonserver/templates/legalese.html: -------------------------------------------------------------------------------- 1 |
2 | privacy policy :: 3 | cookies :: 4 | terms of service :: 5 | contact 6 |
7 | -------------------------------------------------------------------------------- /brythonserver/templates/privacy.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %} 3 | {{ title }} Privacy Policy 4 | {% endblock %} 5 | {% block brythonjs %} 6 | {% endblock %} 7 | {% block body %} 8 | 11 | 12 |
13 | 16 |
17 |
18 | {% include 'legalese.html' %} 19 | 25 |
26 | {% endblock %} 27 | {% block foot %} 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /brythonserver/templates/termsofservice.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %} 3 | {{ title }} Terms of Service 4 | {% endblock %} 5 | {% block brythonjs %} 6 | {% endblock %} 7 | {% block body %} 8 | 11 | 12 |
13 | 16 |
17 |
18 | {% include 'legalese.html' %} 19 | 25 |
26 | {% endblock %} 27 | {% block foot %} 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /brythonserver/utility.py: -------------------------------------------------------------------------------- 1 | """ 2 | Brython-Server utility functions for Github and session management 3 | Author: E Dennison 4 | """ 5 | 6 | import os 7 | import urllib 8 | import json 9 | import base64 10 | import random 11 | import string 12 | from flask import session, url_for 13 | from .definitions import ( 14 | ENV_GITHUBCLIENTID, 15 | ENV_GITHUBSECRET, 16 | SESSION_GITHUBSTATE, 17 | URL_GITHUBAUTHORIZE, 18 | URL_GITHUBRETRIEVETOKEN, 19 | SESSION_METADATA, 20 | SESSION_ACCESSTOKEN, 21 | ) 22 | 23 | 24 | def github_client_id(): 25 | """Retrieve the Github Client ID.""" 26 | return os.environ.get(ENV_GITHUBCLIENTID, "") 27 | 28 | 29 | def github_client_secret(): 30 | """Retriev the Github Client Secret.""" 31 | return os.environ.get(ENV_GITHUBSECRET, "") 32 | 33 | 34 | def newgithubstate(): 35 | """Create new Github STATE for login 36 | 37 | Sets a random string as local session state for Github, if not 38 | already set. 39 | 40 | Return the state string 41 | """ 42 | state = session.get(SESSION_GITHUBSTATE, "") 43 | if SESSION_GITHUBSTATE not in session: 44 | nchars = 20 45 | state = "".join( 46 | random.SystemRandom().choice(string.ascii_uppercase + string.digits) 47 | for _ in range(nchars) 48 | ) 49 | session[SESSION_GITHUBSTATE] = state 50 | return state 51 | 52 | 53 | def getredirecturl(): 54 | """Construct a redirect URL for returning here from Github 55 | 56 | Return the redirect URL 57 | """ 58 | url = url_for("root", _external=True) 59 | if url.endswith(":80"): 60 | url = url[:-3] 61 | return url 62 | 63 | 64 | # Public functions 65 | 66 | 67 | def checkgithubstate(state): 68 | """Verify correct Github STATE has been returned 69 | 70 | Return True if matching, False otherwise 71 | """ 72 | return state == session.get(SESSION_GITHUBSTATE, "") 73 | 74 | 75 | def githubauthurl(): 76 | """Construct a redirect URL TO Github. 77 | 78 | Return the URL 79 | """ 80 | url = URL_GITHUBAUTHORIZE + "?" 81 | url += "client_id=" + github_client_id() 82 | url += "&redirect_uri=" + getredirecturl() 83 | url += "&scope=repo gist&state=" + newgithubstate() 84 | print(url) 85 | return url 86 | 87 | 88 | def githubretrievetoken(code): 89 | """Retrieve a user's access token from Github. 90 | 91 | Returns nothing 92 | """ 93 | gitrequest = urllib.request.Request(URL_GITHUBRETRIEVETOKEN) 94 | gitrequest.add_header("Accept", "application/json") 95 | parameters = { 96 | "client_id": github_client_id(), 97 | "client_secret": github_client_secret(), 98 | "code": code, 99 | "redirect_uri": getredirecturl(), 100 | } 101 | data = urllib.parse.urlencode(parameters) 102 | data = data.encode("utf-8") 103 | with urllib.request.urlopen(gitrequest, data) as response: 104 | jsresponse = json.loads(response.read().decode("utf-8")) 105 | if "access_token" in jsresponse: 106 | session[SESSION_ACCESSTOKEN] = jsresponse.get("access_token") 107 | 108 | 109 | def githubforgetauth(): 110 | """Forget the current Github authorization.""" 111 | session.pop(SESSION_ACCESSTOKEN, None) 112 | 113 | 114 | def finishrequestsetup(url, method): 115 | """Complete preparations for initiating a request to Github 116 | 117 | Arguments: 118 | url -- the github url for the request. 119 | method -- http mehthod (e.g. GET, etc.) 120 | 121 | Return tuple: 122 | gitrequest - the request object from urllib.request.Request 123 | token - the token being used in the request 124 | """ 125 | token = session.get(SESSION_ACCESSTOKEN) 126 | gitrequest = urllib.request.Request(url, method=method) 127 | gitrequest.add_header("Authorization", f"token {token}") 128 | gitrequest.add_header("User-Agent", "Brython-Server") 129 | return gitrequest, token 130 | 131 | 132 | def finishrequest(gitrequest, retrievalmethod, metamethod=None): 133 | """Boilerplate for finishing the API request to Github 134 | 135 | Arguments: 136 | gitrequest -- request object 137 | token -- session token 138 | retrievalmethod -- function for extracting file contents from response 139 | metamethod -- optional function for extracting metadata from response 140 | 141 | Return tuple: 142 | binreturn - the resource data 143 | sha - the resource SHA 144 | """ 145 | jsresponse = sha = None 146 | try: 147 | with urllib.request.urlopen(gitrequest) as response: 148 | jsresponse = json.loads(response.read().decode("utf-8")) 149 | sha = jsresponse.get("sha", "") 150 | except urllib.error.HTTPError as err: 151 | if err.code != 304: # Not Modified - use earlier jsresponse 152 | raise 153 | binreturn = retrievalmethod(jsresponse) 154 | session[SESSION_METADATA] = metamethod(jsresponse) if metamethod else "" 155 | 156 | try: 157 | return binreturn.decode("utf-8"), sha 158 | except UnicodeDecodeError: 159 | return binreturn, sha 160 | 161 | 162 | def gistrequest(gistid, method="GET"): 163 | """Initiate a request to the Github gist API. 164 | 165 | Arguments: 166 | gistid -- the hex identifier for the file 167 | method -- http mehthod (e.g. GET, etc.) (Default is 'GET') 168 | 169 | Return tuple: 170 | gitrequest - the request object from urllib.request.Request 171 | token - the token being used in the request 172 | """ 173 | url = f"https://api.github.com/gists/{gistid}" 174 | return finishrequestsetup(url, method) 175 | 176 | 177 | def githubrequest(user, repo, path, method="GET"): 178 | """Initiate a request to the Github content API. 179 | 180 | Arguments: 181 | user -- the Github user ID/name 182 | repo -- the Github user's repository name 183 | path -- optional path fragment or path to file in the repo 184 | method -- http mehthod (e.g. GET, etc.) (Default is 'GET') 185 | 186 | Return tuple: 187 | gitrequest - the request object from urllib.request.Request 188 | token - the token being used in the request 189 | """ 190 | url = f"https://api.github.com/repos/{user}/{repo}/contents/{path}" 191 | return finishrequestsetup(url, method) 192 | 193 | 194 | def githubretrievegist(gistid): 195 | """Retrieve a gist from Github via API. 196 | 197 | Arguments: 198 | gistid -- hex string identifying a gist 199 | 200 | Return tuple: 201 | content -- the content of the specific file 202 | sha -- the file sha (used for subsequent commits, if any) 203 | """ 204 | gitrequest, _token = gistrequest(gistid) 205 | return finishrequest( 206 | gitrequest, 207 | lambda x: x["files"][list(x["files"].keys())[0]]["content"].encode( 208 | "utf-8" 209 | ), # content 210 | lambda x: list(x["files"].keys())[0], 211 | ) # file name 212 | 213 | 214 | def githubretrievefile(user, repo, path): 215 | """Retrieve a specific file from Github via API. 216 | 217 | Arguments: 218 | user -- the Github user ID/name 219 | repo -- the Github user's repository name 220 | path -- specific path to file within the repo 221 | 222 | Return tuple: 223 | content -- the content of the specific file 224 | sha -- the file sha (used for subsequent commits, if any) 225 | """ 226 | 227 | def retrievalmethod(x): 228 | return base64.b64decode(x["content"].encode("utf-8")) 229 | 230 | gitrequest, _token = githubrequest(user, repo, path) 231 | return finishrequest(gitrequest, retrievalmethod) 232 | 233 | 234 | def githubgetmainfile(user, repo, path): 235 | """Retrieve the 'main' Python file from a repo/directory. Function 236 | attempts to make a sensible decision. 237 | 238 | Arguments: 239 | user -- the Github user ID/name 240 | repo -- the Github user's repository name 241 | path -- specific directory path within the repo 242 | 243 | Return: the name of the file 244 | """ 245 | jsresponse = None 246 | gitrequest, _token = githubrequest(user, repo, path) 247 | with urllib.request.urlopen(gitrequest) as response: 248 | jsresponse = json.loads(response.read().decode("utf-8")) 249 | names = [f["name"] for f in filter(lambda x: x["type"] == "file", jsresponse)] 250 | return selectmainfile(names) 251 | 252 | 253 | def githubpath(user, repo, branch, path, name): 254 | """Build a valid URL to file on Github. 255 | 256 | Note: This is sensitive to changes in how Github URLs work. 257 | 258 | Arguments: 259 | user -- the Github user ID/name 260 | repo -- the Github user's repository name 261 | branch -- specific branch name (typ. master or main) 262 | path -- specific path to file within the repo 263 | name -- specific file name or gist id 264 | 265 | Returns URL to Github file or gist. 266 | """ 267 | if user != "" and repo != "": 268 | retval = f"https://github.com/{format}/{repo}/blob/{branch}/" 269 | else: 270 | retval = "https://gist.github.com/" 271 | if path: 272 | retval += path 273 | if name not in path: 274 | retval += "/" + name 275 | else: 276 | retval += name 277 | return retval 278 | 279 | 280 | def githubloggedin(): 281 | """Return whether we are logged in to Github (True/False).""" 282 | github_token = session.get(SESSION_ACCESSTOKEN) 283 | return github_token 284 | 285 | 286 | def selectmainfile(names): 287 | """Determine 'main' python file from a list of candidates. 288 | 289 | Arguments: 290 | names -- a list of candidate names 291 | Return: the best candidate name 292 | """ 293 | mainfile = "" 294 | for foundname in names: 295 | if ( 296 | mainfile == "" 297 | and len(foundname) > 3 298 | and foundname[-3:] == ".py" 299 | or 300 | # Foud a python file called main.py or __main__.py? Make IT the one! 301 | foundname in ["main.py", "__main__.py"] 302 | ): 303 | mainfile = foundname 304 | return mainfile 305 | -------------------------------------------------------------------------------- /docs/Design.md: -------------------------------------------------------------------------------- 1 | # Brython-Server Design Specification 2 | 3 | ## Architecture 4 | 5 | ### Server Side Routes 6 | 7 | The Brython-Server server side is built using the Python-based Flask application framework, running under 8 | Python 3. The server will provide the following main entry points: 9 | 10 | #### `/` 11 | 12 | Main landing page, presents the user with blank edit and console windows, a Github URL text box, "LOAD", ">" and "LOGIN" 13 | buttons. This URL is also for used with any mode in which Python code is executing under Brython. This is required 14 | in order for any imported files to be correctly referenced using the `/` path. This particular URL will be 15 | described in more detail in another section. 16 | 17 | #### `/static/` 18 | 19 | Path for retrieving static content, including CSS files and any client-side scripts required. 20 | 21 | #### `/favicon.ico` 22 | 23 | Path for retrieving the site favicon file. 24 | 25 | #### `/` 26 | 27 | Path for retrieving any imported files required for the main executing Python file. No support currently provided 28 | for files outside of the main source file's root folder. Support for imported files or resources for the main 29 | Python file are only provided in the context of executing from a Github repository or file. 30 | 31 | Files available at this routing point exist internally as elements of a dictionary 32 | stored as session data by Flask. Brython-Server overrides the default session 33 | implementation (cookies) and uses a Redis backend for all session data. This 34 | permits storage of arbitrarily large temporary files without using the host 35 | file system directly. The session data is populated from sources on Github 36 | as a result of the `load` API method (see next section). 37 | 38 | #### `/api/v1/ (POST, PUT, GET)` 39 | 40 | The server responds to several API URLs. These are used to communicate directly with the 41 | client side, using JSON as the encoding method. 42 | 43 | `method` | Description | Allowed Methods | Data Input (JSON) | Data Output (JSON) 44 | --- | --- | --- | --- | --- 45 | `load` | Load and cache file(s) from the named Github repository. Identify single main file, return its name and content. | POST | `user`, `repo`, `path` {optional path fragment}, `name` {optional main file name} | `name` {main file name}, `path` {main file path}, `content` {main file content}, `success` {true/false} 46 | `commit` | Commit main file changes made by the user to the originating Github file. | PUT | `user`, `repo`, `path` {path fragment}, `name` {main file name}, `editcontent` {current editor content}, `commitmsg` {message to use for Github commit} | `success` {true/false} 47 | 48 | ### Server Github Integration 49 | 50 | The server side handles all interaction with Github, via the Github V3 API. 51 | 52 | If the user is not authenticated with Github, then authentication with the Github API uses the Brython-Server 53 | developer token, which allows up to 5000 transactions per hour, *globally*. In this situation, the user may modifiy code 54 | shown in their browser, and execute it, but will not be able to commit any modifications back to Github. 55 | 56 | The user may press the 'LOGIN' button on the Brython-Server page, which will redirect to a Github login page. Github 57 | will then ask the user if they wish to authorize Brython-Server to have read/write access to their private 58 | repositories. If the user approves, Github will return to Brython server with an authorization token for the 59 | session. All subsequent Github interactions with Brython-Server, during the user's browser session, will be 60 | authorized with this custom token. While logged in the user is subject to their own 5000 transaction per hour 61 | limitation. 62 | 63 | While the user is logged in to Github, there will be a 'LOG OUT' button provided on the Brython-Server web page. 64 | When the user presses 'LOG OUT', the user's session will forget its access token and will not be able to make 65 | further commits to any Github repository. If the user wishes to log back in, they will immediately be issued 66 | a new access token, provided they are still have a session active with the Github web site. 67 | 68 | ### Client Side 69 | 70 | The Brython-Server client side system consists of a single Javascript include, which is loaded with the 71 | Brython-Server web page. 72 | 73 | The principal responsibility of the client side code is to provide communication with the Brython-Server 74 | server side API (`load` and `commit`), described earlier. In addition, there is code for dynamically 75 | enabling the different buttons and links that the user is able to access, depending on their current login state 76 | and whether they have loaded any code from Github. Finally, there are routines that "hijack" the browser's 77 | alert and prompt functions in Javascript, re-routing text to a console textarea on the browser page. 78 | 79 | The main web page has two flavors, determined by the html templates index.html and a exec.html, both of which are rendered from the root URL (`/`) of the server, depending on how it was used. 80 | 81 | #### index.html 82 | 83 | This template is used when visiting the server root, and presents the visitor with a Python code editing pane on the 84 | left hand side, drive by the Ace Javascript editor. The Python execution console is shown in a smaller right-hand pane. 85 | The user may begin writing/editing code immediately and executing it by pressing the '>' button. When the '>' button 86 | is pressed the `brython` function is executed, naming the editor ID as an argument. 87 | 88 | Once a file or project has been loaded from Github, this page will also show a 'SHARE' button which will, when 89 | pressed, open a new tab/window using the `exec.html` template. 90 | 91 | #### exec.html 92 | 93 | This template is used when visiting the server root with arguments in the URL to indicate a specific 94 | Github user, repository and file path. In this instance, the Python execution console consumes a full width pane 95 | on the page and the code is loaded from Github and executed without delay. The URL shown in this mode may be 96 | copied and pasted as a hyperlink on another page or in an e-mail (i.e. "shared"). 97 | 98 | In this instance a button is available which allows the user to edit the main file using the `index.html` template by 99 | opening a new window/tab. 100 | 101 | ### Deployment 102 | 103 | Following are steps for deploying Brython-Server to a Linux host: 104 | 105 | #### Prerequisites 106 | 107 | 1. Use the system package manager to install Python 3. 108 | 2. Follow these instructions to install [Redis](http://redis.io/topics/quickstart). 109 | 110 | #### Clone Brython-Server 111 | 112 | Create a local copy of the Brython-Server sources: 113 | 114 | git clone https://github.com/tiggerntatie/brython-server.git 115 | 116 | #### Create Virtualenv 117 | 118 | Create a virtual environment for the server: 119 | 120 | virtualenv brython-server 121 | 122 | Note that this directory should not necessarily be within the Brython-Server source 123 | tree, or vice versa. 124 | 125 | Activate your virtual environment: 126 | 127 | source brython-server/bin/activate 128 | 129 | Then install the following dependencies using PIP: 130 | 131 | 1. flask 132 | 2. gunicorn 133 | 3. redis 134 | 135 | Test the server locally by running: 136 | 137 | python main.py 138 | 139 | #### Install Brython 140 | 141 | Navigate to the `static` subfolder and clone the Brython project: 142 | 143 | git clone https://github.com/brython-dev/brython.git 144 | 145 | Then check out the version that you wish to use (for example: version 3.3.2): 146 | 147 | git checkout tags/3.3.2 148 | 149 | #### Configure for Automatic Startup 150 | 151 | Create a startup script in `/etc/init`. For example: `/etc/init/brython-server.conf` with 152 | the following contents: 153 | 154 | description "brython-server" 155 | start on (filesystem) 156 | stop on runlevel [016] 157 | 158 | env githubtoken= 159 | env githubsecret= 160 | env githubclientid= 161 | env flasksecret= 162 | 163 | respawn 164 | setuid 165 | setgid 166 | chdir 167 | 168 | exec -b : main:app 169 | 170 | Text enclosed in `<>` brackets indicates information specific to your installation. 171 | 172 | The `env` lines provide installation-specific secret information needed for Brython-Server 173 | to operate properly. Do not archive this information (or this configuration file) with 174 | your project source code. 175 | 176 | In order to support Github integration, you will have to register a Github app. The callback 177 | URL for Brython-Server is its root URL `/`. Once registered, Github will provide you with 178 | a secret token and client ID. See the [Flask session documentation](http://flask.pocoo.org/docs/0.10/quickstart/) 179 | for information on creating a Flask session secret key. 180 | 181 | With this file in place, you can start the server with: 182 | 183 | sudo start brython-server 184 | 185 | #### Use with NGINX 186 | 187 | The gunicorn server can be the sole web server for this application, or you can use it with NGINX 188 | via proxy. Configure your NGINX server thus: 189 | 190 | location /brython-server/ { # <<< put the desired URL path here 191 | proxy_redirect off; 192 | proxy_set_header Host $host; 193 | proxy_set_header X-Real-IP $remote_addr; 194 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 195 | proxy_set_header X-Forwarded-Host $server_name; 196 | proxy_set_header X-Scheme $scheme; 197 | proxy_set_header X-Script-Name /brython-server; 198 | proxy_pass http://127.0.0.1:/; 199 | } 200 | 201 | In this case, configure gunicorn to operate with ip address: `127.0.0.1` and the same port 202 | shown in the NGINX configuration. 203 | 204 | -------------------------------------------------------------------------------- /docs/Functionality.md: -------------------------------------------------------------------------------- 1 | #Brython-Server Functional Specification 2 | 3 | ##Introduction 4 | 5 | The Brython-Server project addresses difficulties encountered when teaching Python (and especially Python 3) to high-school classes. Experience has shown that student engagement is maximized when the programming assignments and projects have a *graphical* component. Unfortunately, as of 2014/2015, installing Python 3 on multiple platforms, with a full graphical environment (such as Pygame) is extremely challenging. 6 | 7 | In addition, with the advent of cloud-based platforms, and many students carrying Google Chromebooks, the requirement of having a native IDE installed on every possible computing platform is becoming an unattainable ideal. 8 | 9 | The Brython-Server project addresses these issues by providing students with: 10 | 11 | 1. A browser-based environment ([Brython](http://brython.info)) for executing Python 3 code. 12 | 2. Support for a graphical programming environment using 3rd party Javascript components 13 | (e.g. [Pixi.js](www.pixijs.com)) for in-browser graphics. 14 | 3. Support for native development using Python 3 and Pygame and/or Pyglet. 15 | 4. Support for executing code online, from sources maintained in Github, for the purpose of 16 | evaluating student work and sharing student work publically. 17 | 5. Support for editing/revising Github repository code online, and committing directly to Github. 18 | 19 | ##Functionality - Overview 20 | 21 | A Brython-Server instance will consist of a web site where the user can enter the URL of a Github repository 22 | (consisting of Python 3 sources) or individual source file within a repository. Brython-Server will retrieve 23 | the source file (and any included files) from Github and return a web page with embedded links to the Brython 24 | distribution and the user's Python source. The web page will execute the user's code in the browser, interact 25 | via console output and input, and (optionally) create dynamic and interactive imagery via HTML5 canvas. 26 | 27 | The main Brython-Server page (e.g. `http://hsscp.org/brython-server`) presents the following elements to the visitor: 28 | 29 | 1. Text box with placeholder text suggesting the user paste a Github repository URL. 30 | 2. Text box for editing Python source code. 31 | 3. Button for loading source from Github. 32 | 4. Button for executing source. 33 | 5. Button for sharing the project as a hyperlink. 34 | 6. Button for linking to a Github user and signing in. 35 | 7. Button for unlinking from the Github user. 36 | 37 | The following sections describe these and other modes of operation in greater detail. 38 | 39 | ###Use Case: Interactive Edit and Execute 40 | 41 | Upon arriving at the main Brython-Server page, the user may begin editing code immediately. Code may be executed 42 | at any time by pressing the execute, ">" button. Output will be displayed in separate console I/O text box. It is a goal 43 | of the project to support the ACE online code editor for this functionality. No support is envisioned for loading or 44 | saving source code to the user's local machine. 45 | 46 | ###Use Case: Github 47 | 48 | Suppose the user has a Github repository with one or more Python 3 source files in it. The user may visit the Brython-Server main page and paste the URL of the Github repository page in the text box. Brython-Server will retrieve the list of top-level files from the repository and invoke the Brython interpreter on one of them, using this priority scheme: 49 | 50 | 1. Execute the only file with a .py extension. 51 | 2. Execute the only file named `__main__.py`. 52 | 3. Execute the only file named `main.py`. 53 | 54 | If the user pastes the Github URL for a specific file in the repository, then Brython-Server will retrieve that file 55 | as the primary file to execute. Other files in the repository may be named as imports. 56 | 57 | Once execution of the chosen python source file is complete, the code may be re-loaded by pressing 58 | the "load" button again. Code may be executed as many times as desired, using the ">" button. 59 | 60 | With code loaded, the page shall display at least the following elements: 61 | 62 | 1. Text indicating what file is executing (e.g. Executing __main__.py from https://github.com/tiggerntatie/brython-student-test) 63 | 2. Console input/output (input from console is implemented as a default browsesr prompt/popup dialog). 64 | 3. Graphics canvas (if the application is graphical). 65 | 4. Error output (included in the console display). 66 | 67 | The user may reload and execute the code as often as desired. 68 | 69 | It is expected that the typical user will **not** use Github as a development IDE. While Github has excellent online code editing support, the turnaround time from edit to execution is relatively slow. 70 | 71 | This approach will work for **public** Github repositories. If the user wishes to execute from **private** repositories, then the user would have to grant Brython-Server permission to access their repositories. 72 | 73 | With access granted (via the "LOG IN" 74 | button), the user may edit the loaded source code directly, execute, and, if desired, **commit the code back to Github** (via 75 | the "COMMIT" button). The "COMMIT" button is only available when the user has logged in to Github. Commit message will default 76 | to indicating a commit from Brython Server, with the date and time of the commit. 77 | 78 | ####Sharing Projects 79 | 80 | When working with a Github repository, the Brython-Server page will include a "SHARE" button that links back to 81 | Brython-Server with URL parameters to specify the user, repository and source file. For example: 82 | 83 | http://hhscp.org/brython-server/?user=tiggerntatie&repo=brython-student-test&name=loopdemo.py 84 | 85 | This link may be shared via e-mail, etc., to allow anyone with a modern browser to execute the Python source file 86 | using Brython-Server. 87 | 88 | ##Support for Python Features 89 | 90 | The Python 3 feature set supported will track the features of the latest released version of Brython. 91 | 92 | The main Python 3 source file may `import` the same standard libraries that Brython supports, in addition to any other Python 3 modules provided by the user at the same level as the main file. 93 | 94 | Goal: Resource files included at the same level are also available on a read-only basis. 95 | 96 | Goal: Some support for read and write file i/o on the client side; it should be impossible to write data 97 | on the server! 98 | 99 | ##Graphics Support 100 | 101 | A key requirement of this system is support for graphics and user keyboard/mouse interaction with executing python code, similar to the functionality possible with Pygame. Brython supports the same basic functionality for working with the HTML 5 Canvas that is present in Javascript. In addition, Brython supports interaction with 3rd party graphics libraries (e.g. [Pixi.js](www.pixijs.com)). Unfortunately, the APIs available in the browser are very different from what is available in native desktop Python installations. Consequently, this project will implement a simple graphics abstraction layer that will sit between the underlying graphics APIs and the user code. The abstraction layer will be in the form of a single Python 3 module that the user may import and use in either the Brython-Server environment, or a desktop environment. 102 | 103 | The abstraction module/layer will be called **actorgraphics** (notional name). 104 | 105 | ###actorgraphics 106 | 107 | The actorgraphics module will be a single Python 3 module, which will autodect its environment and provide simple graphics and UI services from an available underlying graphics/UI library. In the web environment, this could be provided, in part, for example by [paper.js](paperjs.org) or [Pixi.js](www.pixijs.com). In the desktop environment this could be provided at minimum by Tkinter (required), Pyglet or Pygame (goal). **Minimum** functionality implemented by actorgraphics include: 108 | 109 | 1. Event driven architecture. 110 | 2. Timed callbacks. 111 | 3. Keyboard, mouse movement and button events handled at application level. 112 | 4. Single window application. 113 | 5. Window background color or bitmap. 114 | 6. Sprite "actor" class. 115 | 7. Built in sprite types support bitmap, rectangle, ellipse and polygon. 116 | 8. Built in sprite attributes (set/get) include visibility, color, bitmap, position and rotation. 117 | 9. Built in sprite methods include collision detection. 118 | 10. Goal: sound file playback. 119 | 11. Goal: turtle graphics functions: pen functionality associated with sprites. 120 | 121 | The actorgraphics module is not automatically available to programs executed by Brython-Server. It may be available to the user as a download from the Brython-Server landing page. 122 | 123 | User programs that depend on the actorgraphics module may be executed in either the desktop or Brython-Server environment, and will exhibit similar behavior in each. If the user program is executed in a so-called headless environment where no underlying graphics support is available, then it will execute normally, but will not be able to respond to mouse events. It *may* be possible to respond to keypress events in any environment. Supporting operation in a headless environment will permit some unit testing of user code that depends on graphics. 124 | 125 | ##Appendix: Use in Educational Setting 126 | 127 | Although outside the scope of this functional specification, it seems worthwhile to briefly discuss how Brython-Server might be integrated in to the educational setting as part of an introductory course on computer programming. 128 | 129 | We envision using Brython-Server in concert with Github. The instructor would create a repository of assignments, or a collection of assignment repositories,which each student would clone in to her own account. With each assignment there is a corresponding automated test defined, which the student is expected to run to validate their work. When each assignment is complete, the student would make a pull request to the assignment repository, whereupon the instructor would run the requisite test, evaluate the code, and provide feedback and a formal assessment (grade). 130 | 131 | At any time, teacher or student may use Brython-Server to execute the submitted code, provided the Github repository is public. Ability to access private repositories is a goal of this project. It is assumed that the instructor is *not* limited to cloud-based devices and will be able to evaluate code using a native Python 3 interpreter and graphics installation. 132 | 133 | With Brython-Server the student also has the option of sharing work with friends, by sharing the Brython-Server Github URL for the repository. Again, the repository must be public for this to work. 134 | 135 | Up to this point, nothing in this appendix *depends* on the existence of Brython-Server. However, with increasing use of Chromebooks in the classroom, many students will find it inconvenient to install native Python 3 and graphics libraries on their own devices. For these students, editing code and commiting to Github is a viable alternative. 136 | 137 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.1.2 2 | Jinja2== 3.1.2 3 | MarkupSafe==2.1.1 4 | Werkzeug==2.1.2 5 | gunicorn==19.9.0 6 | itsdangerous==2.1.2 7 | brython==3.11.2 8 | ggame==1.1.3 9 | pillow==10.2.0 10 | black==23.12.1 11 | twine==4.0.2 12 | wheel 13 | pylint==3.0.3 14 | pynose==1.4.8 -------------------------------------------------------------------------------- /scripts/buildrelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Note: you must pip install brython in accordance with requirements.txt 3 | # Note: execute from brython-server 4 | 5 | source env/bin/activate 6 | pip install -r requirements.txt 7 | mkdir -p brythonserver/static/brython 8 | pushd brythonserver/static/brython 9 | brython-cli install 10 | popd 11 | rm dist/* 12 | python3 setup.py sdist 13 | python3 -m pip wheel --no-index --no-build-isolation --no-deps --wheel-dir dist dist/*.tar.gz 14 | 15 | -------------------------------------------------------------------------------- /scripts/run_js_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | standard brythonserver/static/bs.js || { echo 'javascript lint failed (standard)' ; exit 1;} 3 | -------------------------------------------------------------------------------- /scripts/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source env/bin/activate 3 | black --check brythonserver || { echo 'black failed (use black first)' ; exit 1; } 4 | python3 -m pylint -r n brythonserver || { echo 'pylint failed' ; exit 1; } -------------------------------------------------------------------------------- /scripts/testuploadrelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # >>>!!!>>> test with: pip install --extra-index-url https://test.pypi.org/simple/ brython-server 4 | python3.11 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* -------------------------------------------------------------------------------- /scripts/uploadrelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 -m twine upload dist/* -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Brython Server package setup for pypi 3 | """ 4 | 5 | import setuptools 6 | from brythonserver.__version__ import VERSION 7 | 8 | with open("README.md", "r") as fh: 9 | long_description = fh.read() 10 | 11 | setuptools.setup( 12 | name="brython-server", 13 | include_package_data=True, 14 | version=VERSION, 15 | author="Eric Dennison", 16 | author_email="ericd@netdenizen.com", 17 | description="Simple web based Python 3 IDE with Brython and Github integration", 18 | long_description=long_description, 19 | long_description_content_type="text/markdown", 20 | url="https://github.com/tiggerntatie/brython-server", 21 | packages=setuptools.find_packages(), 22 | python_requires='>=3.11', 23 | install_requires=[ 24 | 'gunicorn==19.9.0', 25 | 'Flask==2.1.2', 26 | 'werkzeug==2.1.2', 27 | 'pillow==10.2.0', 28 | 'ggame==1.1.3', 29 | ], 30 | classifiers=[ 31 | "Programming Language :: Python :: 3", 32 | "License :: OSI Approved :: MIT License", 33 | "Operating System :: OS Independent", 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Brython-Server debugging server 3 | Author: E Dennison 4 | 5 | Execute with proper environment variables set. 6 | Not for production use (instead, e.g.: gunicorn -b 0.0.0.0:3000 -w 4 brythonserver.main:APP) 7 | (or: gunicorn -b 127.0.0.1:8003 -w 4 brythonserver.main:APP) 8 | """ 9 | 10 | 11 | if __name__ == "__main__": 12 | import os 13 | from random import randint 14 | import brythonserver.__version__ 15 | brythonserver.__version__.VERSION = str(randint(0, 100000)) 16 | import brythonserver.main 17 | brythonserver.main.APP.run(host=os.getenv("IP", "0.0.0.0"), port=int(os.getenv("PORT", "8080"))) 18 | --------------------------------------------------------------------------------