├── .gitignore ├── .pylintrc ├── Makefile ├── README.md ├── __init__.py ├── application ├── __init__.py ├── address_service.py ├── base_service.py ├── environments │ ├── __init__.py │ ├── config.py │ └── local │ │ ├── app │ │ ├── Dockerfile │ │ └── startup.sh │ │ ├── mongo │ │ ├── Dockerfile │ │ ├── seed │ │ │ ├── Dockerfile │ │ │ ├── falcon │ │ │ │ ├── addresses.bson.gz │ │ │ │ └── addresses.metadata.json.gz │ │ │ └── startup.sh │ │ └── startup.sh │ │ ├── mysql │ │ ├── Dockerfile │ │ ├── my.cnf │ │ ├── seed │ │ │ ├── Dockerfile │ │ │ ├── seed.sql.tar.gz │ │ │ └── startup.sh │ │ └── startup.sh │ │ └── wait-for ├── service_register.py ├── spec_service.py ├── specs │ ├── __init__.py │ ├── default.yml │ └── status.yml ├── status_service.py ├── templates │ ├── __init__.py │ ├── address.json │ ├── status.json │ └── user.json └── user_service.py ├── docker-compose.yml ├── domain ├── __init__.py ├── domain_service.py ├── entities.py └── repositories.py ├── infrastructure ├── mongo.py └── mysql.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | # PyLint config for apitools code. 2 | # 3 | # NOTES: 4 | # 5 | # - Rules for test / demo code are generated into 'pylintrc_reduced' 6 | # as deltas from this configuration by the 'run_pylint.py' script. 7 | # 8 | # - 'RATIONALE: API mapping' as a defense for non-default settings is 9 | # based on the fact that this library maps APIs which are outside our 10 | # control, and adhering to the out-of-the-box defaults would induce 11 | # breakage / complexity in those mappings 12 | # 13 | [MASTER] 14 | 15 | # Specify a configuration file. 16 | # DEFAULT: rcfile= 17 | 18 | # Python code to execute, usually for sys.path manipulation such as 19 | # pygtk.require(). 20 | # DEFAULT: init-hook= 21 | 22 | # Profiled execution. 23 | # DEFAULT: profile=no 24 | 25 | # Add files or directories to the blacklist. They should be base names, not 26 | # paths. 27 | # DEFAULT: ignore=CVS 28 | # NOTE: This path must be relative due to the use of 29 | # os.walk in astroid.modutils.get_module_files. 30 | 31 | # Pickle collected data for later comparisons. 32 | # DEFAULT: persistent=yes 33 | 34 | # List of plugins (as comma separated values of python modules names) to load, 35 | # usually to register additional checkers. 36 | # DEFAULT: load-plugins= 37 | 38 | # DEPRECATED 39 | # DEFAULT: include-ids=no 40 | 41 | # DEPRECATED 42 | # DEFAULT: symbols=no 43 | 44 | 45 | [MESSAGES CONTROL] 46 | 47 | # TODO: remove cyclic-import. 48 | disable = 49 | cyclic-import, 50 | fixme, 51 | import-error, 52 | locally-disabled, 53 | locally-enabled, 54 | no-member, 55 | no-name-in-module, 56 | no-self-use, 57 | super-on-old-class, 58 | too-many-arguments, 59 | too-many-function-args, 60 | 61 | 62 | [REPORTS] 63 | 64 | # Set the output format. Available formats are text, parseable, colorized, msvs 65 | # (visual studio) and html. You can also give a reporter class, eg 66 | # mypackage.mymodule.MyReporterClass. 67 | # DEFAULT: output-format=text 68 | 69 | # Put messages in a separate file for each module / package specified on the 70 | # command line instead of printing them on stdout. Reports (if any) will be 71 | # written in a file name "pylint_global.[txt|html]". 72 | # DEFAULT: files-output=no 73 | 74 | # Tells whether to display a full report or only the messages 75 | # DEFAULT: reports=yes 76 | # RATIONALE: run from Travis / tox, and don't need / want to parse output. 77 | reports=no 78 | 79 | # Python expression which should return a note less than 10 (10 is the highest 80 | # note). You have access to the variables errors warning, statement which 81 | # respectively contain the number of errors / warnings messages and the total 82 | # number of statements analyzed. This is used by the global evaluation report 83 | # (RP0004). 84 | # DEFAULT: evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 85 | 86 | # Add a comment according to your evaluation note. This is used by the global 87 | # evaluation report (RP0004). 88 | # DEFAULT: comment=no 89 | 90 | # Template used to display messages. This is a python new-style format string 91 | # used to format the message information. See doc for all details 92 | #msg-template= 93 | 94 | 95 | [SIMILARITIES] 96 | 97 | # Minimum lines number of a similarity. 98 | # DEFAULT: min-similarity-lines=4 99 | min-similarity-lines=15 100 | 101 | # Ignore comments when computing similarities. 102 | # DEFAULT: ignore-comments=yes 103 | 104 | # Ignore docstrings when computing similarities. 105 | # DEFAULT: ignore-docstrings=yes 106 | 107 | # Ignore imports when computing similarities. 108 | # DEFAULT: ignore-imports=no 109 | ignore-imports=yes 110 | 111 | 112 | [VARIABLES] 113 | 114 | # Tells whether we should check for unused import in __init__ files. 115 | # DEFAULT: init-import=no 116 | 117 | # A regular expression matching the name of dummy variables (i.e. expectedly 118 | # not used). 119 | dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) 120 | 121 | 122 | # List of additional names supposed to be defined in builtins. Remember that 123 | # you should avoid to define new builtins when possible. 124 | # DEFAULT: additional-builtins= 125 | 126 | 127 | [LOGGING] 128 | 129 | # Logging modules to check that the string format arguments are in logging 130 | # function parameter format 131 | # DEFAULT: logging-modules=logging 132 | 133 | 134 | [FORMAT] 135 | 136 | # Maximum number of characters on a single line. 137 | # DEFAULT: max-line-length=80 138 | 139 | # Regexp for a line that is allowed to be longer than the limit. 140 | # DEFAULT: ignore-long-lines=^\s*(# )??$ 141 | 142 | # Allow the body of an if to be on the same line as the test if there is no 143 | # else. 144 | # DEFAULT: single-line-if-stmt=no 145 | 146 | # List of optional constructs for which whitespace checking is disabled 147 | # DEFAULT: no-space-check=trailing-comma,dict-separator 148 | # RATIONALE: pylint ignores whitespace checks around the 149 | # constructs "dict-separator" (cases like {1:2}) and 150 | # "trailing-comma" (cases like {1: 2, }). 151 | # By setting "no-space-check" to empty whitespace checks will be 152 | # enforced around both constructs. 153 | no-space-check = 154 | 155 | # Maximum number of lines in a module 156 | # DEFAULT: max-module-lines=1000 157 | max-module-lines=1500 158 | 159 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 160 | # tab). 161 | # DEFAULT: indent-string=' ' 162 | 163 | # Number of spaces of indent required inside a hanging or continued line. 164 | # DEFAULT: indent-after-paren=4 165 | 166 | 167 | [MISCELLANEOUS] 168 | 169 | # List of note tags to take in consideration, separated by a comma. 170 | # DEFAULT: notes=FIXME,XXX,TODO 171 | 172 | 173 | [BASIC] 174 | 175 | # Regular expression which should only match function or class names that do 176 | # not require a docstring. 177 | # DEFAULT: no-docstring-rgx=__.*__ 178 | no-docstring-rgx=(__.*__|main) 179 | 180 | # Minimum line length for functions/classes that require docstrings, shorter 181 | # ones are exempt. 182 | # DEFAULT: docstring-min-length=-1 183 | docstring-min-length=10 184 | 185 | # Regular expression which should only match correct module names. The 186 | # leading underscore is sanctioned for private modules by Google's style 187 | # guide. 188 | module-rgx=^(_?[a-z][a-z0-9_]*)|__init__$ 189 | 190 | # Regular expression matching correct constant names 191 | # DEFAULT: const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 192 | const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ 193 | 194 | # Regular expression matching correct class attribute names 195 | # DEFAULT: class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 196 | class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ 197 | 198 | # Regular expression matching correct class names 199 | # DEFAULT: class-rgx=[A-Z_][a-zA-Z0-9]+$ 200 | class-rgx=^_?[A-Z][a-zA-Z0-9]*$ 201 | 202 | # Regular expression which should only match correct function names. 203 | # 'camel_case' and 'snake_case' group names are used for consistency of naming 204 | # styles across functions and methods. 205 | function-rgx=^(?:(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ 206 | 207 | # Regular expression which should only match correct method names. 208 | # 'camel_case' and 'snake_case' group names are used for consistency of naming 209 | # styles across functions and methods. 'exempt' indicates a name which is 210 | # consistent with all naming styles. 211 | method-rgx=^(?:(?P__[a-z0-9_]+__|next)|(?P_{0,2}[A-Z][a-zA-Z0-9]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ 212 | 213 | # Regular expression matching correct attribute names 214 | # DEFAULT: attr-rgx=[a-z_][a-z0-9_]{2,30}$ 215 | attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ 216 | 217 | # Regular expression matching correct argument names 218 | # DEFAULT: argument-rgx=[a-z_][a-z0-9_]{2,30}$ 219 | argument-rgx=^[a-z][a-z0-9_]*$ 220 | 221 | # Regular expression matching correct variable names 222 | # DEFAULT: variable-rgx=[a-z_][a-z0-9_]{2,30}$ 223 | variable-rgx=^[a-z][a-z0-9_]*$ 224 | 225 | # Regular expression matching correct inline iteration names 226 | # DEFAULT: inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 227 | inlinevar-rgx=^[a-z][a-z0-9_]*$ 228 | 229 | # Good variable names which should always be accepted, separated by a comma 230 | # DEFAULT: good-names=i,j,k,ex,Run,_ 231 | good-names=main,_ 232 | 233 | # Bad variable names which should always be refused, separated by a comma 234 | # DEFAULT: bad-names=foo,bar,baz,toto,tutu,tata 235 | bad-names= 236 | 237 | # List of builtins function names that should not be used, separated by a comma 238 | # 239 | bad-functions=input,apply,reduce 240 | 241 | 242 | [TYPECHECK] 243 | 244 | # Tells whether missing members accessed in mixin class should be ignored. A 245 | # mixin class is detected if its name ends with "mixin" (case insensitive). 246 | # DEFAULT: ignore-mixin-members=yes 247 | 248 | # List of module names for which member attributes should not be checked 249 | # (useful for modules/projects where namespaces are manipulated during runtime 250 | # and thus existing member attributes cannot be deduced by static analysis 251 | # DEFAULT: ignored-modules= 252 | 253 | # List of classes names for which member attributes should not be checked 254 | # (useful for classes with attributes dynamically set). 255 | # DEFAULT: ignored-classes=SQLObject 256 | 257 | # When zope mode is activated, add a predefined set of Zope acquired attributes 258 | # to generated-members. 259 | # DEFAULT: zope=no 260 | 261 | # List of members which are set dynamically and missed by pylint inference 262 | # system, and so shouldn't trigger E0201 when accessed. Python regular 263 | # expressions are accepted. 264 | # DEFAULT: generated-members=REQUEST,acl_users,aq_parent 265 | 266 | 267 | [IMPORTS] 268 | 269 | # Deprecated modules which should not be used, separated by a comma 270 | # DEFAULT: deprecated-modules=regsub,TERMIOS,Bastion,rexec 271 | 272 | # Create a graph of every (i.e. internal and external) dependencies in the 273 | # given file (report RP0402 must not be disabled) 274 | # DEFAULT: import-graph= 275 | 276 | # Create a graph of external dependencies in the given file (report RP0402 must 277 | # not be disabled) 278 | # DEFAULT: ext-import-graph= 279 | 280 | # Create a graph of internal dependencies in the given file (report RP0402 must 281 | # not be disabled) 282 | # DEFAULT: int-import-graph= 283 | 284 | 285 | [CLASSES] 286 | 287 | # List of interface methods to ignore, separated by a comma. This is used for 288 | # instance to not check methods defines in Zope's Interface base class. 289 | # DEFAULT: ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 290 | 291 | # List of method names used to declare (i.e. assign) instance attributes. 292 | # DEFAULT: defining-attr-methods=__init__,__new__,setUp 293 | 294 | # List of valid names for the first argument in a class method. 295 | # DEFAULT: valid-classmethod-first-arg=cls 296 | 297 | # List of valid names for the first argument in a metaclass class method. 298 | # DEFAULT: valid-metaclass-classmethod-first-arg=mcs 299 | 300 | 301 | [DESIGN] 302 | 303 | # Maximum number of arguments for function / method 304 | # DEFAULT: max-args=5 305 | # RATIONALE: API-mapping 306 | max-args = 14 307 | 308 | # Argument names that match this expression will be ignored. Default to name 309 | # with leading underscore 310 | # DEFAULT: ignored-argument-names=_.* 311 | 312 | # Maximum number of locals for function / method body 313 | # DEFAULT: max-locals=15 314 | max-locals=24 315 | 316 | # Maximum number of return / yield for function / method body 317 | # DEFAULT: max-returns=6 318 | max-returns=9 319 | 320 | # Maximum number of branch for function / method body 321 | # DEFAULT: max-branches=12 322 | max-branches=21 323 | 324 | # Maximum number of statements in function / method body 325 | # DEFAULT: max-statements=50 326 | 327 | # Maximum number of parents for a class (see R0901). 328 | # DEFAULT: max-parents=7 329 | 330 | # Maximum number of attributes for a class (see R0902). 331 | # DEFAULT: max-attributes=7 332 | # RATIONALE: API mapping 333 | max-attributes=19 334 | 335 | # Minimum number of public methods for a class (see R0903). 336 | # DEFAULT: min-public-methods=2 337 | # RATIONALE: context mgrs may have *no* public methods 338 | min-public-methods=0 339 | 340 | # Maximum number of public methods for a class (see R0904). 341 | # DEFAULT: max-public-methods=20 342 | # RATIONALE: API mapping 343 | max-public-methods=40 344 | 345 | [ELIF] 346 | max-nested-blocks=6 347 | 348 | [EXCEPTIONS] 349 | 350 | # Exceptions that will emit a warning when being caught. Defaults to 351 | # "Exception" 352 | # DEFAULT: overgeneral-exceptions=Exception 353 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker-compose build 3 | 4 | server: 5 | docker-compose up 6 | 7 | stop: 8 | docker-compose down 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python RESTful api with Domain Driven Design 2 | 3 | This repository contains a python restful api with the following features: 4 | + Swagger Documentation 5 | + Jinja templates for REST/Json standard 6 | + Falcon + gunicorn for http server 7 | + Pony orm for relational data 8 | + All configs via environment variables 9 | + Dependency injection for configs, etc. 10 | + Docker ready 11 | 12 | ## Run the project 13 | After cloning the repository, just run: `make server` 14 | 15 | To stop the server, run: `make stop` 16 | 17 | To share your mysql data with someone else, run the `make backup` command while server is running: 18 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleniac/python-rest-ddd/34a1099b59c3cd9c21701c1f4084b7768e8c052d/__init__.py -------------------------------------------------------------------------------- /application/__init__.py: -------------------------------------------------------------------------------- 1 | """Base application module. Used to register services""" 2 | import falcon 3 | import dependency_injector.providers as providers 4 | from falcon_swagger_ui import register_swaggerui_app 5 | from falcon_auth import FalconAuthMiddleware, JWTAuthBackend 6 | from application.environments.config import CONFIG 7 | from application.service_register import ServiceRegister 8 | 9 | class Application(object): 10 | """Application starter class""" 11 | def __init__(self): 12 | self.app_config = providers.Singleton(CONFIG) 13 | 14 | self.user_loader = lambda user: user 15 | self.jwt_auth = JWTAuthBackend(self.user_loader, 16 | self.app_config().APP_JWT_SECRET, 17 | algorithm='HS256', 18 | auth_header_prefix='Bearer', 19 | audience='localhost:5000', 20 | required_claims=['exp']) 21 | self.auth_middleware = FalconAuthMiddleware(self.jwt_auth, 22 | exempt_routes=[ 23 | self.app_config().APP_SWAGGER_URL, 24 | self.app_config().APP_DOCS_URL 25 | ]) 26 | self.app = falcon.API(middleware=[self.auth_middleware]) 27 | 28 | self.start() 29 | 30 | def start(self): 31 | ServiceRegister.register_services(self.app) 32 | register_swaggerui_app(self.app, 33 | self.app_config().APP_SWAGGER_URL, 34 | self.app_config().APP_DOCS_URL, 35 | config={ 36 | 'supportedSubmitMethods': ['get'], 37 | }) 38 | 39 | app = Application() 40 | -------------------------------------------------------------------------------- /application/address_service.py: -------------------------------------------------------------------------------- 1 | """User Service module""" 2 | import falcon 3 | from bson import json_util 4 | from application.base_service import BaseService 5 | from domain.domain_service import DomainService 6 | 7 | class AddressService(BaseService): 8 | 9 | def on_get(self, req, resp, user_id): 10 | address = DomainService.search_user_address(user=int(user_id)) 11 | if address: 12 | resp.status = falcon.HTTP_200 13 | resp.body = self.render_template('address.json', address=address) 14 | else: 15 | resp.status = falcon.HTTP_404 16 | resp.body = self.render_status(404, 'No address found!') 17 | -------------------------------------------------------------------------------- /application/base_service.py: -------------------------------------------------------------------------------- 1 | """Base service""" 2 | import json 3 | import yaml 4 | from jinja2 import Environment, PackageLoader 5 | 6 | class BaseService(object): 7 | def __init__(self): 8 | self.env = Environment(loader=PackageLoader('application', 'templates')) 9 | self.spec_env = Environment(loader=PackageLoader('application', 'specs')) 10 | 11 | def render_specs(self): 12 | template = self.spec_env.get_template('default.yml') 13 | rendered = template.render() 14 | return json.dumps(yaml.load(rendered)) 15 | 16 | def render_template(self, name, **kwargs): 17 | """Render the given @name template and gives args to it""" 18 | template = self.env.get_template(name) 19 | return template.render(**kwargs) 20 | 21 | def render_status(self, code, message): 22 | """Render a response status""" 23 | template = self.env.get_template('status.json') 24 | return template.render(code=code, message=message) 25 | -------------------------------------------------------------------------------- /application/environments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleniac/python-rest-ddd/34a1099b59c3cd9c21701c1f4084b7768e8c052d/application/environments/__init__.py -------------------------------------------------------------------------------- /application/environments/config.py: -------------------------------------------------------------------------------- 1 | """Module for app configs""" 2 | import os 3 | 4 | class Config(object): 5 | """Saves all configs as constants""" 6 | APP_DOCS_URL = '/api_docs.json' 7 | APP_SWAGGER_URL = '/apidocs' 8 | APP_ENVIRONMENT = os.environ.get('APP_ENVIRONMENT') 9 | APP_RELATIONAL_DATA = os.environ.get('APP_RELATIONAL_DATA') 10 | APP_RELATIONAL_DATA_HOST = os.environ.get('APP_RELATIONAL_DATA_HOST') 11 | APP_RELATIONAL_DATA_PORT = os.environ.get('APP_RELATIONAL_DATA_PORT') 12 | APP_RELATIONAL_DATA_USER = os.environ.get('APP_RELATIONAL_DATA_USER') 13 | APP_RELATIONAL_DATA_PASSWORD = os.environ.get('APP_RELATIONAL_DATA_PASSWORD') 14 | APP_RELATIONAL_DATA_DATABASE = os.environ.get('APP_RELATIONAL_DATA_DATABASE') 15 | APP_DOCUMENT_DATA = os.environ.get('APP_DOCUMENT_DATA') 16 | APP_DOCUMENT_DATA_HOST = os.environ.get('APP_DOCUMENT_DATA_HOST') 17 | APP_DOCUMENT_DATA_PORT = os.environ.get('APP_DOCUMENT_DATA_PORT') 18 | APP_JWT_SECRET = os.environ.get('APP_JWT_SECRET') 19 | APP_DEBUG_MODE = False 20 | 21 | class DevelopmentConfig(Config): 22 | """Extends the production config""" 23 | APP_DEBUG_MODE = True 24 | 25 | APP_ENVIRONMENT = os.environ.get('APP_ENVIRONMENT') 26 | 27 | CONFIG = Config if APP_ENVIRONMENT == 'production' else DevelopmentConfig 28 | -------------------------------------------------------------------------------- /application/environments/local/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.6 2 | 3 | MAINTAINER lnlwd 4 | 5 | ENV PACKAGES="\ 6 | gcc \ 7 | musl \ 8 | linux-headers \ 9 | build-base \ 10 | ca-certificates \ 11 | python3 \ 12 | python3-dev \ 13 | " 14 | 15 | RUN apk update \ 16 | && apk add --no-cache $PACKAGES || \ 17 | (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) \ 18 | 19 | && if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python3 /usr/bin/python; fi \ 20 | && if [[ ! -e /usr/bin/python-config ]]; then ln -sf /usr/bin/python3-config /usr/bin/python-config; fi \ 21 | && if [[ ! -e /usr/bin/idle ]]; then ln -sf /usr/bin/idle3 /usr/bin/idle; fi \ 22 | && if [[ ! -e /usr/bin/pydoc ]]; then ln -sf /usr/bin/pydoc3 /usr/bin/pydoc; fi \ 23 | && if [[ ! -e /usr/bin/easy_install ]]; then ln -sf /usr/bin/easy_install-3 /usr/bin/easy_install; fi \ 24 | 25 | && python3 -m ensurepip \ 26 | && pip3 install --no-cache-dir --upgrade pip setuptools \ 27 | && if [[ ! -e /usr/bin/pip ]]; then ln -sf /usr/bin/pip3 /usr/bin/pip; fi 28 | 29 | COPY ./application/environments/local/app/startup.sh /scripts/startup.sh 30 | RUN chmod +x /scripts/startup.sh 31 | 32 | COPY ./application/environments/local/wait-for /wait-for 33 | RUN chmod +x /wait-for 34 | 35 | ADD . /app 36 | 37 | WORKDIR /app 38 | 39 | RUN pip install -r requirements.txt 40 | 41 | EXPOSE 5000 42 | 43 | CMD ["/bin/sh"] 44 | -------------------------------------------------------------------------------- /application/environments/local/app/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | DEBUG="" 3 | 4 | if [ "$APP_ENVIRONMENT" = "development" ]; then 5 | DEBUG="--reload" 6 | fi 7 | 8 | gunicorn -b 0.0.0.0:5000 $DEBUG application:app.app 9 | -------------------------------------------------------------------------------- /application/environments/local/mongo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:edge 2 | 3 | MAINTAINER lnlwd 4 | 5 | RUN apk update \ 6 | && apk add --no-cache mongodb \ 7 | && mkdir /data && mkdir /data/db \ 8 | && mkdir /scripts \ 9 | && rm /usr/bin/mongoperf 10 | 11 | VOLUME ["/data/db"] 12 | 13 | EXPOSE 27017 14 | 15 | COPY ./application/environments/local/mongo/startup.sh /scripts/startup.sh 16 | RUN chmod +x /scripts/startup.sh 17 | 18 | ENTRYPOINT [ "/scripts/startup.sh" ] 19 | CMD [ "mongod" ] 20 | -------------------------------------------------------------------------------- /application/environments/local/mongo/seed/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:edge 2 | 3 | MAINTAINER lnlwd 4 | 5 | RUN apk update \ 6 | && apk add --no-cache mongodb-tools \ 7 | && rm -rf /var/cache/apk/ 8 | 9 | COPY ./application/environments/local/mongo/seed/*.sh /scripts/ 10 | RUN chmod +x /scripts/*.sh 11 | 12 | COPY ./application/environments/local/wait-for /wait-for 13 | RUN chmod +x /wait-for 14 | 15 | VOLUME ["/seed"] 16 | 17 | CMD ["/bin/sh"] 18 | -------------------------------------------------------------------------------- /application/environments/local/mongo/seed/falcon/addresses.bson.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleniac/python-rest-ddd/34a1099b59c3cd9c21701c1f4084b7768e8c052d/application/environments/local/mongo/seed/falcon/addresses.bson.gz -------------------------------------------------------------------------------- /application/environments/local/mongo/seed/falcon/addresses.metadata.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleniac/python-rest-ddd/34a1099b59c3cd9c21701c1f4084b7768e8c052d/application/environments/local/mongo/seed/falcon/addresses.metadata.json.gz -------------------------------------------------------------------------------- /application/environments/local/mongo/seed/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mongorestore --gzip --host mongo --port 27017 /seed 4 | -------------------------------------------------------------------------------- /application/environments/local/mongo/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Docker entrypoint (pid 1), run as root 3 | [ "$1" = "mongod" ] || exec "$@" || exit $? 4 | 5 | # Make sure that database is owned by user mongodb 6 | [ "$(stat -c %U /data/db)" = mongodb ] || chown -R mongodb /data/db 7 | 8 | # Drop root privilege (no way back), exec provided command as user mongodb 9 | cmd=exec; for i; do cmd="$cmd '$i'"; done 10 | exec su -s /bin/sh -c "$cmd" mongodb 11 | -------------------------------------------------------------------------------- /application/environments/local/mysql/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.6 2 | 3 | MAINTAINER lnlwd 4 | 5 | RUN apk update \ 6 | && apk add --no-cache mysql mysql-client \ 7 | 8 | && addgroup mysql mysql \ 9 | && mkdir /scripts \ 10 | && rm -rf /var/cache/apk/ 11 | 12 | VOLUME ["/var/lib/mysql"] 13 | 14 | COPY ./application/environments/local/mysql/*.sh /scripts/ 15 | RUN chmod +x /scripts/*.sh 16 | 17 | COPY ./application/environments/local/mysql/my.cnf /etc/mysql/my.cnf 18 | 19 | EXPOSE 3306 20 | 21 | ENTRYPOINT ["/scripts/startup.sh"] 22 | -------------------------------------------------------------------------------- /application/environments/local/mysql/my.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | user = root 3 | datadir = /app/mysql 4 | port = 3306 5 | log-bin = /app/mysql/mysql-bin 6 | -------------------------------------------------------------------------------- /application/environments/local/mysql/seed/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.6 2 | 3 | MAINTAINER lnlwd 4 | 5 | RUN apk update \ 6 | && apk add --no-cache mysql-client tar \ 7 | && rm -rf /var/cache/apk/ 8 | 9 | COPY ./application/environments/local/mysql/seed/*.sh /scripts/ 10 | RUN chmod +x /scripts/*.sh 11 | 12 | COPY ./application/environments/local/wait-for /wait-for 13 | RUN chmod +x /wait-for 14 | 15 | VOLUME ["/seed"] 16 | 17 | CMD ["/bin/sh"] 18 | -------------------------------------------------------------------------------- /application/environments/local/mysql/seed/seed.sql.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleniac/python-rest-ddd/34a1099b59c3cd9c21701c1f4084b7768e8c052d/application/environments/local/mysql/seed/seed.sql.tar.gz -------------------------------------------------------------------------------- /application/environments/local/mysql/seed/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | tar -xzvf /seed/seed.sql.tar.gz -C /seed 4 | mysql -u root -p$MYSQL_ROOT_PASSWORD -h mysql $MYSQL_DATABASE < /seed/seed.sql 5 | rm /seed/seed.sql 6 | -------------------------------------------------------------------------------- /application/environments/local/mysql/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -d /app/mysql ]; then 4 | echo "[i] MySQL directory already present, skipping creation" 5 | else 6 | echo "[i] MySQL data directory not found, creating initial DBs" 7 | 8 | mysql_install_db --user=root > /dev/null 9 | 10 | if [ "$MYSQL_ROOT_PASSWORD" = "" ]; then 11 | MYSQL_ROOT_PASSWORD=111111 12 | echo "[i] MySQL root Password: $MYSQL_ROOT_PASSWORD" 13 | fi 14 | 15 | MYSQL_DATABASE=${MYSQL_DATABASE:-""} 16 | MYSQL_USER=${MYSQL_USER:-""} 17 | MYSQL_PASSWORD=${MYSQL_PASSWORD:-""} 18 | 19 | if [ ! -d "/run/mysqld" ]; then 20 | mkdir -p /run/mysqld 21 | fi 22 | 23 | tfile=`mktemp` 24 | if [ ! -f "$tfile" ]; then 25 | return 1 26 | fi 27 | 28 | cat << EOF > $tfile 29 | USE mysql; 30 | FLUSH PRIVILEGES; 31 | GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY "$MYSQL_ROOT_PASSWORD" WITH GRANT OPTION; 32 | GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION; 33 | UPDATE user SET password=PASSWORD("") WHERE user='root' AND host='localhost'; 34 | EOF 35 | 36 | if [ "$MYSQL_DATABASE" != "" ]; then 37 | echo "[i] Creating database: $MYSQL_DATABASE" 38 | echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` CHARACTER SET utf8 COLLATE utf8_general_ci;" >> $tfile 39 | 40 | if [ "$MYSQL_USER" != "" ]; then 41 | echo "[i] Creating user: $MYSQL_USER with password $MYSQL_PASSWORD" 42 | echo "GRANT ALL ON \`$MYSQL_DATABASE\`.* to '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" >> $tfile 43 | fi 44 | fi 45 | 46 | /usr/bin/mysqld --user=root --bootstrap --verbose=0 < $tfile 47 | initial_data = `cat /backup/backup.sql` 48 | echo $initial_data 49 | /usr/bin/mysqld --user=root --bootstrap --verbose=1 < "USE $MYSQL_DATABASE;$initial_data" 50 | rm -f $tfile 51 | fi 52 | 53 | exec /usr/bin/mysqld --user=root --console 54 | -------------------------------------------------------------------------------- /application/environments/local/wait-for: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TIMEOUT=15 4 | QUIET=0 5 | 6 | echoerr() { 7 | if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi 8 | } 9 | 10 | usage() { 11 | exitcode="$1" 12 | cat << USAGE >&2 13 | Usage: 14 | $cmdname host:port [-t timeout] [-- command args] 15 | -q | --quiet Do not output any status messages 16 | -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout 17 | -- COMMAND ARGS Execute command with args after the test finishes 18 | USAGE 19 | exit "$exitcode" 20 | } 21 | 22 | wait_for() { 23 | command="$*" 24 | for i in `seq $TIMEOUT` ; do 25 | nc -z "$HOST" "$PORT" > /dev/null 2>&1 26 | 27 | result=$? 28 | if [ $result -eq 0 ] ; then 29 | if [ -n "$command" ] ; then 30 | exec $command 31 | fi 32 | exit 0 33 | fi 34 | sleep 1 35 | done 36 | echo "Operation timed out" >&2 37 | exit 1 38 | } 39 | 40 | while [ $# -gt 0 ] 41 | do 42 | case "$1" in 43 | *:* ) 44 | HOST=$(printf "%s\n" "$1"| cut -d : -f 1) 45 | PORT=$(printf "%s\n" "$1"| cut -d : -f 2) 46 | shift 1 47 | ;; 48 | -q | --quiet) 49 | QUIET=1 50 | shift 1 51 | ;; 52 | -t) 53 | TIMEOUT="$2" 54 | if [ "$TIMEOUT" = "" ]; then break; fi 55 | shift 2 56 | ;; 57 | --timeout=*) 58 | TIMEOUT="${1#*=}" 59 | shift 1 60 | ;; 61 | --) 62 | shift 63 | break 64 | ;; 65 | --help) 66 | usage 0 67 | ;; 68 | *) 69 | echoerr "Unknown argument: $1" 70 | usage 1 71 | ;; 72 | esac 73 | done 74 | 75 | if [ "$HOST" = "" -o "$PORT" = "" ]; then 76 | echoerr "Error: you need to provide a host and port to test." 77 | usage 2 78 | fi 79 | 80 | wait_for "$@" 81 | -------------------------------------------------------------------------------- /application/service_register.py: -------------------------------------------------------------------------------- 1 | """Service Register""" 2 | from application.status_service import StatusService 3 | from application.user_service import UserService 4 | from application.spec_service import SpecService 5 | from application.address_service import AddressService 6 | 7 | class ServiceRegister(object): 8 | 9 | @staticmethod 10 | def register_services(app): 11 | app.add_route('/', StatusService()) 12 | app.add_route('/user/{user_name}', UserService()) 13 | app.add_route('/address/{user_id}', AddressService()) 14 | 15 | app.add_route('/api_docs.json', SpecService()) 16 | -------------------------------------------------------------------------------- /application/spec_service.py: -------------------------------------------------------------------------------- 1 | """Status Service to tell if the application is running or not""" 2 | 3 | import falcon 4 | from application.base_service import BaseService 5 | 6 | class SpecService(BaseService): 7 | 8 | auth = { 9 | 'auth_disabled': True 10 | } 11 | 12 | def on_get(self, req, resp): # pylint: disable=unused-argument 13 | resp.status = falcon.HTTP_200 14 | resp.body = self.render_specs() 15 | -------------------------------------------------------------------------------- /application/specs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleniac/python-rest-ddd/34a1099b59c3cd9c21701c1f4084b7768e8c052d/application/specs/__init__.py -------------------------------------------------------------------------------- /application/specs/default.yml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | version: '1.0' 4 | title: Falcon API 5 | description: Falcon sample API 6 | host: falcon-api.org 7 | basePath: "/" 8 | schemes: 9 | - http 10 | - https 11 | paths: 12 | {% include "status.yml" %} 13 | definitions: 14 | Status: 15 | type: object 16 | properties: 17 | code: 18 | type: integer 19 | message: 20 | type: string 21 | -------------------------------------------------------------------------------- /application/specs/status.yml: -------------------------------------------------------------------------------- 1 | "/": 2 | get: 3 | description: Get application status 4 | tags: 5 | - status 6 | produces: 7 | - application/json 8 | security: [] 9 | deprecated: false 10 | responses: 11 | 200: 12 | description: Application status for healthchecking 13 | schema: 14 | $ref: '#/definitions/Status' 15 | examples: 16 | code: 200 17 | message: running 18 | -------------------------------------------------------------------------------- /application/status_service.py: -------------------------------------------------------------------------------- 1 | """Status Service to tell if the application is running or not""" 2 | 3 | import falcon 4 | from application.base_service import BaseService 5 | 6 | class StatusService(BaseService): 7 | auth = { 8 | 'auth_disabled': True 9 | } 10 | 11 | def on_get(self, req, resp): # pylint: disable=unused-argument 12 | resp.status = falcon.HTTP_200 13 | resp.body = self.render_status(200, 'running') 14 | -------------------------------------------------------------------------------- /application/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleniac/python-rest-ddd/34a1099b59c3cd9c21701c1f4084b7768e8c052d/application/templates/__init__.py -------------------------------------------------------------------------------- /application/templates/address.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "{{address._id}}", 3 | "type": "address", 4 | "attributes": { 5 | "user": {{address.user | tojson}}, 6 | "street": {{address.address.street | tojson}}, 7 | "number": {{address.address.number | tojson}}, 8 | "city": {{address.address.city | tojson}}, 9 | "province": {{address.address.province | tojson}}, 10 | "country": {{address.address.country | tojson}}, 11 | "zip": {{address.address.zip | tojson}} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /application/templates/status.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": {{code | tojson}}, 3 | "message": {{message | tojson}} 4 | } 5 | -------------------------------------------------------------------------------- /application/templates/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": {{user.user_id | tojson}}, 4 | "type": "user", 5 | "attributes": { 6 | "name": {{user.user_name | tojson}}, 7 | "cpf": {{user.user_cpf | tojson}} 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /application/user_service.py: -------------------------------------------------------------------------------- 1 | """User Service module""" 2 | import falcon 3 | from application.base_service import BaseService 4 | from domain.domain_service import DomainService 5 | 6 | class UserService(BaseService): 7 | 8 | def on_get(self, req, resp, user_name): 9 | user = DomainService.search_user_data(user_name=user_name) 10 | if user: 11 | resp.status = falcon.HTTP_200 12 | resp.body = self.render_template('user.json', user=user) 13 | else: 14 | resp.status = falcon.HTTP_404 15 | resp.body = self.render_status(404, 'No user found!') 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mysql: 4 | build: 5 | context: . 6 | dockerfile: ./application/environments/local/mysql/Dockerfile 7 | container_name: falcon_mysql 8 | ports: 9 | - "3307:3306" 10 | volumes: 11 | - mysql_data:/var/lib/mysql 12 | restart: on-failure 13 | environment: 14 | MYSQL_ROOT_PASSWORD: ckBuZG9tcDRzc3dvcmQK 15 | MYSQL_USER: falcon 16 | MYSQL_PASSWORD: falcon 17 | MYSQL_DATABASE: falcon 18 | healthcheck: 19 | test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] 20 | interval: 10s 21 | timeout: 5s 22 | retries: 10 23 | 24 | mysql_seed: 25 | build: 26 | context: . 27 | dockerfile: ./application/environments/local/mysql/seed/Dockerfile 28 | container_name: falcon_mysql_seed 29 | command: sh -c '/wait-for mysql:3306 -t 100 -- /scripts/startup.sh' 30 | volumes: 31 | - ./application/environments/local/mysql/seed:/seed 32 | links: 33 | - "mysql" 34 | depends_on: 35 | - "mysql" 36 | restart: on-failure 37 | environment: 38 | MYSQL_DATABASE: falcon 39 | MYSQL_ROOT_PASSWORD: ckBuZG9tcDRzc3dvcmQK 40 | 41 | mongo: 42 | build: 43 | context: . 44 | dockerfile: ./application/environments/local/mongo/Dockerfile 45 | container_name: falcon_mongo 46 | ports: 47 | - "27017:27017" 48 | volumes: 49 | - mongo_data:/data/db 50 | restart: on-failure 51 | 52 | mongo_seed: 53 | build: 54 | context: . 55 | dockerfile: ./application/environments/local/mongo/seed/Dockerfile 56 | container_name: falcon_mongo_seed 57 | command: sh -c '/wait-for mongo:27017 -t 100 -- /scripts/startup.sh' 58 | volumes: 59 | - ./application/environments/local/mongo/seed:/seed 60 | links: 61 | - "mongo" 62 | depends_on: 63 | - "mongo" 64 | restart: on-failure 65 | environment: 66 | MONGODB_DATABASE: falcon 67 | 68 | api: 69 | build: 70 | context: . 71 | dockerfile: ./application/environments/local/app/Dockerfile 72 | container_name: falcon_api 73 | command: sh -c '/wait-for mysql:3306 -t 100 -- /scripts/startup.sh' 74 | ports: 75 | - "5000:5000" 76 | volumes: 77 | - .:/app 78 | links: 79 | - "mysql" 80 | - "mongo" 81 | depends_on: 82 | - "mysql" 83 | restart: on-failure 84 | environment: 85 | APP_ENVIRONMENT: development 86 | APP_JWT_SECRET: qwertyuiopasdfghjklzxcvbnm123456 87 | APP_RELATIONAL_DATA: mysql 88 | APP_RELATIONAL_DATA_HOST: mysql 89 | APP_RELATIONAL_DATA_PORT: 3306 90 | APP_RELATIONAL_DATA_USER: falcon 91 | APP_RELATIONAL_DATA_PASSWORD: falcon 92 | APP_RELATIONAL_DATA_DATABASE: falcon 93 | APP_DOCUMENT_DATA: mongodb 94 | APP_DOCUMENT_DATA_HOST: mongo 95 | APP_DOCUMENT_DATA_PORT: 27017 96 | PYTHONUNBUFFERED: 'TRUE' 97 | 98 | 99 | volumes: 100 | mysql_data: 101 | mongo_data: 102 | -------------------------------------------------------------------------------- /domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleniac/python-rest-ddd/34a1099b59c3cd9c21701c1f4084b7768e8c052d/domain/__init__.py -------------------------------------------------------------------------------- /domain/domain_service.py: -------------------------------------------------------------------------------- 1 | """Domain Service Module""" 2 | from domain.repositories import AllUsers, AllAddresses 3 | 4 | class DomainService(object): 5 | 6 | @staticmethod 7 | def search_user_data(**kwargs): 8 | return AllUsers.find_user_by(**kwargs) 9 | 10 | @staticmethod 11 | def search_user_address(**kwargs): 12 | return AllAddresses.find_by(**kwargs) 13 | -------------------------------------------------------------------------------- /domain/entities.py: -------------------------------------------------------------------------------- 1 | """Entities module""" 2 | from pony import orm 3 | from infrastructure.mysql import relational_database 4 | 5 | 6 | class User(relational_database.db.Entity): 7 | _table_ = 'users' 8 | user_id = orm.PrimaryKey(int, auto=True, column='id_usuario') 9 | user_name = orm.Required(str, column='nr_nome') 10 | user_cpf = orm.Optional(str, column='nr_cpf') 11 | 12 | relational_database.db.generate_mapping(create_tables=False) 13 | -------------------------------------------------------------------------------- /domain/repositories.py: -------------------------------------------------------------------------------- 1 | """Repositories modules""" 2 | from pony import orm 3 | from domain.entities import User 4 | from infrastructure.mongo import document_database 5 | 6 | class AllUsers(object): 7 | 8 | @staticmethod 9 | @orm.db_session 10 | def find_user_by(**kawrgs): 11 | return User.get(**kawrgs) 12 | 13 | class AllAddresses(object): 14 | collection = document_database.client.falcon.addresses 15 | 16 | @classmethod 17 | def find_by(cls, **kwargs): 18 | address = cls.collection.find_one(kwargs) 19 | return address 20 | -------------------------------------------------------------------------------- /infrastructure/mongo.py: -------------------------------------------------------------------------------- 1 | """Document database module""" 2 | import dependency_injector.providers as providers 3 | from pymongo import MongoClient 4 | from application.environments.config import CONFIG 5 | 6 | class Database(object): 7 | def __init__(self): 8 | self.app_config = providers.Singleton(CONFIG) 9 | self.client = MongoClient('{0}://{1}:{2}/'.format(self.app_config().APP_DOCUMENT_DATA, 10 | self.app_config().APP_DOCUMENT_DATA_HOST, 11 | self.app_config().APP_DOCUMENT_DATA_PORT)) 12 | 13 | document_database = Database() 14 | -------------------------------------------------------------------------------- /infrastructure/mysql.py: -------------------------------------------------------------------------------- 1 | """Relational database module""" 2 | import dependency_injector.providers as providers 3 | from pony import orm 4 | from application.environments.config import CONFIG 5 | 6 | class Database(object): 7 | def __init__(self): 8 | self.app_config = providers.Singleton(CONFIG) 9 | self.db = orm.Database() 10 | self.db.bind(self.app_config().APP_RELATIONAL_DATA, 11 | host=self.app_config().APP_RELATIONAL_DATA_HOST, 12 | port=int(self.app_config().APP_RELATIONAL_DATA_PORT), 13 | user=self.app_config().APP_RELATIONAL_DATA_USER, 14 | passwd=self.app_config().APP_RELATIONAL_DATA_PASSWORD, 15 | db=self.app_config().APP_RELATIONAL_DATA_DATABASE) 16 | 17 | relational_database = Database() 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | falcon==1.2.0 2 | falcon-auth==1.0.3 3 | gunicorn==19.7.1 4 | Jinja2==2.11.3 5 | pony==0.7.1 6 | PyMySQL==0.7.11 7 | dependency-injector==3.4.8 8 | falcon-swagger-ui==0.0.7 9 | PyYAML==5.4 10 | pymongo==3.5.1 11 | --------------------------------------------------------------------------------