├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── django_node ├── __init__.py ├── base_service.py ├── exceptions.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── install_package_dependencies.py │ │ ├── node_server_config.py │ │ ├── start_node_server.py │ │ └── uninstall_package_dependencies.py ├── models.py ├── node.py ├── node_server.py ├── npm.py ├── package.json ├── package_dependent.py ├── server.py ├── services │ ├── __init__.py │ └── echo.js ├── settings.py └── utils.py ├── docs ├── js_services.md ├── management_commands.md ├── node.md ├── node_server.md ├── npm.md └── settings.md ├── example ├── README.md ├── djangosite │ ├── __init__.py │ ├── services.py │ ├── services │ │ └── hello_world.js │ ├── settings.py │ ├── urls.py │ └── views.py ├── manage.py └── requirements.txt ├── requirements.txt ├── runtests.py ├── setup.py └── tests ├── __init__.py ├── package.json ├── services.py ├── services ├── error.js └── timeout.js ├── settings.py ├── tests.py └── utils.py /.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 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | node_modules 57 | example/db.sqlite3 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | 7 | env: 8 | - DJANGO_VERSION=1.6.10 9 | - DJANGO_VERSION=1.7.5 10 | - DJANGO_VERSION=1.8a1 11 | 12 | matrix: 13 | exclude: 14 | - python: "3.4" 15 | env: DJANGO_VERSION=1.6.10 16 | 17 | install: 18 | - "npm install -g npm" 19 | - "pip install Django==$DJANGO_VERSION" 20 | - "pip install -r requirements.txt" 21 | 22 | script: python runtests.py 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ### 2.2.0 (26/01/2015) 5 | 6 | - Bugfix to correct the DJANGO_NODE['PATH_TO_NPM'] setting. 7 | 8 | ### 2.1.0 (24/12/2014) 9 | 10 | - Adding support for node.run to temporarily toggle NODE_ENV to production. 11 | 12 | ### 2.0.1 (14/12/2014) 13 | 14 | - Improving the clarity of the terminal output generated by `django_node.npm.install`. 15 | 16 | ### 2.0.0 (12/12/2014) 17 | 18 | - API change for `django_node.npm.install` 19 | - Extra arguments are now accepted as `*args`, rather than as a tuple. `silent` must now be an explicit keyword argument. 20 | 21 | ### 1.0.0 (12/12/2014) 22 | 23 | - Removed the `RAISE_ON_MISSING_DEPENDENCIES` and `RAISE_ON_OUTDATED_DEPENDENCIES` settings. 24 | 25 | ### 0.2.0 (7/12/2014) 26 | 27 | - Most of the functions are now exposed from `django_node.node` and `django_node.npm`. 28 | - The Node.js API is now composed of... 29 | - `django_node.node.is_installed` 30 | - `django_node.node.version` 31 | - `django_node.node.version_raw` 32 | - `django_node.node.run` 33 | - `django_node.node.ensure_installed` 34 | - `django_node.node.ensure_version_gte` 35 | - The NPM API is now composed of... 36 | - `django_node.npm.is_installed` 37 | - `django_node.npm.version` 38 | - `django_node.npm.version_raw` 39 | - `django_node.npm.run` 40 | - `django_node.npm.ensure_installed` 41 | - `django_node.npm.ensure_version_gte` 42 | - `django_node.npm.install` 43 | 44 | 45 | ### 0.1.0 (3/12/2014) 46 | 47 | - Initial release 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mark Finger 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 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | include LICENSE.txt 3 | include README.md 4 | include requirements.txt 5 | include runtests.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Deprecated 2 | ---------- 3 | 4 | `django-node` has been deprecated. The core has been split into the following packages: 5 | 6 | - [python-js-host](https://github.com/markfinger/python-js-host) 7 | - [python-nodejs](https://github.com/markfinger/python-nodejs) 8 | - [python-npm](https://github.com/markfinger/python-npm) 9 | 10 | 11 | django-node 12 | =========== 13 | 14 | [![Build Status](https://travis-ci.org/markfinger/django-node.svg?branch=master)](https://travis-ci.org/markfinger/django-node) 15 | 16 | django-node provides a way of hosting persistent JS services which are easily accessible from a django application. 17 | 18 | Using services opens up a number of possibilites which are difficult or impossible to perform in a typical Django application, for example: 19 | - Server-side rendering of JS (Isomorphic JavaScript) 20 | - Background processes, such as file watchers 21 | - WebSockets 22 | 23 | Behind the scenes, django-node will connect to either a pre-existing instance of [django-node-server](https://github.com/markfinger/django-node-server) or will create an instance as a subprocess. 24 | 25 | Additionally, django-node provides a number of bindings and utilites to assist with integrating Node and NPM into a Django application. 26 | 27 | **Please note** that django-node is a work in progress. In particular, the JS services API is prone to change as issues are identified and fixed. 28 | 29 | 30 | Basic documentation 31 | ------------------- 32 | 33 | - [Basic usage](#basic-usage) 34 | - [Installation](#installation) 35 | - [Examples](#examples) 36 | - [Running the tests](#running-the-tests) 37 | 38 | 39 | API documentation 40 | ----------------- 41 | 42 | - [JS services](docs/js_services.md) 43 | - [Managment commands](docs/management_commands.md) 44 | - [NodeServer](docs/node_server.md) 45 | - [Node](docs/node.md) 46 | - [NPM](docs/npm.md) 47 | - [Settings](docs/settings.md) 48 | 49 | 50 | Basic usage 51 | ----------- 52 | 53 | To create a JS service, define a function and export it as a module. 54 | 55 | ```javascript 56 | // my_app/hello_world_service.js 57 | 58 | var service = function(request, response) { 59 | var name = request.query.name; 60 | response.send( 61 | 'Hello, ' + name + '!'; 62 | ); 63 | }; 64 | 65 | module.exports = service; 66 | ``` 67 | 68 | Create a python interface to your service by inheriting from `django_node.base_service.BaseService`. 69 | 70 | ```python 71 | # my_app/services.py 72 | 73 | import os 74 | from django_node.base_service import BaseService 75 | 76 | class HelloWorldService(BaseService): 77 | # An absolute path to a file containing the JS service 78 | path_to_source = os.path.join(os.path.dirname(__file__), 'hello_world_service.js') 79 | 80 | def greet(self, name): 81 | response = self.send(name=name) 82 | return response.text 83 | ``` 84 | 85 | Configure django-node to load your service by adding the service's module as a 86 | dotstring to the `DJANGO_NODE['SERVICES']` setting. 87 | 88 | ```python 89 | # in settings.py 90 | 91 | DJANGO_NODE = { 92 | 'SERVICES': ( 93 | 'my_app.services', 94 | ), 95 | } 96 | ``` 97 | 98 | During django-node's initialisation, the modules defined in `DJANGO_NODE['SERVICES']` are 99 | imported and all of the classes contained which inherit from `django_node.base_service.BaseService` will be 100 | loaded into a [django-node-server](https://github.com/markfinger/django-node-server) instance. 101 | 102 | You can now send a request to your service and receive a response. 103 | 104 | ```python 105 | hello_world_service = HelloWorldService() 106 | 107 | greeting = hello_world_service.greet('World') 108 | 109 | print(greeting) # prints 'Hello, World!' 110 | ``` 111 | 112 | Besides JS services, django-node also provides a number of bindings and utilities for 113 | interacting with Node and NPM. 114 | 115 | ```python 116 | import os 117 | from django_node import node, npm 118 | 119 | # Run a particular file with Node.js 120 | stderr, stdout = node.run('/path/to/some/file.js', '--some-argument', 'some_value') 121 | 122 | # Call `npm install` within the current file's directory 123 | stderr, stdout = npm.install(os.path.dirname(__file__)) 124 | ``` 125 | 126 | 127 | Installation 128 | ------------ 129 | 130 | ```bash 131 | pip install django-node 132 | ``` 133 | 134 | Add `'django_node'` to your `INSTALLED_APPS` 135 | 136 | ```python 137 | INSTALLED_APPS = ( 138 | # ... 139 | 'django_node', 140 | ) 141 | ``` 142 | 143 | 144 | Examples 145 | -------- 146 | 147 | The following apps make heavy use of django-node and illustrate how to perform non-trivial tasks. 148 | 149 | - [django-react](http://github.com/markfinger/django-react) 150 | - [django-webpack](http://github.com/markfinger/django-webpack) 151 | 152 | 153 | Running the tests 154 | ----------------- 155 | 156 | ```bash 157 | mkvirtualenv django-node 158 | pip install -r requirements.txt 159 | python runtests.py 160 | ``` 161 | -------------------------------------------------------------------------------- /django_node/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markfinger/django-node/a2f56bf027fd3c4cbc6a0213881922a50acae1d6/django_node/__init__.py -------------------------------------------------------------------------------- /django_node/base_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import warnings 3 | import json 4 | from django.utils import six 5 | if six.PY2: 6 | from urlparse import urljoin 7 | from urlparse import urlparse 8 | elif six.PY3: 9 | from urllib.parse import urlparse 10 | from urllib.parse import urljoin 11 | from .exceptions import ServiceSourceDoesNotExist, MalformedServiceName, ServerConfigMissingService, NodeServiceError 12 | from .settings import SERVICES, SERVICE_TIMEOUT 13 | from .utils import convert_html_to_plain_text 14 | from .package_dependent import PackageDependent 15 | 16 | 17 | class BaseService(PackageDependent): 18 | path_to_source = None 19 | name = None 20 | server = None 21 | timeout = SERVICE_TIMEOUT 22 | 23 | def __init__(self): 24 | self.warn_if_not_configured() 25 | 26 | @classmethod 27 | def warn_if_not_configured(cls): 28 | if cls.__module__ not in SERVICES: 29 | service_warning = ( 30 | '{class_name} has been instantiated, but the module "{module}" is missing from ' 31 | 'the SERVICES setting.' 32 | ).format(class_name=cls, module=cls.__module__) 33 | warnings.warn(service_warning) 34 | 35 | @classmethod 36 | def validate(cls): 37 | path_to_source = cls.get_path_to_source() 38 | if not path_to_source or not os.path.exists(path_to_source): 39 | raise ServiceSourceDoesNotExist(cls.get_path_to_source()) 40 | 41 | # Ensure that the name is a relative url starting with `/` 42 | name = cls.get_name() 43 | if urlparse(name).netloc or not name.startswith('/') or name == '/': 44 | raise MalformedServiceName(name) 45 | 46 | @classmethod 47 | def get_name(cls): 48 | if cls.name is not None: 49 | return cls.name 50 | 51 | python_path = '{module_path}.{class_name}'.format( 52 | module_path=cls.__module__, 53 | class_name=cls.__name__, 54 | ) 55 | 56 | cls.name = urljoin('/', python_path.replace('.', '/')) 57 | 58 | return cls.name 59 | 60 | @classmethod 61 | def get_path_to_source(cls): 62 | return cls.path_to_source 63 | 64 | def get_server(self): 65 | if self.server is not None: 66 | return self.server 67 | 68 | from .server import server 69 | self.server = server 70 | 71 | return self.server 72 | 73 | def handle_response(self, response): 74 | if response.status_code != 200: 75 | error_message = convert_html_to_plain_text(response.text) 76 | message = 'Error at {name}: {error_message}' 77 | raise NodeServiceError(message.format( 78 | name=self.get_name(), 79 | error_message=error_message, 80 | )) 81 | return response 82 | 83 | def get_json_decoder(self): 84 | return None 85 | 86 | def generate_cache_key(self, serialized_data, data): 87 | return None 88 | 89 | def ensure_loaded(self): 90 | if self.__class__ not in self.get_server().services: 91 | raise ServerConfigMissingService(self.__class__) 92 | 93 | def send(self, **kwargs): 94 | self.ensure_loaded() 95 | 96 | data = kwargs 97 | serialized_data = json.dumps(data, cls=self.get_json_decoder()) 98 | 99 | response = self.server.send_request_to_service( 100 | self.get_name(), 101 | timeout=self.timeout, 102 | data={ 103 | 'cache_key': self.generate_cache_key(serialized_data, data), 104 | 'data': serialized_data 105 | } 106 | ) 107 | 108 | return self.handle_response(response) -------------------------------------------------------------------------------- /django_node/exceptions.py: -------------------------------------------------------------------------------- 1 | class ErrorInterrogatingEnvironment(Exception): 2 | pass 3 | 4 | 5 | class MissingDependency(Exception): 6 | pass 7 | 8 | 9 | class OutdatedDependency(Exception): 10 | pass 11 | 12 | 13 | class MalformedVersionInput(Exception): 14 | pass 15 | 16 | 17 | class NpmInstallArgumentsError(Exception): 18 | pass 19 | 20 | 21 | class DynamicImportError(Exception): 22 | pass 23 | 24 | 25 | class NodeServerStartError(Exception): 26 | pass 27 | 28 | 29 | class NodeServerAddressInUseError(Exception): 30 | pass 31 | 32 | 33 | class NodeServerConnectionError(Exception): 34 | pass 35 | 36 | 37 | class NodeServerTimeoutError(Exception): 38 | pass 39 | 40 | 41 | class NodeServiceError(Exception): 42 | pass 43 | 44 | 45 | class ServiceSourceDoesNotExist(Exception): 46 | pass 47 | 48 | 49 | class MalformedServiceName(Exception): 50 | pass 51 | 52 | 53 | class ServerConfigMissingService(Exception): 54 | pass 55 | 56 | 57 | class MalformedServiceConfig(Exception): 58 | pass 59 | 60 | 61 | class ModuleDoesNotContainAnyServices(Exception): 62 | pass -------------------------------------------------------------------------------- /django_node/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markfinger/django-node/a2f56bf027fd3c4cbc6a0213881922a50acae1d6/django_node/management/__init__.py -------------------------------------------------------------------------------- /django_node/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markfinger/django-node/a2f56bf027fd3c4cbc6a0213881922a50acae1d6/django_node/management/commands/__init__.py -------------------------------------------------------------------------------- /django_node/management/commands/install_package_dependencies.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | 4 | class Command(BaseCommand): 5 | def handle(self, *args, **options): 6 | from django_node.settings import PACKAGE_DEPENDENCIES 7 | if PACKAGE_DEPENDENCIES: 8 | print('Installing package dependencies in {package_dependencies}'.format( 9 | package_dependencies=PACKAGE_DEPENDENCIES 10 | )) 11 | from django_node.package_dependent import install_configured_package_dependencies 12 | install_configured_package_dependencies() 13 | 14 | from django_node.server import server 15 | for dependent in (server,) + server.services: 16 | print('Installing package dependencies for {dependent}'.format(dependent=dependent)) 17 | dependent.install_dependencies() -------------------------------------------------------------------------------- /django_node/management/commands/node_server_config.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | 4 | class Command(BaseCommand): 5 | def handle(self, *args, **options): 6 | from django_node.server import server 7 | print(server.get_serialised_config()) -------------------------------------------------------------------------------- /django_node/management/commands/start_node_server.py: -------------------------------------------------------------------------------- 1 | from optparse import make_option 2 | from django.core.management.base import BaseCommand 3 | 4 | 5 | class Command(BaseCommand): 6 | option_list = ( 7 | make_option( 8 | '-d', '--debug', 9 | dest='debug', 10 | action='store_const', 11 | const=True, 12 | help='Start the server with a debugger', 13 | ), 14 | ) + BaseCommand.option_list 15 | 16 | def handle(self, *args, **options): 17 | from django_node.server import server 18 | 19 | print('Starting server...\n') 20 | 21 | server.start( 22 | debug=options['debug'], 23 | use_existing_process=False, 24 | blocking=True, 25 | ) 26 | -------------------------------------------------------------------------------- /django_node/management/commands/uninstall_package_dependencies.py: -------------------------------------------------------------------------------- 1 | from django_node import settings 2 | settings.INSTALL_PACKAGE_DEPENDENCIES_DURING_RUNTIME = False 3 | 4 | from django.core.management.base import BaseCommand 5 | 6 | 7 | class Command(BaseCommand): 8 | def handle(self, *args, **options): 9 | from django_node.settings import PACKAGE_DEPENDENCIES 10 | if PACKAGE_DEPENDENCIES: 11 | print('Uninstalling package dependencies in {package_dependencies}'.format( 12 | package_dependencies=PACKAGE_DEPENDENCIES 13 | )) 14 | from django_node.package_dependent import uninstall_configured_package_dependencies 15 | uninstall_configured_package_dependencies() 16 | 17 | from django_node.server import server 18 | for dependent in (server,) + server.services: 19 | print('Uninstalling package dependencies for {dependent}'.format(dependent=dependent)) 20 | dependent.uninstall_dependencies() -------------------------------------------------------------------------------- /django_node/models.py: -------------------------------------------------------------------------------- 1 | from .settings import INSTALL_PACKAGE_DEPENDENCIES_DURING_RUNTIME, PACKAGE_DEPENDENCIES 2 | from .package_dependent import install_configured_package_dependencies 3 | 4 | if INSTALL_PACKAGE_DEPENDENCIES_DURING_RUNTIME and PACKAGE_DEPENDENCIES: 5 | install_configured_package_dependencies() -------------------------------------------------------------------------------- /django_node/node.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .settings import PATH_TO_NODE 3 | from .utils import ( 4 | node_installed, node_version_raw, raise_if_dependency_missing, NODE_NAME, node_version, 5 | raise_if_dependency_version_less_than, run_command, 6 | ) 7 | 8 | is_installed = node_installed 9 | version = node_version 10 | version_raw = node_version_raw 11 | 12 | 13 | def ensure_installed(): 14 | raise_if_dependency_missing(NODE_NAME) 15 | 16 | 17 | def ensure_version_gte(required_version): 18 | ensure_installed() 19 | raise_if_dependency_version_less_than(NODE_NAME, required_version) 20 | 21 | 22 | def run(*args, **kwargs): 23 | ensure_installed() 24 | 25 | production = kwargs.pop('production', None) 26 | if production: 27 | node_env = os.environ.get('NODE_ENV', None) 28 | os.environ['NODE_ENV'] = 'production' 29 | 30 | results = run_command( 31 | (PATH_TO_NODE,) + tuple(args) 32 | ) 33 | 34 | if production: 35 | if node_env is not None: 36 | os.environ['NODE_ENV'] = node_env 37 | else: 38 | del os.environ['NODE_ENV'] 39 | 40 | return results -------------------------------------------------------------------------------- /django_node/node_server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import atexit 4 | import json 5 | import subprocess 6 | import logging 7 | import tempfile 8 | import requests 9 | from requests.exceptions import ConnectionError, ReadTimeout, Timeout 10 | from django.utils import six 11 | if six.PY2: 12 | from urlparse import urljoin 13 | elif six.PY3: 14 | from urllib.parse import urljoin 15 | from .services import EchoService 16 | from .settings import ( 17 | PATH_TO_NODE, SERVER_PROTOCOL, SERVER_ADDRESS, SERVER_PORT, NODE_VERSION_REQUIRED, NPM_VERSION_REQUIRED, 18 | SERVICES, INSTALL_PACKAGE_DEPENDENCIES_DURING_RUNTIME 19 | ) 20 | from .exceptions import ( 21 | NodeServerConnectionError, NodeServerStartError, NodeServerAddressInUseError, NodeServerTimeoutError, 22 | MalformedServiceConfig 23 | ) 24 | from .utils import resolve_dependencies, discover_services 25 | from .package_dependent import PackageDependent 26 | 27 | 28 | class NodeServer(PackageDependent): 29 | """ 30 | A persistent Node server which sits alongside the python process 31 | and responds over HTTP 32 | """ 33 | 34 | protocol = SERVER_PROTOCOL 35 | address = SERVER_ADDRESS 36 | port = SERVER_PORT 37 | path_to_source = os.path.join(os.path.dirname(__file__), 'node_modules', 'django-node-server', 'index.js') 38 | package_dependencies = os.path.dirname(__file__) 39 | shutdown_on_exit = True 40 | is_running = False 41 | logger = logging.getLogger(__name__) 42 | echo_service = EchoService() 43 | services = (EchoService,) 44 | service_config = SERVICES 45 | process = None 46 | 47 | def __init__(self): 48 | resolve_dependencies( 49 | node_version_required=NODE_VERSION_REQUIRED, 50 | npm_version_required=NPM_VERSION_REQUIRED, 51 | ) 52 | if not isinstance(self.service_config, tuple): 53 | raise MalformedServiceConfig( 54 | 'DJANGO_NODE[\'SERVICES\'] setting must be a tuple. Found "{setting}"'.format(setting=SERVICES) 55 | ) 56 | services = discover_services(self.service_config) 57 | if services: 58 | self.services += services 59 | if INSTALL_PACKAGE_DEPENDENCIES_DURING_RUNTIME: 60 | for dependent in (self,) + self.services: 61 | dependent.install_dependencies() 62 | 63 | def get_config(self): 64 | services = () 65 | for service in self.services: 66 | services += ({ 67 | 'name': service.get_name(), 68 | 'path_to_source': service.get_path_to_source(), 69 | },) 70 | return { 71 | 'address': self.address, 72 | 'port': self.port, 73 | 'services': services, 74 | 'startup_output': self.get_startup_output(), 75 | } 76 | 77 | def get_serialised_config(self): 78 | return json.dumps(self.get_config()) 79 | 80 | def start(self, debug=None, use_existing_process=None, blocking=None): 81 | if debug is None: 82 | debug = False 83 | if use_existing_process is None: 84 | use_existing_process = True 85 | if blocking is None: 86 | blocking = False 87 | 88 | if debug: 89 | use_existing_process = False 90 | blocking = True 91 | 92 | if use_existing_process and self.test(): 93 | self.is_running = True 94 | return 95 | 96 | if not use_existing_process and self.test(): 97 | raise NodeServerAddressInUseError( 98 | 'A process is already listening at {server_url}'.format( 99 | server_url=self.get_server_url() 100 | ) 101 | ) 102 | 103 | # Ensure that the process is terminated if the python process stops 104 | if self.shutdown_on_exit: 105 | atexit.register(self.stop) 106 | 107 | with tempfile.NamedTemporaryFile() as config_file: 108 | config_file.write(six.b(self.get_serialised_config())) 109 | config_file.flush() 110 | 111 | cmd = (PATH_TO_NODE,) 112 | if debug: 113 | cmd += ('debug',) 114 | cmd += ( 115 | self.path_to_source, 116 | '--config', config_file.name, 117 | ) 118 | 119 | self.log('Starting process with {cmd}'.format(cmd=cmd)) 120 | 121 | if blocking: 122 | # Start the server in a blocking process 123 | subprocess.call(cmd) 124 | return 125 | 126 | # While rendering templates Django will silently ignore some types of exceptions, 127 | # so we need to intercept them and raise our own class of exception 128 | try: 129 | # TODO: set NODE_ENV. See `env` arg https://docs.python.org/2/library/subprocess.html#popen-constructor 130 | self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 131 | except (TypeError, AttributeError): 132 | msg = 'Failed to start server with {arguments}'.format(arguments=cmd) 133 | six.reraise(NodeServerStartError, NodeServerStartError(msg), sys.exc_info()[2]) 134 | 135 | # Block until the server is ready and pushes the expected output to stdout 136 | output = self.process.stdout.readline() 137 | output = output.decode('utf-8') 138 | 139 | if output.strip() != self.get_startup_output(): 140 | # Read in the rest of the error message 141 | output += self.process.stdout.read().decode('utf-8') 142 | if 'EADDRINUSE' in output: 143 | raise NodeServerAddressInUseError( 144 | ( 145 | 'Port "{port}" already in use. ' 146 | 'Try changing the DJANGO_NODE[\'SERVER_PORT\'] setting. ' 147 | '{output}' 148 | ).format( 149 | port=self.port, 150 | output=output, 151 | ) 152 | ) 153 | else: 154 | raise NodeServerStartError(output) 155 | 156 | self.is_running = True 157 | 158 | # Ensure that the server is running 159 | if not self.test(): 160 | self.stop() 161 | raise NodeServerStartError( 162 | 'Server does not appear to be running. Tried to test the server at "{echo_endpoint}"'.format( 163 | echo_endpoint=self.echo_service.get_name(), 164 | ) 165 | ) 166 | 167 | self.log('Started process') 168 | 169 | def get_startup_output(self): 170 | return 'Node server listening at {server_url}'.format( 171 | server_url=self.get_server_url() 172 | ) 173 | 174 | def stop(self): 175 | if self.process is not None and self.test(): 176 | self.process.terminate() 177 | self.log('Terminated process') 178 | self.is_running = False 179 | 180 | def get_server_url(self): 181 | if self.protocol and self.address and self.port: 182 | return '{protocol}://{address}:{port}'.format( 183 | protocol=self.protocol, 184 | address=self.address, 185 | port=self.port, 186 | ) 187 | 188 | def log(self, message): 189 | self.logger.info( 190 | '{server_name} [Address: {server_url}] {message}'.format( 191 | server_name=self.__class__.__name__, 192 | server_url=self.get_server_url(), 193 | message=message, 194 | ) 195 | ) 196 | 197 | def test(self): 198 | """ 199 | Returns a boolean indicating if the server is currently running 200 | """ 201 | return self.echo_service.test() 202 | 203 | def send_request_to_service(self, endpoint, timeout=None, data=None, ensure_started=None): 204 | if ensure_started is None: 205 | ensure_started = True 206 | 207 | if ensure_started and not self.is_running: 208 | self.start() 209 | 210 | self.log('Sending request to endpoint "{url}" with data "{data}"'.format( 211 | url=endpoint, 212 | data=data, 213 | )) 214 | 215 | absolute_url = urljoin(self.get_server_url(), endpoint) 216 | 217 | try: 218 | return requests.post(absolute_url, timeout=timeout, data=data) 219 | except ConnectionError as e: 220 | six.reraise(NodeServerConnectionError, NodeServerConnectionError(absolute_url, *e.args), sys.exc_info()[2]) 221 | except (ReadTimeout, Timeout) as e: 222 | six.reraise(NodeServerTimeoutError, NodeServerTimeoutError(absolute_url, *e.args), sys.exc_info()[2]) -------------------------------------------------------------------------------- /django_node/npm.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from .exceptions import NpmInstallArgumentsError 4 | from .settings import PATH_TO_NPM, NPM_INSTALL_PATH_TO_PYTHON, NPM_INSTALL_COMMAND 5 | from .utils import ( 6 | NPM_NAME, npm_installed, npm_version, npm_version_raw, raise_if_dependency_missing, 7 | raise_if_dependency_version_less_than, run_command 8 | ) 9 | 10 | is_installed = npm_installed 11 | version = npm_version 12 | version_raw = npm_version_raw 13 | 14 | 15 | def ensure_installed(): 16 | raise_if_dependency_missing(NPM_NAME) 17 | 18 | 19 | def ensure_version_gte(required_version): 20 | ensure_installed() 21 | raise_if_dependency_version_less_than(NPM_NAME, required_version) 22 | 23 | 24 | def run(*args): 25 | ensure_installed() 26 | return run_command((PATH_TO_NPM,) + tuple(args)) 27 | 28 | 29 | def install(target_dir): 30 | if not target_dir or not os.path.exists(target_dir) or not os.path.isdir(target_dir): 31 | raise NpmInstallArgumentsError( 32 | 'npm.install\'s `target_dir` parameter must be a string pointing to a directory. Received: {0}'.format( 33 | target_dir 34 | ) 35 | ) 36 | 37 | ensure_installed() 38 | 39 | command = (PATH_TO_NPM, NPM_INSTALL_COMMAND) 40 | 41 | if NPM_INSTALL_PATH_TO_PYTHON: 42 | command += ('--python={path_to_python}'.format(path_to_python=NPM_INSTALL_PATH_TO_PYTHON),) 43 | 44 | subprocess.call(command, cwd=target_dir) -------------------------------------------------------------------------------- /django_node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "django-node-server": "git://github.com/markfinger/django-node-server#a61702d296ea4eb5b5de6ff32e87006b5f15b53c" 4 | }, 5 | "private": true 6 | } 7 | -------------------------------------------------------------------------------- /django_node/package_dependent.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from .settings import PACKAGE_DEPENDENCIES 4 | from .utils import resolve_dependencies 5 | 6 | 7 | def install_dependencies(directory): 8 | resolve_dependencies(path_to_run_npm_install_in=directory) 9 | 10 | 11 | def uninstall_dependencies(directory): 12 | path_to_dependencies = os.path.join(directory, 'node_modules') 13 | if os.path.isdir(path_to_dependencies): 14 | shutil.rmtree(path_to_dependencies) 15 | 16 | 17 | def install_configured_package_dependencies(): 18 | for directory in PACKAGE_DEPENDENCIES: 19 | install_dependencies(directory) 20 | 21 | 22 | def uninstall_configured_package_dependencies(): 23 | for directory in PACKAGE_DEPENDENCIES: 24 | uninstall_dependencies(directory) 25 | 26 | 27 | class PackageDependent(object): 28 | # An optional path to a directory containing a package.json file 29 | package_dependencies = None 30 | 31 | @classmethod 32 | def install_dependencies(cls): 33 | if cls.package_dependencies is not None: 34 | install_dependencies(cls.package_dependencies) 35 | 36 | @classmethod 37 | def uninstall_dependencies(cls): 38 | if cls.package_dependencies is not None: 39 | uninstall_dependencies(cls.package_dependencies) -------------------------------------------------------------------------------- /django_node/server.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from .settings import SERVER 3 | from .utils import dynamic_import_attribute 4 | 5 | # Allow the server to be configurable 6 | server = dynamic_import_attribute(SERVER) 7 | if inspect.isclass(server): 8 | server = server() -------------------------------------------------------------------------------- /django_node/services/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from ..base_service import BaseService 4 | from ..exceptions import NodeServerConnectionError, NodeServerTimeoutError 5 | from ..settings import SERVER_TEST_TIMEOUT 6 | 7 | 8 | class EchoService(BaseService): 9 | """ 10 | A basic service which will return the value of the parameter 11 | `echo` as the response. 12 | 13 | Internally, NodeServer uses this service to test if the server 14 | is running as expected. 15 | """ 16 | 17 | path_to_source = os.path.join(os.path.dirname(__file__), 'echo.js') 18 | timeout = SERVER_TEST_TIMEOUT 19 | expected_output = '__NODE_SERVER_RUNNING__' 20 | 21 | @classmethod 22 | def warn_if_not_configured(cls): 23 | pass 24 | 25 | def test(self): 26 | self.ensure_loaded() 27 | 28 | try: 29 | response = self.get_server().send_request_to_service( 30 | self.get_name(), 31 | timeout=self.timeout, 32 | ensure_started=False, 33 | data={ 34 | 'data': json.dumps({'echo': self.expected_output}) 35 | } 36 | ) 37 | except (NodeServerConnectionError, NodeServerTimeoutError): 38 | return False 39 | 40 | if response.status_code != 200: 41 | return False 42 | 43 | return response.text == self.expected_output -------------------------------------------------------------------------------- /django_node/services/echo.js: -------------------------------------------------------------------------------- 1 | var service = function(data, response) { 2 | var echo = data.echo; 3 | 4 | if (!echo) { 5 | var err = 'Missing `echo` in data'; 6 | response.status(500).send(err); 7 | console.error(new Error(err)); 8 | return; 9 | } 10 | 11 | response.send(echo); 12 | }; 13 | 14 | module.exports = service; 15 | -------------------------------------------------------------------------------- /django_node/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from django.conf import settings 4 | 5 | setting_overrides = getattr(settings, 'DJANGO_NODE', {}) 6 | 7 | NODE_VERSION_REQUIRED = setting_overrides.get( 8 | 'NODE_VERSION_REQUIRED', 9 | (0, 10, 25) 10 | ) 11 | 12 | NPM_VERSION_REQUIRED = setting_overrides.get( 13 | 'NPM_VERSION_REQUIRED', 14 | (2, 0, 0) 15 | ) 16 | 17 | PATH_TO_NODE = setting_overrides.get( 18 | 'PATH_TO_NODE', 19 | 'node' 20 | ) 21 | 22 | NODE_VERSION_COMMAND = setting_overrides.get( 23 | 'NODE_VERSION_COMMAND', 24 | '--version', 25 | ) 26 | 27 | NODE_VERSION_FILTER = setting_overrides.get( 28 | 'NODE_VERSION_FILTER', 29 | lambda version: tuple(map(int, (version[1:] if version[0] == 'v' else version).split('.'))), 30 | ) 31 | 32 | PATH_TO_NPM = setting_overrides.get( 33 | 'PATH_TO_NPM', 34 | 'npm' 35 | ) 36 | 37 | NPM_VERSION_COMMAND = setting_overrides.get( 38 | 'NPM_VERSION_COMMAND', 39 | '--version', 40 | ) 41 | 42 | NPM_VERSION_FILTER = setting_overrides.get( 43 | 'NPM_VERSION_FILTER', 44 | lambda version: tuple(map(int, version.split('.'))), 45 | ) 46 | 47 | NPM_INSTALL_COMMAND = setting_overrides.get( 48 | 'NPM_INSTALL_COMMAND', 49 | 'install', 50 | ) 51 | 52 | NPM_INSTALL_PATH_TO_PYTHON = setting_overrides.get( 53 | 'NPM_INSTALL_PATH_TO_PYTHON', 54 | None, 55 | ) 56 | 57 | SERVER = setting_overrides.get( 58 | 'SERVER', 59 | 'django_node.node_server.NodeServer', 60 | ) 61 | 62 | SERVER_PROTOCOL = setting_overrides.get( 63 | 'SERVER_PROTOCOL', 64 | 'http', 65 | ) 66 | 67 | SERVER_ADDRESS = setting_overrides.get( 68 | 'SERVER_ADDRESS', 69 | '127.0.0.1', 70 | ) 71 | 72 | # Read in the server port from a `DJANGO_NODE_SERVER_PORT` environment variable 73 | SERVER_PORT = os.environ.get('DJANGO_NODE_SERVER_PORT', None) 74 | if SERVER_PORT is None: 75 | SERVER_PORT = setting_overrides.get( 76 | 'SERVER_PORT', 77 | '63578', 78 | ) 79 | 80 | SERVICES = setting_overrides.get( 81 | 'SERVICES', 82 | (), 83 | ) 84 | 85 | SERVICE_TIMEOUT = setting_overrides.get( 86 | 'SERVICE_TIMEOUT', 87 | 10.0, 88 | ) 89 | 90 | SERVER_TEST_TIMEOUT = setting_overrides.get( 91 | 'SERVER_TEST_TIMEOUT', 92 | 2.0, 93 | ) 94 | 95 | PACKAGE_DEPENDENCIES = setting_overrides.get( 96 | 'PACKAGE_DEPENDENCIES', 97 | () 98 | ) 99 | 100 | INSTALL_PACKAGE_DEPENDENCIES_DURING_RUNTIME = setting_overrides.get( 101 | 'INSTALL_PACKAGE_DEPENDENCIES_DURING_RUNTIME', 102 | True, 103 | ) 104 | 105 | if INSTALL_PACKAGE_DEPENDENCIES_DURING_RUNTIME: 106 | # Prevent dependencies from being installed during init if 107 | # either of the package dependency commands are being run 108 | for i, arg in enumerate(sys.argv): 109 | if ( 110 | arg.endswith('manage.py') and 111 | 'uninstall_package_dependencies' in sys.argv or 112 | 'install_package_dependencies' in sys.argv 113 | ): 114 | INSTALL_PACKAGE_DEPENDENCIES_DURING_RUNTIME = False 115 | break -------------------------------------------------------------------------------- /django_node/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | import tempfile 4 | import importlib 5 | import re 6 | import inspect 7 | from django.utils import six 8 | from .settings import ( 9 | PATH_TO_NODE, PATH_TO_NPM, NODE_VERSION_COMMAND, NODE_VERSION_FILTER, NPM_VERSION_COMMAND, NPM_VERSION_FILTER, 10 | ) 11 | from .exceptions import ( 12 | DynamicImportError, ErrorInterrogatingEnvironment, MalformedVersionInput, MissingDependency, OutdatedDependency, 13 | ModuleDoesNotContainAnyServices 14 | ) 15 | 16 | 17 | def run_command(cmd_to_run): 18 | """ 19 | Wrapper around subprocess that pipes the stderr and stdout from `cmd_to_run` 20 | to temporary files. Using the temporary files gets around subprocess.PIPE's 21 | issues with handling large buffers. 22 | 23 | Note: this command will block the python process until `cmd_to_run` has completed. 24 | 25 | Returns a tuple, containing the stderr and stdout as strings. 26 | """ 27 | with tempfile.TemporaryFile() as stdout_file, tempfile.TemporaryFile() as stderr_file: 28 | 29 | # Run the command 30 | popen = subprocess.Popen(cmd_to_run, stdout=stdout_file, stderr=stderr_file) 31 | popen.wait() 32 | 33 | stderr_file.seek(0) 34 | stdout_file.seek(0) 35 | 36 | stderr = stderr_file.read() 37 | stdout = stdout_file.read() 38 | 39 | if six.PY3: 40 | stderr = stderr.decode() 41 | stdout = stdout.decode() 42 | 43 | return stderr, stdout 44 | 45 | 46 | def _interrogate(cmd_to_run, version_filter): 47 | try: 48 | stderr, stdout = run_command(cmd_to_run) 49 | if stderr: 50 | raise ErrorInterrogatingEnvironment(stderr) 51 | installed = True 52 | version_raw = stdout.strip() 53 | except OSError: 54 | installed = False 55 | version_raw = None 56 | version = None 57 | if version_raw: 58 | version = version_filter(version_raw) 59 | return installed, version, version_raw, 60 | 61 | # Interrogate the system 62 | node_installed, node_version, node_version_raw = _interrogate( 63 | (PATH_TO_NODE, NODE_VERSION_COMMAND,), 64 | NODE_VERSION_FILTER, 65 | ) 66 | npm_installed, npm_version, npm_version_raw = _interrogate( 67 | (PATH_TO_NPM, NPM_VERSION_COMMAND,), 68 | NPM_VERSION_FILTER, 69 | ) 70 | 71 | NPM_NAME = 'NPM' 72 | NODE_NAME = 'Node.js' 73 | 74 | 75 | def _validate_version_iterable(version): 76 | if not isinstance(version, tuple): 77 | raise MalformedVersionInput( 78 | 'Versions must be tuples. Received {0}'.format(version) 79 | ) 80 | if len(version) < 3: 81 | raise MalformedVersionInput( 82 | 'Versions must have three numbers defined. Received {0}'.format(version) 83 | ) 84 | for number in version: 85 | if not isinstance(number, six.integer_types): 86 | raise MalformedVersionInput( 87 | 'Versions can only contain number. Received {0}'.format(version) 88 | ) 89 | 90 | 91 | def _check_if_version_is_outdated(current_version, required_version): 92 | _validate_version_iterable(required_version) 93 | for i, number_required in enumerate(required_version): 94 | if number_required > current_version[i]: 95 | return True 96 | elif number_required < current_version[i]: 97 | return False 98 | return current_version != required_version 99 | 100 | 101 | def _format_version(version): 102 | return '.'.join(map(six.text_type, version)) 103 | 104 | 105 | def raise_if_dependency_missing(application, required_version=None): 106 | if application == NPM_NAME: 107 | is_installed = npm_installed 108 | path = PATH_TO_NPM 109 | else: 110 | is_installed = node_installed 111 | path = PATH_TO_NODE 112 | if not is_installed: 113 | error = '{application} is not installed or cannot be found at path "{path}".'.format( 114 | application=application, 115 | path=path, 116 | ) 117 | if required_version: 118 | error += 'Version {required_version} or greater is required.'.format( 119 | required_version=_format_version(required_version) 120 | ) 121 | raise MissingDependency(error) 122 | 123 | 124 | def raise_if_dependency_version_less_than(application, required_version): 125 | if application == NPM_NAME: 126 | current_version = npm_version 127 | else: 128 | current_version = node_version 129 | if _check_if_version_is_outdated(current_version, required_version): 130 | raise OutdatedDependency( 131 | ( 132 | 'The installed {application} version is outdated. Version {current_version} is installed, but version ' 133 | '{required_version} is required. Please update {application}.' 134 | ).format( 135 | application=application, 136 | current_version=_format_version(current_version), 137 | required_version=_format_version(required_version), 138 | ) 139 | ) 140 | 141 | 142 | def dynamic_import_module(import_path): 143 | try: 144 | return importlib.import_module(import_path) 145 | except ImportError as e: 146 | msg = 'Failed to import "{import_path}"'.format( 147 | import_path=import_path 148 | ) 149 | six.reraise(DynamicImportError, DynamicImportError(msg, e.__class__.__name__, *e.args), sys.exc_info()[2]) 150 | 151 | 152 | def dynamic_import_attribute(import_path): 153 | module_import_path = '.'.join(import_path.split('.')[:-1]) 154 | imported_module = dynamic_import_module(module_import_path) 155 | try: 156 | return getattr(imported_module, import_path.split('.')[-1]) 157 | except AttributeError as e: 158 | msg = 'Failed to import "{import_path}"'.format( 159 | import_path=import_path 160 | ) 161 | six.reraise(DynamicImportError, DynamicImportError(msg, e.__class__.__name__, *e.args), sys.exc_info()[2]) 162 | 163 | html_entity_map = { 164 | ' ': ' ', 165 | '<': '<', 166 | '>': '>', 167 | '&': '&', 168 | '‘': '\'', 169 | '’': '\'', 170 | '";': '"', 171 | '“': '"', 172 | '”': '"', 173 | '–': '-', 174 | '—': '-', 175 | '´': '`', 176 | } 177 | 178 | 179 | # The various HTML decoding solutions that are proposed by 180 | # the python community seem to have issues where the unicode 181 | # characters are printed in encoded form. This solution is 182 | # not desirable, but works for django-node's purposes. 183 | def decode_html_entities(html): 184 | """ 185 | Decodes a limited set of HTML entities. 186 | """ 187 | if not html: 188 | return html 189 | 190 | for entity, char in six.iteritems(html_entity_map): 191 | html = html.replace(entity, char) 192 | 193 | return html 194 | 195 | 196 | def convert_html_to_plain_text(html): 197 | if not html: 198 | return html 199 | 200 | if six.PY2: 201 | html = html.decode('utf-8') 202 | 203 | html = decode_html_entities(html) 204 | # Replace HTML break rules with new lines 205 | html = html.replace('
', '\n') 206 | # Remove multiple spaces 207 | html = re.sub(' +', ' ', html) 208 | 209 | return html 210 | 211 | 212 | def resolve_dependencies(node_version_required=None, npm_version_required=None, path_to_run_npm_install_in=None): 213 | from . import node, npm # Avoid a circular import 214 | 215 | # Ensure that the external dependencies are met 216 | if node_version_required is not None: 217 | node.ensure_version_gte(node_version_required) 218 | if npm_version_required is not None: 219 | npm.ensure_version_gte(npm_version_required) 220 | 221 | # Ensure that the required packages have been installed 222 | if path_to_run_npm_install_in is not None: 223 | npm.install(path_to_run_npm_install_in) 224 | 225 | 226 | def discover_services(service_config): 227 | from .base_service import BaseService # Avoid a circular import 228 | 229 | services = () 230 | 231 | for import_path in service_config: 232 | module = dynamic_import_module(import_path) 233 | module_contains_services = False 234 | for attr_name in dir(module): 235 | service = getattr(module, attr_name) 236 | if ( 237 | inspect.isclass(service) and 238 | service is not BaseService and 239 | issubclass(service, BaseService) and 240 | service not in services and 241 | getattr(service, 'path_to_source', None) 242 | ): 243 | service.validate() 244 | services += (service,) 245 | module_contains_services = True 246 | if not module_contains_services: 247 | raise ModuleDoesNotContainAnyServices(import_path) 248 | 249 | return services -------------------------------------------------------------------------------- /docs/js_services.md: -------------------------------------------------------------------------------- 1 | JS services 2 | =========== 3 | 4 | JS services operate under a request/response pattern, whereby the python process sends 5 | a request to a service and will wait until the service has responded. 6 | 7 | **TODO** 8 | - sending data to services (kwargs to BaseService.send) 9 | - exporting the service as a CommonJS module 10 | - how to access node's ecosystem (call django_node.npm.install when defining your services) 11 | -------------------------------------------------------------------------------- /docs/management_commands.md: -------------------------------------------------------------------------------- 1 | Managment commands 2 | ================== 3 | 4 | - `./manage.py start_node_server` 5 | - `./manage.py start_node_server --debug` 6 | - `./manage.py node_server_config` 7 | 8 | TODO: improve docs 9 | -------------------------------------------------------------------------------- /docs/node.md: -------------------------------------------------------------------------------- 1 | Node 2 | ==== 3 | 4 | The `django_node.node` module provides utils for introspecting and calling [Node](http://nodejs.org/). 5 | 6 | **Methods** 7 | - [django_node.node.run()](#django_nodenoderun) 8 | - [django_node.node.ensure_installed()](#django_nodenodeensure_installed) 9 | - [django_node.node.ensure_version_gte()](#django_nodenodeensure_version_gte) 10 | 11 | **Attributes** 12 | - [django_node.node.is_installed](#django_nodenodeis_installed) 13 | - [django_node.node.version](#django_nodenodeversion) 14 | - [django_node.node.version_raw](#django_nodenodeversion_raw) 15 | 16 | 17 | ### django_node.node.run() 18 | 19 | Invokes Node with the arguments provided and return the resulting stderr and stdout. 20 | 21 | Accepts an optional keyword argument, `production`, which will ensure that the command is run 22 | with the `NODE_ENV` environment variable set to 'production'. 23 | 24 | ```python 25 | from django_node import node 26 | 27 | stderr, stdout = node.run('/path/to/some/file.js', '--some-argument') 28 | 29 | # With NODE_ENV set to production 30 | stderr, stdout = node.run('/path/to/some/file.js', '--some-argument', production=True) 31 | ``` 32 | 33 | ### django_node.node.ensure_installed() 34 | 35 | Raises an exception if Node is not installed. 36 | 37 | ### django_node.node.ensure_version_gte() 38 | 39 | Raises an exception if the installed version of Node is less than the version required. 40 | 41 | Arguments: 42 | 43 | `version_required`: a tuple containing the minimum version required. 44 | 45 | ```python 46 | from django_node import node 47 | 48 | node.ensure_version_gte((0, 10, 0,)) 49 | ``` 50 | 51 | ### django_node.node.is_installed 52 | 53 | A boolean indicating if Node is installed. 54 | 55 | ### django_node.node.version 56 | 57 | A tuple containing the version of Node installed. For example, `(0, 10, 33)` 58 | 59 | ### django_node.node.version_raw 60 | 61 | A string containing the raw version returned from Node. For example, `'v0.10.33'` 62 | -------------------------------------------------------------------------------- /docs/node_server.md: -------------------------------------------------------------------------------- 1 | Node Server 2 | =========== 3 | 4 | The `django_node.node_server` module provides an interface, `NodeServer`, for interacting 5 | with a JS service host such as 6 | [django-node-server](https://github.com/markfinger/django-node-server). In practice, 7 | you will rarely need to interact with the server itself, rather you should use 8 | django-node's [JS services](js_services.md). 9 | 10 | django-node opens up a singleton instance of `NodeServer` via the `django_node.server` 11 | module. 12 | 13 | ``` 14 | from django_node.server import server 15 | 16 | # Test if the server is running 17 | server.test() 18 | ``` 19 | 20 | If you wish to change the behaviour of the server singleton, you can change the 21 | `DJANGO_NODE['SERVER']` setting to a dotstring pointing to your server class, which 22 | will be imported at runtime. 23 | -------------------------------------------------------------------------------- /docs/npm.md: -------------------------------------------------------------------------------- 1 | NPM 2 | === 3 | 4 | The `django_node.npm` module provides utils for introspecting and calling [NPM](https://www.npmjs.com/). 5 | 6 | **Methods** 7 | - [django_node.npm.install()](#django_nodenpminstall) 8 | - [django_node.npm.run()](#django_nodenpmrun) 9 | - [django_node.npm.ensure_installed()](#django_nodenpmensure_installed) 10 | - [django_node.npm.ensure_version_gte()](#django_nodenpmensure_version_gte) 11 | 12 | **Attributes** 13 | - [django_node.npm.is_installed](#django_nodenpmis_installed) 14 | - [django_node.npm.version](#django_nodenpmversion) 15 | - [django_node.npm.version_raw](#django_nodenpmversion_raw) 16 | 17 | ### django_node.npm.install() 18 | 19 | Invokes NPM's install command in a specified directory. `install` blocks the python 20 | process and will direct npm's output to stdout. 21 | 22 | A typical use case for `install` is to ensure that your dependencies specified in 23 | a `package.json` file are installed during runtime. The first time that `install` is 24 | run in a directory, it will block until the dependencies are installed to the file 25 | system, successive calls will resolve almost immediately. Installing your dependencies 26 | at run time allows for projects and apps to easily maintain independent dependencies 27 | which are resolved on demand. 28 | 29 | Arguments: 30 | 31 | - `target_dir`: a string pointing to the directory which the command will be invoked in. 32 | 33 | ```python 34 | import os 35 | from django_node import npm 36 | 37 | # Install the dependencies in a particular directory's package.json 38 | npm.install('/path/to/some/directory/') 39 | 40 | # Install the dependencies in the same directory as the current python file 41 | npm.install(os.path.dirname(__file__)) 42 | ``` 43 | 44 | ### django_node.npm.run() 45 | 46 | Invokes NPM with the arguments provided and returns the resulting stderr and stdout. 47 | 48 | ```python 49 | from django_node import npm 50 | 51 | stderr, stdout = npm.run('install', '--save', 'some-package') 52 | ``` 53 | 54 | ### django_node.npm.ensure_installed() 55 | 56 | Raises an exception if NPM is not installed. 57 | 58 | ### django_node.npm.ensure_version_gte() 59 | 60 | Raises an exception if the installed version of NPM is less than the version required. 61 | 62 | Arguments: 63 | 64 | - `version_required`: a tuple containing the minimum version required. 65 | 66 | ```python 67 | from django_node import npm 68 | 69 | npm.ensure_version_gte((2, 0, 0,)) 70 | ``` 71 | 72 | ### django_node.npm.is_installed 73 | 74 | A boolean indicating if NPM is installed. 75 | 76 | ### django_node.npm.version 77 | 78 | A tuple containing the version of NPM installed. For example, `(2, 0, 0)` 79 | 80 | ### django_node.npm.version_raw 81 | 82 | A string containing the raw version returned from NPM. For example, `'2.0.0'` 83 | -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | Settings can be overridden by defining a dictionary named `DJANGO_NODE` in your settings file. For example: 5 | ```python 6 | DJANGO_NODE = { 7 | 'PATH_TO_NODE': '/path/to/some/binary', 8 | } 9 | ``` 10 | 11 | **Settings** 12 | - [PATH_TO_NODE](#django_nodepath_to_node) 13 | - [NODE_VERSION_COMMAND](#django_nodenode_version_command) 14 | - [NODE_VERSION_FILTER](#django_nodenode_version_filter) 15 | - [PATH_TO_NPM](#django_nodepath_to_npm) 16 | - [NPM_VERSION_COMMAND](#django_nodenpm_version_command) 17 | - [NPM_VERSION_FILTER](#django_nodenpm_version_filter) 18 | - [NPM_INSTALL_COMMAND](#django_nodenpm_install_command) 19 | - [NPM_INSTALL_PATH_TO_PYTHON](#django_nodenpm_install_path_to_python) 20 | 21 | ### DJANGO_NODE['PATH_TO_NODE'] 22 | 23 | A path that will resolve to Node. 24 | 25 | Default: 26 | ```python 27 | 'node' 28 | ``` 29 | 30 | ### DJANGO_NODE['NODE_VERSION_COMMAND'] 31 | 32 | The command invoked on Node to retrieve its version. 33 | 34 | Default: 35 | ```python 36 | '--version' 37 | ``` 38 | 39 | ### DJANGO_NODE['NODE_VERSION_FILTER'] 40 | 41 | A function which will generate a tuple of version numbers from 42 | the raw version string returned from Node. 43 | 44 | Default 45 | ```python 46 | lambda version: tuple(map(int, (version[1:] if version[0] == 'v' else version).split('.'))) 47 | ``` 48 | 49 | ### DJANGO_NODE['PATH_TO_NPM'] 50 | 51 | A path that will resolve to NPM. 52 | 53 | Default 54 | ```python 55 | 'npm' 56 | ``` 57 | 58 | ### DJANGO_NODE['NPM_VERSION_COMMAND'] 59 | 60 | The command invoked on NPM to retrieve its version. 61 | 62 | Default 63 | ```python 64 | '--version' 65 | ``` 66 | 67 | ### DJANGO_NODE['NPM_VERSION_FILTER'] 68 | 69 | A function which will generate a tuple of version numbers from 70 | the raw version string returned from NPM. 71 | 72 | Default 73 | ```python 74 | lambda version: tuple(map(int, version.split('.'))), 75 | ``` 76 | 77 | ### DJANGO_NODE['NPM_INSTALL_COMMAND'] 78 | 79 | The install command invoked on NPM. This is prepended to all calls to `django_node.npm.install`. 80 | 81 | Default 82 | ```python 83 | 'install' 84 | ``` 85 | 86 | ### DJANGO_NODE['NPM_INSTALL_PATH_TO_PYTHON'] 87 | 88 | A path to a python interpreter which will be provided by NPM to any dependencies which require 89 | Python. 90 | 91 | If you are using Python 3 as your system default or virtualenv `python`, NPM may throw errors 92 | while installing certain libraries - such as `gyp` - which depend on Python 2.x. Specifying a 93 | path to a Python 2.x interpreter should resolve these errors. 94 | 95 | Default 96 | ```python 97 | None 98 | ``` 99 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | Django Node Example 2 | =================== 3 | 4 | The example project illustrates a simple way to integrate server-side 5 | execution of JS into a Python process. 6 | 7 | The relevant parts are: 8 | - [djangosite/services/hello_world.js](djangosite/services/hello_world.js) - your JS service which takes in data and returns a response 9 | - [djangosite/services.py](djangosite/services.py) - a python interface to your JS service 10 | - [djangosite/views.py](djangosite/views.py) - sending data to your service and rendering the output 11 | 12 | The following settings inform django-node to load the specific services into the node server. 13 | 14 | ```python 15 | INSTALLED_APPS = ( 16 | # ... 17 | 'django_node', 18 | ) 19 | 20 | # ... 21 | 22 | DJANGO_NODE = { 23 | 'SERVICES': ( 24 | 'djangosite.services', 25 | ) 26 | } 27 | ``` 28 | 29 | Running the example 30 | ------------------- 31 | 32 | ```bash 33 | cd django-node/example 34 | mkvirtualenv django-node-example 35 | pip install -r requirements.txt 36 | ./manage.py runserver 37 | ``` 38 | 39 | Visit http://127.0.0.1:8000 in your browser. 40 | -------------------------------------------------------------------------------- /example/djangosite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markfinger/django-node/a2f56bf027fd3c4cbc6a0213881922a50acae1d6/example/djangosite/__init__.py -------------------------------------------------------------------------------- /example/djangosite/services.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django_node.base_service import BaseService 3 | 4 | 5 | class HelloWorldService(BaseService): 6 | path_to_source = os.path.join(os.path.dirname(__file__), 'services', 'hello_world.js') 7 | 8 | def greet(self, name): 9 | response = self.send(name=name) 10 | return response.text -------------------------------------------------------------------------------- /example/djangosite/services/hello_world.js: -------------------------------------------------------------------------------- 1 | var service = function(data, res) { 2 | var greeting = 'Hello, ' + data.name + '!'; 3 | res.end(greeting); 4 | }; 5 | 6 | module.exports = service; -------------------------------------------------------------------------------- /example/djangosite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for djangosite project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = 'ge23t*ei3s43lj7v0p_wn*3)ee2yjda2iny)m$@c1vnr$1+u#y' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 40 | 'django_node', 41 | ) 42 | 43 | MIDDLEWARE_CLASSES = ( 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ) 52 | 53 | ROOT_URLCONF = 'djangosite.urls' 54 | 55 | # Database 56 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 57 | 58 | DATABASES = { 59 | 'default': { 60 | 'ENGINE': 'django.db.backends.sqlite3', 61 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 62 | } 63 | } 64 | 65 | # Internationalization 66 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 67 | 68 | LANGUAGE_CODE = 'en-us' 69 | 70 | TIME_ZONE = 'UTC' 71 | 72 | USE_I18N = True 73 | 74 | USE_L10N = True 75 | 76 | USE_TZ = True 77 | 78 | 79 | # Static files (CSS, JavaScript, Images) 80 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 81 | 82 | STATIC_URL = '/static/' 83 | 84 | DJANGO_NODE = { 85 | 'SERVICES': ( 86 | 'djangosite.services', 87 | ) 88 | } -------------------------------------------------------------------------------- /example/djangosite/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.contrib import admin 3 | 4 | urlpatterns = patterns('', 5 | url(r'^$', 'djangosite.views.hello_world', name='hello_world'), 6 | 7 | url(r'^admin/', include(admin.site.urls)), 8 | ) 9 | -------------------------------------------------------------------------------- /example/djangosite/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from .services import HelloWorldService 3 | 4 | hello_world_service = HelloWorldService() 5 | 6 | 7 | def hello_world(request): 8 | content = hello_world_service.greet('World') 9 | return HttpResponse(content) -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangosite.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | django-node==4.0.0 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | requests>=2.5.1 -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import django 5 | 6 | 7 | if __name__ == '__main__': 8 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' 9 | if hasattr(django, 'setup'): # Only compatible with Django >= 1.7 10 | django.setup() 11 | 12 | # For Django 1.6, need to import after setting DJANGO_SETTINGS_MODULE. 13 | from django.conf import settings 14 | from django.test.utils import get_runner 15 | 16 | TestRunner = get_runner(settings) 17 | test_runner = TestRunner() 18 | failures = test_runner.run_tests(['tests']) 19 | sys.exit(bool(failures)) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | VERSION = '4.0.2' 4 | 5 | setup( 6 | name='django-node', 7 | version=VERSION, 8 | packages=find_packages(exclude=('tests', 'example',)), 9 | package_data={ 10 | 'django_node': [ 11 | 'node_server.js', 12 | 'services/echo.js', 13 | 'package.json', 14 | ], 15 | }, 16 | install_requires=[ 17 | 'django', 18 | 'requests>=2.5.1', 19 | ], 20 | description='Bindings and utils for integrating Node.js and NPM into a Django application', 21 | long_description=(''' 22 | Deprecated 23 | ---------- 24 | 25 | django-node has been deprecated. The project has been split into the following packages: 26 | 27 | https://github.com/markfinger/python-js-host 28 | 29 | https://github.com/markfinger/python-nodejs 30 | 31 | https://github.com/markfinger/python-npm 32 | 33 | Documentation for django-node is available at https://github.com/markfinger/django-node 34 | '''), 35 | author='Mark Finger', 36 | author_email='markfinger@gmail.com', 37 | url='https://github.com/markfinger/django-node', 38 | ) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markfinger/django-node/a2f56bf027fd3c4cbc6a0213881922a50acae1d6/tests/__init__.py -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-node-tests", 3 | "version": "0.0.1", 4 | "private": true, 5 | "dependencies": { 6 | "yargs": "^1.3.3" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/services.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django_node.base_service import BaseService 3 | 4 | 5 | class TimeoutService(BaseService): 6 | path_to_source = os.path.join(os.path.dirname(__file__), 'services', 'timeout.js') 7 | timeout = 1.0 8 | 9 | 10 | class ErrorService(BaseService): 11 | path_to_source = os.path.join(os.path.dirname(__file__), 'services', 'error.js') -------------------------------------------------------------------------------- /tests/services/error.js: -------------------------------------------------------------------------------- 1 | var error = function(request, response) { 2 | throw new Error('Error service') 3 | }; 4 | 5 | module.exports = error; -------------------------------------------------------------------------------- /tests/services/timeout.js: -------------------------------------------------------------------------------- 1 | // This service should cause a timeout exception to be raised 2 | 3 | var timeout = function(request, response) { 4 | setTimeout(function() { 5 | response.send(500, 'timeout should have occurred already') 6 | }, 11000); 7 | }; 8 | 9 | module.exports = timeout; -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | 3 | SECRET_KEY = '_' 4 | 5 | INSTALLED_APPS = ( 6 | 'django_node', 7 | ) 8 | 9 | DJANGO_NODE = { 10 | 'SERVICES': ( 11 | 'tests.services', 12 | ) 13 | } -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import unittest 4 | from django.utils import six 5 | from django_node import node, npm 6 | from django_node.node_server import NodeServer 7 | from django_node.server import server 8 | from django_node.base_service import BaseService 9 | from django_node.exceptions import ( 10 | OutdatedDependency, MalformedVersionInput, NodeServiceError, NodeServerAddressInUseError, NodeServerTimeoutError, 11 | ServiceSourceDoesNotExist, MalformedServiceName 12 | ) 13 | from django_node.services import EchoService 14 | from .services import TimeoutService, ErrorService 15 | from .utils import StdOutTrap 16 | 17 | TEST_DIR = os.path.abspath(os.path.dirname(__file__)) 18 | PATH_TO_NODE_MODULES = os.path.join(TEST_DIR, 'node_modules') 19 | DEPENDENCY_PACKAGE = 'yargs' 20 | PATH_TO_INSTALLED_PACKAGE = os.path.join(PATH_TO_NODE_MODULES, DEPENDENCY_PACKAGE) 21 | PACKAGE_TO_INSTALL = 'jquery' 22 | PATH_TO_PACKAGE_TO_INSTALL = os.path.join(PATH_TO_NODE_MODULES, PACKAGE_TO_INSTALL) 23 | PATH_TO_PACKAGE_JSON = os.path.join(TEST_DIR, 'package.json') 24 | 25 | echo_service = EchoService() 26 | timeout_service = TimeoutService() 27 | error_service = ErrorService() 28 | 29 | 30 | class TestDjangoNode(unittest.TestCase): 31 | 32 | maxDiff = None 33 | 34 | def setUp(self): 35 | self.package_json_contents = self.read_package_json() 36 | 37 | def tearDown(self): 38 | if os.path.exists(PATH_TO_NODE_MODULES): 39 | shutil.rmtree(PATH_TO_NODE_MODULES) 40 | self.write_package_json(self.package_json_contents) 41 | if server.is_running: 42 | # Reset the server 43 | server.stop() 44 | 45 | def read_package_json(self): 46 | with open(PATH_TO_PACKAGE_JSON, 'r') as package_json_file: 47 | return package_json_file.read() 48 | 49 | def write_package_json(self, contents): 50 | with open(PATH_TO_PACKAGE_JSON, 'w+') as package_json_file: 51 | package_json_file.write(contents) 52 | 53 | def test_node_is_installed(self): 54 | self.assertTrue(node.is_installed) 55 | 56 | def test_node_version_raw(self): 57 | self.assertTrue(isinstance(node.version_raw, six.string_types)) 58 | self.assertGreater(len(node.version_raw), 0) 59 | 60 | def test_node_version(self): 61 | self.assertTrue(isinstance(node.version, tuple)) 62 | self.assertGreaterEqual(len(node.version), 3) 63 | 64 | def test_npm_is_installed(self): 65 | self.assertTrue(npm.is_installed) 66 | 67 | def test_npm_version_raw(self): 68 | self.assertTrue(isinstance(npm.version_raw, six.string_types)) 69 | self.assertGreater(len(npm.version_raw), 0) 70 | 71 | def test_npm_version(self): 72 | self.assertTrue(isinstance(npm.version, tuple)) 73 | self.assertGreaterEqual(len(npm.version), 3) 74 | 75 | def test_ensure_node_installed(self): 76 | node.ensure_installed() 77 | 78 | def test_ensure_npm_installed(self): 79 | npm.ensure_installed() 80 | 81 | def test_ensure_node_version_greater_than(self): 82 | self.assertRaises(MalformedVersionInput, node.ensure_version_gte, 'v99999.0.0') 83 | self.assertRaises(MalformedVersionInput, node.ensure_version_gte, '99999.0.0') 84 | self.assertRaises(MalformedVersionInput, node.ensure_version_gte, (None,)) 85 | self.assertRaises(MalformedVersionInput, node.ensure_version_gte, (10,)) 86 | self.assertRaises(MalformedVersionInput, node.ensure_version_gte, (999999999,)) 87 | self.assertRaises(MalformedVersionInput, node.ensure_version_gte, (999999999, 0,)) 88 | 89 | self.assertRaises(OutdatedDependency, node.ensure_version_gte, (999999999, 0, 0,)) 90 | 91 | node.ensure_version_gte((0, 0, 0,)) 92 | node.ensure_version_gte((0, 9, 99999999)) 93 | node.ensure_version_gte((0, 10, 33,)) 94 | 95 | def test_ensure_npm_version_greater_than(self): 96 | self.assertRaises(MalformedVersionInput, npm.ensure_version_gte, 'v99999.0.0') 97 | self.assertRaises(MalformedVersionInput, npm.ensure_version_gte, '99999.0.0') 98 | self.assertRaises(MalformedVersionInput, npm.ensure_version_gte, (None,)) 99 | self.assertRaises(MalformedVersionInput, npm.ensure_version_gte, (10,)) 100 | self.assertRaises(MalformedVersionInput, npm.ensure_version_gte, (999999999,)) 101 | self.assertRaises(MalformedVersionInput, npm.ensure_version_gte, (999999999, 0,)) 102 | 103 | self.assertRaises(OutdatedDependency, npm.ensure_version_gte, (999999999, 0, 0,)) 104 | 105 | npm.ensure_version_gte((0, 0, 0,)) 106 | npm.ensure_version_gte((0, 9, 99999999)) 107 | npm.ensure_version_gte((2, 1, 8,)) 108 | 109 | def test_node_run_returns_output(self): 110 | stderr, stdout = node.run('--version',) 111 | stdout = stdout.strip() 112 | self.assertEqual(stdout, node.version_raw) 113 | 114 | def test_npm_run_returns_output(self): 115 | stderr, stdout = npm.run('--version',) 116 | stdout = stdout.strip() 117 | self.assertEqual(stdout, npm.version_raw) 118 | 119 | def test_npm_install_can_install_dependencies(self): 120 | npm.install(TEST_DIR) 121 | self.assertTrue(os.path.exists(PATH_TO_NODE_MODULES)) 122 | self.assertTrue(os.path.exists(PATH_TO_INSTALLED_PACKAGE)) 123 | 124 | def test_node_server_services_can_be_validated(self): 125 | class MissingSource(BaseService): 126 | pass 127 | self.assertRaises(ServiceSourceDoesNotExist, MissingSource.validate) 128 | 129 | class AbsoluteUrlName(EchoService): 130 | name = 'http://foo.com' 131 | self.assertRaises(MalformedServiceName, AbsoluteUrlName.validate) 132 | 133 | class MissingOpeningSlashName(EchoService): 134 | name = 'foo/bar' 135 | self.assertRaises(MalformedServiceName, MissingOpeningSlashName.validate) 136 | 137 | def test_node_server_services_are_discovered(self): 138 | for service in (EchoService, ErrorService, TimeoutService): 139 | self.assertIn(service, server.services) 140 | 141 | def test_node_server_can_start_and_stop(self): 142 | self.assertIsInstance(server, NodeServer) 143 | server.start() 144 | self.assertTrue(server.is_running) 145 | self.assertTrue(server.test()) 146 | server.stop() 147 | self.assertFalse(server.is_running) 148 | self.assertFalse(server.test()) 149 | server.start() 150 | self.assertTrue(server.is_running) 151 | self.assertTrue(server.test()) 152 | server.stop() 153 | self.assertFalse(server.is_running) 154 | self.assertFalse(server.test()) 155 | 156 | def test_node_server_process_can_rely_on_externally_controlled_processes(self): 157 | self.assertFalse(server.test()) 158 | new_server = NodeServer() 159 | new_server.start() 160 | self.assertTrue(server.test()) 161 | new_server.stop() 162 | self.assertFalse(new_server.test()) 163 | self.assertFalse(server.test()) 164 | 165 | def test_node_server_process_can_raise_on_port_collisions(self): 166 | self.assertFalse(server.test()) 167 | new_server = NodeServer() 168 | new_server.start() 169 | self.assertTrue(server.test()) 170 | self.assertEqual(server.address, new_server.address) 171 | self.assertEqual(server.port, new_server.port) 172 | self.assertRaises(NodeServerAddressInUseError, server.start, use_existing_process=False) 173 | new_server.stop() 174 | self.assertFalse(server.test()) 175 | server.start(use_existing_process=False) 176 | self.assertTrue(server.test()) 177 | 178 | def test_node_server_config_is_as_expected(self): 179 | config = server.get_config() 180 | self.assertEqual(config['address'], server.address) 181 | self.assertEqual(config['port'], server.port) 182 | self.assertEqual(config['startup_output'], server.get_startup_output()) 183 | 184 | services = (EchoService, ErrorService, TimeoutService) 185 | self.assertEqual(len(config['services']), len(services)) 186 | 187 | service_names = [obj['name'] for obj in config['services']] 188 | service_sources = [obj['path_to_source'] for obj in config['services']] 189 | for service in services: 190 | self.assertIn(service.get_name(), service_names) 191 | self.assertIn(service.get_path_to_source(), service_sources) 192 | 193 | def test_node_server_echo_service_pumps_output_back(self): 194 | response = echo_service.send(echo='test content') 195 | self.assertEqual(response.text, 'test content') 196 | 197 | def test_node_server_throws_timeout_on_long_running_services(self): 198 | self.assertRaises(NodeServerTimeoutError, timeout_service.send) 199 | 200 | def test_node_server_error_service_works(self): 201 | self.assertRaises(NodeServiceError, error_service.send) 202 | 203 | def test_node_server_config_management_command_provides_the_expected_output(self): 204 | from django_node.management.commands.node_server_config import Command 205 | 206 | with StdOutTrap() as output: 207 | Command().handle() 208 | 209 | self.assertEqual(''.join(output), server.get_serialised_config()) -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from django.utils import six 3 | if six.PY2: 4 | from cStringIO import StringIO 5 | elif six.PY3: 6 | from io import StringIO 7 | 8 | 9 | class StdOutTrap(list): 10 | def __enter__(self): 11 | self._stdout = sys.stdout 12 | sys.stdout = self._stringio = StringIO() 13 | return self 14 | 15 | def __exit__(self, *args): 16 | self.extend(self._stringio.getvalue().splitlines()) 17 | sys.stdout = self._stdout --------------------------------------------------------------------------------