{"id":25770847,"url":"https://github.com/borfast/acmemegastore","last_synced_at":"2025-02-27T02:38:37.450Z","repository":{"id":50156234,"uuid":"88416267","full_name":"borfast/acmemegastore","owner":"borfast","description":"Acme Megastore demo project","archived":false,"fork":false,"pushed_at":"2023-07-26T22:13:32.000Z","size":418,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2023-07-26T23:43:56.817Z","etag":null,"topics":["aiohttp","aiohttp-server","api","demo","demo-app","flask"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/borfast.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}},"created_at":"2017-04-16T13:49:45.000Z","updated_at":"2023-07-26T23:43:56.818Z","dependencies_parsed_at":"2023-01-25T13:45:15.385Z","dependency_job_id":null,"html_url":"https://github.com/borfast/acmemegastore","commit_stats":null,"previous_names":[],"tags_count":0,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borfast%2Facmemegastore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borfast%2Facmemegastore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borfast%2Facmemegastore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borfast%2Facmemegastore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/borfast","download_url":"https://codeload.github.com/borfast/acmemegastore/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240967289,"owners_count":19886215,"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","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":["aiohttp","aiohttp-server","api","demo","demo-app","flask"],"created_at":"2025-02-27T02:38:37.045Z","updated_at":"2025-02-27T02:38:37.440Z","avatar_url":"https://github.com/borfast.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Codacy Badge](https://api.codacy.com/project/badge/Grade/60bf2812dbb54e03ace2a9baa9f205dd)](https://app.codacy.com/app/borfast/acmemegastore?utm_source=github.com\u0026utm_medium=referral\u0026utm_content=borfast/acmemegastore\u0026utm_campaign=badger)\n[![Build Status](https://travis-ci.org/borfast/acmemegastore.svg?branch=master)](https://travis-ci.org/borfast/acmemegastore)\n[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/borfast/acmemegastore/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/borfast/acmemegastore/?branch=master)\n\nThis is a small sample project I made in the past for a job application. I\ndecided to publish it on Github, so I cleaned it up, updated it and changed\nit so it's not easily found by other applicants trying to cheat.\n\nIt implements a new feature for the imaginary Acme Megastore e-commerce\nplatform. It produces a list of popular purchases, so customers can see who\nelse bought the same products as them.\n\nIt accepts HTTP requests to `/api/recent_purchases/:username` and responds\nwith a list of recently purchased products, and the names of other users who\nrecently purchased them.\n\nOriginally, data about users, products, and purchases was available via an\nexternal API referred to as \"data API\", provided to me by an outside server.\nIn order to openly publish the code without revealing the company and\nwithout depending on their server, I reimplemented the data API using Flask.\nAll its code is in `acme/data`, specifically the `acme/data/data_server.py`\nfile. In order to simulate network latency and show caching in effect, the\ndata API delays its response by a random amount. A full reference for the\ndata API is available in `doc/data_api.md`. If you run `docker-composer up`,\na container running it will be launched for you. For now, there are there are\nno tests for the data API since it wasn't something I wrote for the original\nsubmission. I might add them later.\n\nThere were two specific details requested:\n* The application must cache API requests so that it can respond as quickly\n as possible.\n* If a given username cannot be found, the API should respond with\n \"User with username of '{{username}}' was not found\"\n\nThis is how it was requested that the code determined the \"Popular purchases\":\n\n* Fetch 5 recent purchases for the given user:\n`GET /api/purchases/by_user/:username?limit=5`\n* For each of the resulting products, get a list of all people who previously\npurchased that product: `GET /api/purchases/by_product/:product_id`\n* At the same time, request info about the products:\n `GET /api/products/:product_id`\n* Finally, put all of the data together and sort it so that the product with\n the highest number of recent purchases is first.\n\nExample response:\n```\n[\n {\n   \"id\": 555622,\n   \"face\": \"｡◕‿◕｡\",\n   \"price\": 1100,\n   \"size\": 27,\n   \"recent\": [\n     \"Frannie79\",\n     \"Barney_Bins18\",\n     \"Hortense6\",\n     \"Melvina84\"\n   ]\n },\n ...\n]\n```\n\nThe remainder of this file is my original \"report\" for the job application.\n\n\n### README.md\n\nThis over-sized readme file is meant to explain the rationale behind some of\nthe decisions I made while writing this, as well as allow you to navigate the\nproject and understand how to run it.\n\nEven though this is a ridiculously simple application, I did some things in a\nbit more serious way in order to demonstrate some more skills and knowledge\nfor larger scale and more complex applications.\n\nFor example, I could have done all of this in one single file in a\nmonolithic way but I preferred to keep things as modular, independent\nand decoupled as possible to demonstrate some principles which I consider\nimportant, such as dependency injection (note that I'm not talking about a\nDI framework or IoC container, just the basic pattern of injecting\ndependencies into an object or function instead of creating them in it, or\nletting it find the dependency), unit tests, etc.\n\nOne of the advantages of DI is that it allows for much easier isolated testing\nand as such, I also wrote a few tests. I didn't aim for 100% coverage but the\ntests I wrote should be enough for demonstration purposes.  In a real project\nI would aim for much higher test coverage.\n\nAnother important principle that works in tandem with DI is the Liskov\nSubstitution Principle (LSP), which I demonstrate on the Cache classes and\nwhich allows for the easy substitution of the RedisCache with a MemoryCache\nin the API tests. The current implementation uses Redis as a cache backend\nbut by having an abstract base cache class defining an \"interface\" and\nhaving the Api class based on that interface, replacing Redis with something\nelse would be a simple matter of writing a new cache class implementing the\n5 methods from AbstractCache and passing an instance of it to the Api\nconstructor instead of an instance of RedisCache. I didn't do this for other\nclasses to save time.\n\nAsyncio and aiohttp were used for the implementation because Acme Megastore's\nbusiness is clearly growing at an unprecedented pace and their\ne-commerce platform will need to be able to handle many concurrent users, a\ngoal which is better served by an asynchronous server than traditional\nblocking code. Plus, it's more impressive than just plain-old blocking code\nand thus I can show off a little more. :-)\n\nRedis was used as a cache backend. I could have just as easily used\nmemcached but there's no significant difference in performance and Redis is\nmore versatile, meaning that if necessary it could be used for other purposes\nwithout having to introduce another piece of technology into the application\nstack. Still, if a different cache backend was required, it could easily be\nreplaced as stated above. An important note regarding Redis and the cache: to\nkeep things simple I did not worry about persistent storage for Redis, which\nmeans that depending on the configuration present in the Redis container, each\ntime Redis is restarted, all the cache may be wiped.\n\nIn order to simplify the task of getting the application up and running and\nsince Docker is so prevalent these days, I created the necessary files\n(`Dockerfile` and `docker-compose.yml`) to run the application without having\nto set up anything manually. The last section of this file contains\ninstructions for running the application in Docker and also manually, if\nrequired, or if you want to run the provided unit tests.\n\n\n## Project structure\n\nThis is not meant to be an exhaustive description of each file and\ndirectory, just a brief overview of the non-obvious, most important files and\nof how things are organised.\n\n### Environment variables\n\nThe application reads some configuration values from environment variables.\n\nIn order to make it easier to set up, it can also read the necessary values \nfrom a `.env` file in the project root. This file does not exist in the \nrepository, since it's unique to each system it will run on. \n \nThere is a file called `dot_env_example` which you can use as a base to \ncreate your own `.env`. The variable names should be self-explanatory.\n\nFetching configuration data from environment variables allows the\napplication to run seamlessly on platforms such as Heroku,\nwhich use environment variables to pass configuration settings to\nthe applications. It also allows you to create custom Docker images more \neasily without having to hard-code any settings. Finally, it allows you\nto keep configuration separated from the code, making it safer (no \ncredentials to be compromised) and easier to scale or move the application \nto more or different servers.\n\n### Dependencies and Pipenv\n\nThis project uses [Pipfile](https://github.com/pypa/pipfile) to declare its \ndependencies. You can install them using [Pipenv](http://pipenv.org/) (see \nthe \"Installing Python 3.6\" section further down for details).\n\n### `fixtures` and `test` directories\n\nIn order to avoid having to use the data API while testing, I collected\nsome data from it and saved it to JSON files so I could make some of the\ntests a bit more realistic. These files are saved in the `test/fixtures`\ndirectory. I could also have done this with Faker, with the added bonus of\nbeing more future-proof but this way it was quicker.\n\nUnsurprisingly, the `test` directory contains the unit tests.\n\n### `acme` directory\n\nThis is the meat of the project. It's where all the source code is kept. The\nmain file is `server.py`, from which you can follow the usage of the\nother files and the execution logic of the application.\n\nAlmost all files here include a single class, which is responsible for a\nsingle thing. For instance, `api.py` contains the `Api` class, which is\nresponsible for communicating with the data API, and nothing else. Caching\nis handled elsewhere (in `cache.py`), assembling the results is handled\nelsewhere (in `popular.py`), etc.\n\nThe code first creates an instance of the `Megastore` class (which lives in\n`Megastore.pyore.py`) and uses it as the handler for the aiohttp\n`web.Application` instance. `Megastore` has some dependencies that must be\ninjected, though, and these dependencies also have other dependencies, so\nbefore instantiating `Megastore` I create instances of those classes (`Api`,\n`AsyncHttpClient`, `RedisCache`, etc) and pass them where necessary.\n\nNote that `server.py` is where all the dependencies are created. No\ndependency creation occurs within other classes. For bigger projects, a\ndependency injection framework may be useful but in this case, it would be\noverkill.\n\n## Assumptions and shortcuts\n\nIn order to save some time, I made some assumptions and took some shortcuts\nwhere I considered they wouldn't hurt the performance of the application or\nthe demonstration of my skills. In a real project, I would have clarified\nsome of these beforehand with a project owner or manager. These are\nhopefully all of them, as far as I can recall. They are also documented in\ncomments in the code where they take place.\n\n* I didn't write tests for absolutely everything, only enough for\ndemonstration purposes.\n\n* I didn't create abstract base classes / \"interfaces\" for every case.\n\n* I assumed when someone accesses the API without specifying a username, it\nshould return a 404.\n\n* When fetching data from the data API, as long as the response doesn't have\nan error status code, I immediately decode the response assuming it is a\nJSON string.\n\n* When counting the number of purchases per product, I assumed the same\nperson buying the same product twice counts as two purchases for the purpose\nof sorting the final list of popular products by number of purchases. The other\npossibility would be to count just one purchase per person, even if that person\nbought the same product more than once, which effectively is counting how many\npeople bought the product and not how many total purchases the product had.\n\n* For the final list of popular products, I assumed the end user is not\ninterested in knowing how many times someone purchased a certain product, so\nany duplicated user names are removed.\n\n\n## How to run the project\n\nThere are two ways to run the project:\n\n1. Isolated in a couple of Docker containers;\n2. Manually / directly in your machine.\n\nThe first option is probably the easiest. It sets up a couple of Docker\ncontainers with everything ready for you, while the second option requires\nthat you install everything manually.\n\nNote that in order to run the supplied unit tests you have to use the manual\nmethod (see below). It is not required to have Redis running nor access to\nthe data API, since these services are all mocked in the tests.\n\n\n### Running with Docker\n\nFor this option you just need Docker and Docker Compose installed. For\nWindows and Mac users, the Docker Toolbox contains everything you need and\nit's probably the easiest way to go. Linux installation requires a couple\nextra steps but it's not complicated. They have instructions here:\nhttps://docs.docker.com/compose/install/\n\nAfter having Docker and Docker Compose installed, running the project is as\nsimple as executing `docker-compose up` inside the `docker` directory.\n\nYou should see some output from Docker building the images (only the first\ntime you run it), then Redis will also output some information and the API will\nbe available at `http://localhost:8080/api/recent_purchases/`\n\n### Running directly on your machine\n\nThis option is a bit more involved since it requires installing some\nsoftware, namely Python 3.6 (which is not commonly installed in operating\nsystems our-of-the-box), Redis and a few Python packages.\n\n#### Installing Python 3.6\n\nYou need Python 3.5 or newer because of the async/await syntax which is only\navailable from 3.5 and later.\n\nTo install Python I strongly recommend using\n[pyenv](https://github.com/yyuu/pyenv) and for the remainder of these \ninstructions I assume you are using it. I suggest you use the automatic \ninstaller, since it installs pyenv and its companion tools for you, as well \nas a handy `pyenv update` command. Pyenv allows you to have multiple Python \nversions installed in your system without conflicting with one another.\n\nAfter having pyenv installed you need to install Python 3.5 or newer. At the\ntime of writing the most recent version is 3.6.4 and I will assume that's\nthe version you will use. You can install it with `pyenv install 3.6.4`. \nThis might take a few minutes.\n\nNow you can ask Pipenv to install the project dependencies by running \n`pipenv install --python 3.6.4` if you want only the production dependencies,\nor  `pipenv install --dev --python 3.6.4` if you want local development and \ntesting dependencies.\n\nPipenv will install Python 3.6.4 via Pyenv, if it is not yet installed,\ncreate a new virtual environment for the project using the specified Python \nversion, and install all the dependencies into it.\n\n\n#### Installing Redis\n\nRedis can be installed via some package managers on Mac and Linux or\ncompiled from source. They have instructions for that:\nhttp://redis.io/topics/quickstart\n\nI recommend using a package manager or better yet, especially if you use\nWindows (because Redis is not very well supported on Windows), use a Docker\ncontainer (you can see how to install Docker above):\n`docker run --name acmemegastore-redis -d redis`\n\nThis will launch a Docker container named `acmemegastore-redis`. It will\nbe necessary to change the `REDIS_HOST` setting in `.env` and set it to the\nDocker container's IP address, which you can get by running the following\ncommand:\n`docker inspect acmemegastore-redis | grep \"IPAddress\"`\n\nAfter you're done, you can remove the container and the Redis image\n(assuming you don't use it for anything else) with the following commands:\n`docker rm acmemegastore-redis` and `docker rmi redis`\n\n\n#### Running the application\n\nWith all the Python packages installed and Redis running, you're ready to\nfinally launch the application. In the project root, execute `pipenv run python\n -m acme.server`.\n\nFor this method, the API is available at\n`http://0.0.0.0:8080/api/recent_purchases/` instead of\n`http://localhost:8080/api/recent_purchases/`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fborfast%2Facmemegastore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fborfast%2Facmemegastore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fborfast%2Facmemegastore/lists"}