diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..2fc80e3
--- /dev/null
+++ b/.env.example
@@ -0,0 +1 @@
+PORT=3000
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..de5b88d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,145 @@
+# Created by .ignore support plugin (hsz.mobi)
+### Python template
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# Other
+.idea/
+*.iml
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a64ca90
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Davide Caruso
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1caf2a3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,131 @@
+
+
+
+
+> *Python+GraphQL* microservice to check **ports availability** in a host.
+
+## Install
+```shell script
+git clone git@github.com:davidecaruso/wazowski.git && cd wazowski
+cp .env.example .env && vi .env
+pip3 install --no-cache-dir -r requirements.txt
+python3 main.py
+```
+
+## Getting started
+Many of the following *GraphQL* queries accept these parameters:
+- `start`: is the starting port value of the range. Default value is **1024**
+- `end`: is the ending port value of the range. Default value is **65535**
+- `host`: is the host where the search will be performed. Default value is **"127.0.0.1"**
+
+#### Check if a port is free
+> **POST** /graphql
+
+```graphql
+query Query {
+ check(port: 3000, host: "127.0.0.1")
+}
+```
+> **200**
+```json
+{
+ "data": {
+ "check": true
+ }
+}
+```
+
+#### Get the list of free ports in a range
+> **POST** /graphql
+
+```graphql
+query Query {
+ list(start: 3000, end: 3010, host: "127.0.0.1")
+}
+```
+> **200**
+```json
+{
+ "data": {
+ "list": [
+ 3000,
+ 3002,
+ 3010
+ ]
+ }
+}
+```
+
+#### Get next free port to a given one in a range
+> **POST** /graphql
+
+```graphql
+query Query {
+ next(port: 3007, start: 3000, end: 3010, host: "127.0.0.1")
+}
+```
+> **200**
+```json
+{
+ "data": {
+ "next": 3008
+ }
+}
+```
+
+#### Get the previous free port to a given one in a range
+> **POST** /graphql
+
+```graphql
+query Query {
+ previous(port: 3007, start: 3000, end: 3010, host: "127.0.0.1")
+}
+```
+> **200**
+```json
+{
+ "data": {
+ "next": 3006
+ }
+}
+```
+
+#### Get the previous free port to a given one in a range
+> **POST** /graphql
+
+```graphql
+query Query {
+ previous(port: 3007, start: 3000, end: 3010, host: "127.0.0.1")
+}
+```
+> **200**
+```json
+{
+ "data": {
+ "previous": 3006
+ }
+}
+```
+
+#### Get a random free port in a range
+> **POST** /graphql
+
+```graphql
+query Query {
+ random(start: 3000, end: 3010, host: "127.0.0.1")
+}
+```
+> **200**
+```json
+{
+ "data": {
+ "random": 3265
+ }
+}
+```
+
+## Author
+[Davide Caruso](https://about.me/davidecaruso)
+
+## License
+Licensed under [MIT](LICENSE).
diff --git a/logo.jpg b/logo.jpg
new file mode 100644
index 0000000..7f30e14
Binary files /dev/null and b/logo.jpg differ
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..84d5ad7
--- /dev/null
+++ b/main.py
@@ -0,0 +1,16 @@
+import os
+
+from aiohttp import web
+from dotenv import load_dotenv, find_dotenv
+
+from src.controller import resolve
+from src.wazowski import Wazowski
+
+load_dotenv(find_dotenv())
+
+app = web.Application()
+app.add_routes([web.post('/graphql', resolve)])
+
+if __name__ == '__main__':
+ port = int(os.getenv('PORT'))
+ web.run_app(app, port=port if Wazowski.is_port_free(port) else Wazowski.next_free_port(port))
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..954ad7c
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+aiohttp
+graphene
+python-dotenv
diff --git a/src/controller.py b/src/controller.py
new file mode 100644
index 0000000..4c946b6
--- /dev/null
+++ b/src/controller.py
@@ -0,0 +1,16 @@
+from aiohttp import web
+
+from src.graphql import schema
+
+
+async def resolve(request):
+ body = await request.json()
+ try:
+ result = schema.execute(str(body['query']))
+ return web.json_response({
+ 'data': dict(result.data.items()) if result.data else None
+ }, status=200)
+ except Exception as e:
+ return web.json_response({
+ 'error': e
+ }, status=500)
diff --git a/src/graphql.py b/src/graphql.py
new file mode 100644
index 0000000..a4e85e7
--- /dev/null
+++ b/src/graphql.py
@@ -0,0 +1,59 @@
+from graphene import ObjectType, String, Int, Boolean, List, Schema
+
+from src.wazowski import Wazowski
+
+
+class Query(ObjectType):
+ """
+ GraphQL queries and resolvers
+ """
+
+ list = List(
+ of_type=Int,
+ start=Int(default_value=Wazowski.FIRST_PORT),
+ end=Int(default_value=Wazowski.LAST_PORT),
+ host=String(default_value=Wazowski.HOST)
+ )
+
+ check = Boolean(
+ port=Int(required=True),
+ host=String(default_value=Wazowski.HOST)
+ )
+
+ next = Int(
+ port=Int(required=True),
+ start=Int(default_value=Wazowski.FIRST_PORT),
+ end=Int(default_value=Wazowski.LAST_PORT),
+ host=String(default_value=Wazowski.HOST)
+ )
+
+ previous = Int(
+ port=Int(required=True),
+ start=Int(default_value=Wazowski.FIRST_PORT),
+ end=Int(default_value=Wazowski.LAST_PORT),
+ host=String(default_value=Wazowski.HOST)
+ )
+
+ random = Int(
+ start=Int(default_value=Wazowski.FIRST_PORT),
+ end=Int(default_value=Wazowski.LAST_PORT),
+ host=String(default_value=Wazowski.HOST)
+ )
+
+ def resolve_list(self, info, start, end, host):
+ return Wazowski.free_ports(start, end, host)
+
+ def resolve_check(self, info, port, host):
+ return Wazowski.is_port_free(port, host)
+
+ def resolve_next(self, info, port, start, end, host):
+ return Wazowski.next_free_port(port, start, end, host)
+
+ def resolve_previous(self, info, port, start, end, host):
+ return Wazowski.previous_free_port(port, start, end, host)
+
+ def resolve_random(self, info, start, end, host):
+ return Wazowski.random_free_port(start, end, host)
+
+
+schema = Schema(query=Query)
diff --git a/src/wazowski.py b/src/wazowski.py
new file mode 100644
index 0000000..fb417da
--- /dev/null
+++ b/src/wazowski.py
@@ -0,0 +1,87 @@
+import random
+import socket
+
+
+class Wazowski:
+ FIRST_PORT = 1024
+ LAST_PORT = 65535
+ HOST = '127.0.0.1'
+
+ @staticmethod
+ def free_ports(start=FIRST_PORT, end=LAST_PORT, host=HOST):
+ """
+ Return the free ports on host in a range
+ :param start: The starting port of the range. Default: 1024
+ :param end: The ending port of the range. Default: 65535
+ :param host: The host. Default: 127.0.0.1
+ :return: List of port numbers
+ """
+ ports = []
+ while start <= end:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ try:
+ s.bind((host, start))
+ ports.append(start)
+ except:
+ pass
+
+ s.close()
+ start += 1
+
+ return sorted(ports)
+
+ @staticmethod
+ def is_port_free(port, host=HOST):
+ """
+ Check if the given port is free or not
+ :param port: The port of reference
+ :param host: The host. Default: 127.0.0.1
+ :return: True if port is free, False if port is not free
+ """
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ try:
+ s.bind((host, port))
+ s.close()
+ return True
+ except:
+ s.close()
+ return False
+
+ @staticmethod
+ def random_free_port(start=FIRST_PORT, end=LAST_PORT, host=HOST):
+ """
+ Return a random free port on host in a range
+ :param start: The starting port of the range. Default: 1024
+ :param end: The ending port of the range. Default: 65535
+ :param host: The host. Default: 127.0.0.1
+ :return: Port number
+ """
+ return random.choice(Wazowski.free_ports(start, end, host))
+
+ @staticmethod
+ def previous_free_port(port, start=FIRST_PORT, end=LAST_PORT, host=HOST):
+ """
+ Return a the previous free port of a given port on host in a range
+ :param port: The port of reference
+ :param start: The starting port of the range. Default: 1024
+ :param end: The ending port of the range. Default: 65535
+ :param host: The host. Default: 127.0.0.1
+ :return: Port number or None
+ """
+ for free_port in Wazowski.free_ports(start, end, host)[::-1]:
+ if free_port < port:
+ return free_port
+
+ @staticmethod
+ def next_free_port(port, start=FIRST_PORT, end=LAST_PORT, host=HOST):
+ """
+ Return a the next free port of a given port on host in a range
+ :param port: The port of reference
+ :param start: The starting port of the range. Default: 1024
+ :param end: The ending port of the range. Default: 65535
+ :param host: The host. Default: 127.0.0.1
+ :return: Port number or None
+ """
+ for free_port in Wazowski.free_ports(start, end, host):
+ if free_port > port:
+ return free_port