{"id":15874344,"url":"https://github.com/garutilorenzo/simple-bottlepy-application","last_synced_at":"2026-04-29T22:40:25.063Z","repository":{"id":154321810,"uuid":"436725136","full_name":"garutilorenzo/simple-bottlepy-application","owner":"garutilorenzo","description":"A very simple Python Bottle application","archived":false,"fork":false,"pushed_at":"2021-12-29T16:31:08.000Z","size":330,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-01T22:48:31.033Z","etag":null,"topics":["bootstrap5","bottle","bottlepy","postgresql","python3","redis","rest-api","sqlalchemy","webapp"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/garutilorenzo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-12-09T18:44:21.000Z","updated_at":"2023-04-26T12:55:08.000Z","dependencies_parsed_at":null,"dependency_job_id":"9e2d960f-5485-48d1-a3da-a4b8c17d0dd4","html_url":"https://github.com/garutilorenzo/simple-bottlepy-application","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/garutilorenzo/simple-bottlepy-application","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/garutilorenzo%2Fsimple-bottlepy-application","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/garutilorenzo%2Fsimple-bottlepy-application/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/garutilorenzo%2Fsimple-bottlepy-application/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/garutilorenzo%2Fsimple-bottlepy-application/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/garutilorenzo","download_url":"https://codeload.github.com/garutilorenzo/simple-bottlepy-application/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/garutilorenzo%2Fsimple-bottlepy-application/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279005265,"owners_count":26083861,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-10T02:00:06.843Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bootstrap5","bottle","bottlepy","postgresql","python3","redis","rest-api","sqlalchemy","webapp"],"created_at":"2024-10-06T01:21:53.512Z","updated_at":"2025-10-10T20:14:07.006Z","avatar_url":"https://github.com/garutilorenzo.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Simple python Bottle application\n\n[![docker swarm ingress CI](https://github.com/garutilorenzo/simple-bottlepy-application/actions/workflows/ci.yml/badge.svg)](https://github.com/garutilorenzo/simple-bottlepy-application/actions/workflows/ci.yml)\n[![GitHub issues](https://img.shields.io/github/issues/garutilorenzo/simple-bottlepy-application)](https://github.com/garutilorenzo/simple-bottlepy-application/issues)\n![GitHub](https://img.shields.io/github/license/garutilorenzo/simple-bottlepy-application)\n[![GitHub forks](https://img.shields.io/github/forks/garutilorenzo/simple-bottlepy-application)](https://github.com/garutilorenzo/simple-bottlepy-application/network)\n[![GitHub stars](https://img.shields.io/github/stars/garutilorenzo/simple-bottlepy-application)](https://github.com/garutilorenzo/simple-bottlepy-application/stargazers)\n\nA very simple web application written in python.\n\n# Table of Contents\n\n* [Requirements](#requirements)\n* [The application stack](#the-application-stack)\n* [Data used in this example site](#data-used)\n* [Setup the environmant](#setup-the-environmant)\n* [Download sample data](#download-sample-data)\n* [Start the environment](#start-the-environment)\n* [Application Overview](#application-overview)\n* [App configuration](#app-configuration)\n* [DB configuration](#db-configuration)\n* [SQLAlchemy plugin](#sqlalchemy-plugin)\n* [Redis Cache](#redis-cache)\n* [Data export](#data-export)\n* [Data import](#data-import)\n\n## Requirements \n\nTo use this environment you need [Docker](https://docs.docker.com/get-docker/) an [Docker compose](https://docs.docker.com/compose/install/) installed.\n\n## The application stack\n\nBackend:\n\n* [BottlePy](https://bottlepy.org/docs/dev/)\n* [SQLAlchemy](https://www.sqlalchemy.org/)\n\nFrontend:\n\n* [Bootstrap 5](https://getbootstrap.com/)\n* [jQuery](https://jquery.com/)\n\nDatabase:\n\n* [PostgreSQL](https://www.postgresql.org/)\n* [Redis](https://redis.io/)\n\nWebserver:\n\n* [Nginx](https://www.nginx.com/) - Only Prod env.\n\n## Data used\n\nTo use this example application we need to import some example data. This example application use the \"Stack Exchange Data Dump\" available on [archive.org](https://archive.org/details/stackexchange).\n\nAll the data used by this site is under the [cc-by-sa 4.0](https://creativecommons.org/licenses/by-sa/4.0/) license.\n\n## Setup the environmant\n\nWe are now ready to setup our environment. For dev purposes link the docker-compose-dev.yml do docker-compose.yml\n\n```bash\nln -s docker-compose-dev.yml docker-compose.yml\n```\n\nFor prod environments:\n\n```bash\nln -s docker-compose-dev.yml docker-compose.yml\n```\n\nThe difference between dev and prod envs are:\n\n|   Prod   |   Dev   |\n| -------- | ------- |\n| Nginx is used to expose our example application | Built-in HTTP development server |\n| Http port 80 | Http port 8080 |\n| Debug mode is disabled | Debug mode is enabled |\n| Reloader is disabled | Reloader is enabled |\n\nNow we can download some sample data.\n\n## Download sample data\n\nTo dwonload some example data run:\n\n```bash\n./download_samples.sh\n```\n\nBy default the archive with the 'meta' attribute will be downloaded. If you want more data remove in download_samples.sh 'meta' form the archive name.\n\nSmall data:\n\n```bash\nfor sample in workplace.meta.stackexchange.com.7z unix.meta.stackexchange.com.7z\n```\n\nBig data:\n\n\n```bash\nfor sample in workplace.stackexchange.com.7z unix.stackexchange.com.7z\n```\n\n**Note** not all the stackexchange sites where imported on this example. After you choose the archives you will download adjust the network.py schema under src/schema/\n\n```python\nclass Sites(enum.Enum):\n    vi = 'vi.stackexchange.com'\n    workplace = 'workplace.stackexchange.com'\n    wordpress = 'wordpress.stackexchange.com'\n    unix = 'unix.stackexchange.com'\n    tex = 'tex.stackexchange.com'\n```\n\nOnce the data is downloaded we can import the data:\n\n```bash\ndocker-compose run --rm bottle bash\n\nweb@4edf053b7e4f:~/src$  python init_db.py # \u003c- Initialize DB\nweb@4edf053b7e4f:~/src$  python import_data.py # \u003c- Import sample data\n```\n\nNow for each data sample you have downloaded (Eg. tex, unix, vi) a python subprocess is started and will import in order:\n\n* all the tags\n* all the users\n* all the posts\n* all the post history events\n\nOnce the import is finished we can start our environment\n\n## Start the environment\n\nWith our DB populated we can now start our web application:\n\n```bash\ndocker-compose up -d\n\nCreating network \"bottle-exchange_default\" with the default driver\nCreating postgres ... done\nCreating redis    ... done\nCreating bottle   ... done\n```\n\nThe application will be available at http://localhost:8080 for the dev and http://localhost for the prod\n\n## Application Overview\n\n### Index\n\nHome page with a search form. On every change on the \"Network\" select, the \"Tags\" select is populated by an ajax call on /api/autocomplete/form/get_tags (POST) (for more details see src/bottle/static/asset/js/custom.js).\n\nThe POST call is authenticated with a random hard coded string (see src/bottle/app.py, api_get_tags)\n\n### Tags\n\nList of all available tags with a pagination nav.\nClicking on the tag name the application will search all questions matching the tag you have selected, by clicking the site name the application will search all questions matching the tag and the site you ave selected.\n\n### Users\n\nTable view of all available users with a pagination nav.\n\nClicking on the username, we enter on the detail's page of the user. In the details user page we see: UP Votes,Views,Down Votes. If the user has populated the \"About me\" field, we see a button that trigger a modal with the about me details.\nIf the user ha asked or answered some question we see a list of question in the \"Post\" section.\n\n### Posts\n\nList of all posts with a pagination nav\n\n### API/REST endpoint\n\nThis application expose one api/rest route: /api/get/tags. You can query this route making a POST call, you have to make the call using a json payload:\n\n```bash\ncurl --header \"Content-Type: application/json\" \\\n  --request POST \\\n  --data '{\"auth_key\":\"dd4d5ff1c13!28356236c402d7ada.aed8b797ebd299b942291bc66,f804492be2009f14\"}' \\\n  http://localhost:8080/api/get/tags | jq\n\n {\n  \"data\": [\n    {\n      \"clean_name\": \"html5\",\n      \"created_time\": \"2021-12-29 11:33:06.517152+00:00\",\n      \"id\": \"1\",\n      \"name\": \"html5\",\n      \"network_sites\": \"Sites.wordpress\",\n      \"questions\": \"91\",\n      \"tag_id\": \"2\",\n      \"updated_time\": \"None\"\n    },\n    ...\n    ],\n  \"errors\": [],\n  \"items\": 5431,\n  \"last_page\": 27\n}\n```\n\nThe auth_key is hard coded in src/bottle/app.py\n\n## App configuration\n\nThe application's configuration are loaded by the load_config module (src/load_config.py).\n\nThis module will load a .yml file under:\n\n```\n/app/src/\u003cBOTTLE_APP_NAME\u003e/config/\u003cBOTTLE_APP_ENVIRONMENT\u003e\n```\n\n*BOTTLE_APP_NAME* and *BOTTLE_APP_ENVIRONMENT* are environment variables.\n\nBOTTLE_APP_NAME is the name of the path where our bottle application lives, in this case *bottle*. BOTTLE_APP_ENVIRONMENT value is prod or env.\n\nAn example configuration is:\n\n```yaml\n---\nenable_debug: True\nenable_reloader: True\nhttp_port: 8080\npgsql_username: \"bottle\"\npgsql_password: \"b0tTl3_Be#\"\npgsql_db: \"bottle_exchange\"\npgsql_host: \"pgsql\"\npgsql_port: 5432\ncreate_db_schema: True\ndefault_result_limit: 50\n```\n\n## DB configuration\n\nThe database configuration is defined under the src/schema module.\n\nThe base.py file contains the engine configuration:\n\n```python\nimport load_config # \u003c- See App configuration\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import sessionmaker\n\nmain_config = load_config.load_config()\nconn_string = 'postgresql+psycopg2://{pgsql_username}:{pgsql_password}@{pgsql_host}:{pgsql_port}/{pgsql_db}'.format(**main_config)\n\nengine = create_engine(conn_string, pool_size=80, pool_recycle=60)\nSession = sessionmaker(bind=engine)\n\nBase = declarative_base()\n```\n\nAll the tables are defined in a separate file always under schema:\n\n| schema  |  Tables  | Description |\n| ------- | ------- | ----------- |\n| `network.py` | None | Sites class is not a SQLAlchemy object, but is an Enum used by all the other tables |\n| `posts.py` | posts,post_history | Table definition for post and post_history tables. This module contains also three enum definitions: PostType, PostHistoryType, CloseReason |\n| `tags.py` | tags | Tags table |\n| `users.py` | users | Users table |\n\n\n## SQLAlchemy plugin\n\nIn this example application is used and insalled a SQLAlchemy plugin (src/bottle/bottle_sa.py). This plugin is used to handle the SQLAlchemy session:\n\n```python\nfrom schema.base import Base, engine # \u003c- Base and engine are defined in the schema module, see \"DB configuration\"\nfrom bottle_sa import SQLAlchemyPlugin\nimport load_config\n\nmain_config = load_config.load_config()\n\n# Main Bottle app/application\napp = application = Bottle()\n\n# DB Plugin\nsaPlugin = SQLAlchemyPlugin(\n    engine=engine, metadata=Base.metadata, \n    create=main_config['create_db_schema'], \n    config=main_config,\n)\napplication.install(saPlugin)\n```\n\nThis plugin pass an extra parameter on each function defined in src/bottle/app.py. By default this parameter is 'db', but it can be changed by passing the extra parameter 'keyword' on the SQLAlchemyPlugin init.\n\nSo an example function will be:\n\n```python\n@app.route('/docs')\n@view('docs')\ndef index(db): # \u003c- db is our SQLAlchemy session\n    return dict(page_name='docs')\n```\n\n## Redis Cache\n\nIn this example application we use redis to cache some pages. The caching \"approach\" is very useful if you have:\n\n* a site with few updates\n* a slow page/route \n* you have to decrease the load of your DB\n\nThe RedisCache is defined in src/bottle/bottle_cache.py and this is an example usage:\n\n```python\nfrom bottle_cache import RedisCache\n\n# Cache\ncache = RedisCache()\n\n@app.route('/tags')\n@app.route('/tags/\u003cpage_nr:int\u003e')\n@cache.cached()\n@view('tags')\ndef get_tags(db, page_nr=1):\n    do_something()\n    return something\n```\n\nYou can init RedisCache class with an extra parameter config:\n\n```python\nconfig = {'redis_host': '\u003credis_hostname'\u003e, 'redis_port': 6379, 'redis_db': 0, 'cache_expiry': 86400}\ncache = RedisCache(config=config)\n```\n\nBy default the configurations are:\n\n| Param   | Default | Description |\n| ------- | ------- | ----------- |\n| `redis_host` | `redis` | Redis FQDN or ip address |\n| `redis_port` | `6379` | Redis listen port |\n| `redis_db` | `0` | Redis database |\n| `cache_expiry` | `3600` | Global cache expiry time in seconds |\n\nThe @cached decorator can accept some arguments:\n\n| Param   | Default | Description |\n| ------- | ------- | ----------- |\n| `expiry` | `None` | Route cache expiry time. If not defined is the same value as the global expiry time |\n| `key_prefix` | `bottle_cache_%s` | Redis key prefix |\n| `content_type` | `text/html; charset=UTF-8` | Default content type |\n\n### Caching json requests\n\nA json caching example would be:\n\n```python\n@app.route('/api/get/tags', method='POST')\n@cache.cached(content_type='application/json')\ndef api_get_tags(db):\n    do_something()\n    return something\n```\n\n### Invalidate cache\n\nTo invalidate the cache pass **invalidate_cache** key as query parameter or in the body request if you make a POST call\n\n### Skip/bypass cache\n\nTo skip or bypass the cache pass **skip_cache** key as query parameter or in the body request if you make a POST call\n\n## Data export\n\nTo backup PgSQL data run dump_db.sh\n\n```bash\n./dump_db.sh\n```\n\nthe dump will be placed in the root directory of this repository, the file will be named dump.sql.gz (Gzipped format)\n\n## Data import\n\nTo import an existing DB uncomment the following line in the docker-compose.yml:\n\n```yaml\nvolumes:\n    - type: volume\n      source: postgres\n      target: /var/lib/postgresql/data\n    - ./sql:/docker-entrypoint-initdb.d # \u003c- uncomment this line\n```\n\nand plache your dump in gzip or plain text format under sql/ (create the directory first)\n\n## Stop the environment\n\nTo stop the environment run\n\n```bash\ndocker-compose down\n\nStopping bottle   ... done\nStopping postgres ... done\nStopping redis    ... done\nRemoving bottle   ... done\nRemoving postgres ... done\nRemoving redis    ... done\nRemoving network bottle-exchange_default\n```\n\nTo clean up all the data (pgsql data) pass the extra argument \"-v\" to docker-compose down. With this parameter the pgsql volume will be deleted.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgarutilorenzo%2Fsimple-bottlepy-application","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgarutilorenzo%2Fsimple-bottlepy-application","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgarutilorenzo%2Fsimple-bottlepy-application/lists"}