{"id":23357937,"url":"https://github.com/dimanu-py/course-platform","last_synced_at":"2026-04-26T23:31:19.534Z","repository":{"id":266119565,"uuid":"897399244","full_name":"dimanu-py/course-platform","owner":"dimanu-py","description":"Simple course platform applying DDD","archived":false,"fork":false,"pushed_at":"2025-02-21T07:44:10.000Z","size":199,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-07T20:18:14.239Z","etag":null,"topics":["ddd","fastapi","outside-in-tdd","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dimanu-py.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2024-12-02T15:06:43.000Z","updated_at":"2025-02-21T07:44:14.000Z","dependencies_parsed_at":"2025-02-21T08:35:31.643Z","dependency_job_id":null,"html_url":"https://github.com/dimanu-py/course-platform","commit_stats":null,"previous_names":["dimanu-py/course-platform","dimanu-py/influencer-platform"],"tags_count":0,"template":false,"template_full_name":"dimanu-py/instant-python","purl":"pkg:github/dimanu-py/course-platform","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dimanu-py%2Fcourse-platform","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dimanu-py%2Fcourse-platform/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dimanu-py%2Fcourse-platform/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dimanu-py%2Fcourse-platform/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dimanu-py","download_url":"https://codeload.github.com/dimanu-py/course-platform/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dimanu-py%2Fcourse-platform/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32317162,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-26T23:26:28.701Z","status":"ssl_error","status_checked_at":"2026-04-26T23:26:25.802Z","response_time":129,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["ddd","fastapi","outside-in-tdd","python"],"created_at":"2024-12-21T10:35:31.414Z","updated_at":"2026-04-26T23:31:19.519Z","avatar_url":"https://github.com/dimanu-py.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003ch1\u003e⚡️ DDD Course Platform Pet Project ⚡️\u003c/h1\u003e\n\u003c/div\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#requirements\"\u003eRequirements\u003c/a\u003e\u0026nbsp;\u0026nbsp;•\u0026nbsp;\n  \u003ca href=\"#use\"\u003eSet up the Project\u003c/a\u003e\u0026nbsp;\u0026nbsp;•\u0026nbsp;\n  \u003ca href=\"#postgres\"\u003ePostgresSQL\u003c/a\u003e\u0026nbsp;\u0026nbsp;•\u0026nbsp;\n  \u003ca href=\"#rabbitmq\"\u003eRabbitMQ\u003c/a\u003e\n\u003c/p\u003e\n\n\u003ca name=requirements\u003e\u003c/a\u003e\n## Requirements\n\nThe project runs with [Python 3.12](https://www.python.org/downloads/release/python-3120/). \n\nThe recommended way to install Python is using [pyenv](https://github.com/pyenv/pyenv) if you are on Linux or MacOS. Here is a summary of the steps,\nbut it's recommended to visit the documentation for more details.\n\n\u003cdetails\u003e\u003csummary\u003eInstall Python with pyenv\u003c/summary\u003e\n\n1. Install pyenv:\n    ```bash\n    curl https://pyenv.run | bash\n    ```\n\n2. Set you bash profile to load pyenv. In my case I use fish:\n\n    ```bash\n    set -Ux PYENV_ROOT $HOME/.pyenv\n    fish_add_path $PYENV_ROOT/bin\n   ```\n   \n    Then, add the following line to `~/.config/fish/config.fish`:\n\n    ```bash\n    echo pyenv init - | source \u003e\u003e ~/.config/fish/config.fish\n    ```\n3. Install the selected Python version (you can see available version with `pyenv install --list`):\n    ```bash\n    pyenv install 3.12\n    ```\n4. Go to your project folder and select this Python version for the folder\n    ```bash\n    pyenv local 3.12\n    ```\n\u003c/details\u003e\n\nAfter installing _pyenv_ you only need to install the package manager, in this case I prefer\nto use [pdm](https://github.com/pdm-project/pdm). Just need to run the following command on\nyour project folder:\n    \n```bash\npip install pdm\n```\n\nTo install directly all dependencies, run:\n\n```bash\nmake install\n```\n\n\u003ca name=use\u003e\u003c/a\u003e\n## Set up the Project\n\nIn order to set up the project, you need to follow the steps below:\n\n1. Clone the repository on you local machine\n    ```bash\n   git clone \u003crepo_url\u003e\n   ```\n2. Run the `make local-setup` command to be able to run the hooks inside [hooks](./scripts/hooks) folder.\n\n\u003e [!NOTE]\n\u003e If you want to ignore the hooks folder, you can remove it and just run `make install` command.\n\n3. Run infra containers declared in the [docker-compose.yml](./docker-compose.yml) file:\n    ```bash\n    docker-compose up -d\n    ```\n4. Run the tests to check if everything is working:\n    ```bash\n    make test\n    ```\n\n\u003ca name=postgres\u003e\u003c/a\u003e\n## PostgresSQL\n\n@TODO \n\n\u003ca name=event-sourcing\u003e\u003c/a\u003e\n## RabbitMQ\n\n\u003cdetails\u003e\u003csummary\u003ePython tutorial\u003c/summary\u003e\n\n- [`Producers`](#producers) publish messages to [`exchanges`](#exchanges).\n- The [`exchanges`](#exchanges) takes those messages and route them to [`queues`](#queues).\n- [`Exchanges`](#exchanges) distribute the messages to the [`queues`](#queues) based on the [`bindings`](#bindings).\n- The [`consumers`](#consumers) are subscribed to [`queues`](#queues) and consume the messages from them.\n\n\u003ca name=exchanges\u003e\u003c/a\u003e\n### Exchanges\n\n\u003e They are responsible for getting [`producers`](#producers) messages and routing them to the [`queues`](#queues).\n\nExchanges can be configured with different attributes:\n\n- `exchange`: The name of the exchange. If not set, a random exchange name will be generated.\n- `durable`: If set to `True` the exchange will survive server restarts, otherwise it will be deleted.\n- `auto_doelete`: If set to `True` the exchange will be deleted when no queues are bound to it.\n- `exchange_type`: The type of the exchange. The default is `direct`, but there are other types like `fanout`, `topic`.\n    - `'direct'`: The message is routed to the queues whose binding key exactly matches the routing key of the message.\n    - `'fanout'`: The message is routed to all the queues bound to the exchange. Here routing key is ignored.\n    - `'topic'`: The message is routed to the queues whose binding key matches the routing key of the message.\n\nTo create a new exchange we need to run the following command:\n\n```python\nimport pika\n\n\nconnection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))\nchannel = connection.channel()\n\nchannel.exchange_declare(\n    exchange=\"videos\",\n    exchange_type=\"topic\"\n)\n```\n\n\u003ca name=queues\u003e\u003c/a\u003e\n### Queues\n\n\u003e They store messages until they are consumed by the [`consumers`](#consumers).\n\nQueues can be configured with different attributes:\n- `queue`: The name of the queue. If not set, a random queue name will be generated.\n- `durable`: If set to `True` the queue will survive server restarts, otherwise it will be deleted.\n- `exclusive`: If set to `True` the queue will be used by only one connection and will be deleted when the connection closes.\n- `auto_delete`: If set to `True` the queue will be deleted when no consumers are connected to it.\n\nTo create a new queue we need to run the following command:\n\n```python\nchannel.queue_declare(\n    queue=\"users.send_email_on_video_created\",\n    durable=True,\n    exclusive=True\n)\n```\n\n\u003e Creating queues are idempotent operations, so we can run the same command multiple times without any side effects.\n\u003e If we don't know who will create the queue first, we can create it in the [`producer`](#producers) and [`consumer`](#consumers) code.\n\n\u003ca name=bindings\u003e\u003c/a\u003e\n### Bindings\n\n\u003e They are the link between the [`exchanges`](#exchanges) and the [`queues`](#queues).\n\nTo let the [`exchange`](#exchanges) know where to send the messages we need to create a [`binding`](#bindings) between the [`exchange`](#exchanges) \nand the [`queues`](#queues).\n\n```python\nchannel.queue_bind(\n    exchange=\"videos\",\n    queue=\"users.send_email_on_video_created\",\n    routing_key=\"videos.created\"\n)\n```\n\nThe [`queue`](#queues) will receive the messages when its `routing_key` matches the `binding_key` of the [`exchange`](#exchanges).\n\n\u003ca name=producers\u003e\u003c/a\u003e\n### Producers\n\n\u003e They are the services that publish messages to the [`exchanges`](#exchanges).\n\nProducers are intended to be long-lived and open their connections on startup.\n\nTo publish an event we need to create an [`exchange`](#exchanges), we can't send a message directly to a [`queue`](#queues).\n1. Declare the [`exchange`](#exchanges) they want to publish the message to (same steps as in the [exchanges](#exchanges) section):\n    \n    ```python\n    channel.exchange_declare(\n        exchange=\"videos\",\n        exchange_type=\"topic\"\n    )\n    ```\n\n2. Publish the message specifying the `exchange`(name) and the `routing_key` arguments if it's declared of type `topic` or `direct`. This\nrouting key should have the same name as the [`binding_key`](#bindings) of the [`queue`](#queues) that will receive the message.\n\n    ```python\n    channel.basic_publish(\n        exchange=\"videos\",\n        routing_key=\"videos.created\",\n        body=\"Video Created!\"\n    )\n    ```\n\n    If we want to ensure that the event survives a server restart, we need to set the `delivery_mode` to `Persistent`:\n    \n    ```python\n    import pika\n    \n    channel.basic_publish(\n        exchange=\"videos\",\n        routing_key=\"videos.created\",\n        body=\"Video Created!\",\n        properties=pika.BasicProperties(\n            delivery_mode=pika.DeliveryMode.Persistent\n        )\n    )\n    ```\n\n\u003ca name=consumers\u003e\u003c/a\u003e\n### Consumers\n\n\u003e They are the services that consume the messages from the [`queues`](#queues).\n\nConsumers are intended to be long-lived and open their connections on startup. We will say that a consumer is subscribed to a queue\nwhen it starts consuming messages from it.\n\nAll consumers need to:\n1. Define the [`queue`](#queues) they want to consume messages from. Additionally, they can define the [`exchange`](#exchanges) \nthe queue will be subscribed to. As creating a [`queue`](#queues), this is an idempotent operation, so we will create just one exchange.\n\n    ```python\n    channel.exchange_declare(\n        exchange=\"videos\",\n        exchange_type=\"topic\"\n    )\n    channel.queue_declare(\n        queue=\"users.send_email_on_video_created\",\n        durable=True,\n        exclusive=True\n    )\n    ```\n\n2. [Bind](#bindings) that [`queue`](#queues) to the [`exchange`](#exchanges) with the `routing_key`.\n\n    ```python\n    channel.queue_bind(\n        exchange=\"videos\",\n        queue=\"users.send_email_on_video_created\",\n        routing_key=\"videos.created\"\n    )\n    ```\n   \n3. Define a callback function that will be called when a message is received. This function will be responsible for\nprocessing the message.\n\n    ```python\n    from pika.channel import Channel\n    from pika.spec import BasicProperties, Basic\n    \n    def callback(channel: Channel, method: Basic.Deliver, properties: BasicProperties, body: bytes):\n        print(f\"[x] Received {method.routing_key}: {body.decode()}\")\n    ```\n   \n    To ensure that the message is not lost if the consumer crashes it's recommended to add a manual message acknowledgment in the callback:\n        \n    ```python\n    from pika.channel import Channel\n    from pika.spec import BasicProperties, Basic\n   \n    def callback(channel: Channel, method: Basic.Deliver, properties: BasicProperties, body: bytes):\n        print(f\"[x] Received {method.routing_key}: {body.decode()}\")\n        channel.basic_ack(delivery_tag=method.delivery_tag)\n    ```\n\n4. Start consuming messages by subscribing to the queue.\n\n    ```python\n    channel.basic_consume(\n        queue=\"users.send_email_on_video_created\",\n        on_message_callback=callback,\n        auto_ack=False  # Set to True if you want to automatically acknowledge the message\n    )\n    channel.start_consuming()\n    ```\n   \n    When consuming, we can configure the [`queue`](#queues) to not send a new message to the consumer until it has processed and\n    acknowledged the previous one. This is called _fair dispatch_ and can be set as follows:\n    \n    ```python\n    channel.basic_qos(prefetch_count=1)\n    ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eHow is applied in the project\u003c/summary\u003e\n\n\u003c/details\u003e","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdimanu-py%2Fcourse-platform","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdimanu-py%2Fcourse-platform","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdimanu-py%2Fcourse-platform/lists"}