diff --git a/.circleci/config.yml b/.circleci/config.yml index 1265f324b..987026da7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -53,71 +53,78 @@ workflows: # Inside the workflow, you define the jobs you want to run. # For more details on extending your workflow, see the configuration docs: https://circleci.com/docs/2.0/configuration-reference/#workflows jobs: - - rust/lint-test-build: - release: true + # - rust/lint-test-build: + # release: true - build-and-test jobs: - lint-test-build: - description: | - Check the rust sub section - executor: - name: default - tag: << parameters.version >> - parameters: - cache_version: - default: v1 - description: Cache version to use - increment this to build a fresh cache. - type: string - clippy_arguments: - default: "" - description: Arguments to pass to cargo run clippy. - type: string - release: - default: false - description: Whether to build the binary for release or debug. - type: boolean - version: - default: 1.49.0 - description: Version of Rust executor to utilize. - type: string - with_cache: - default: true - description: Whether to restore and save the cache or not - set to no if running multiple commands in one job. - type: boolean - working_directory: - default: ~/robyn - description: Path to the directory containing your Cargo.lock file. Not needed if Cargo.lock lives in the root. - type: string - steps: - - checkout: - path: /home/circleci/robyn - - when: - condition: <> - steps: - - restore_cache: - keys: - - cargo-<>-{{ checksum "Cargo.lock" }} - - clippy: - flags: <> - with_cache: false - working_directory: <> - - test: - with_cache: false - working_directory: <> - # command: cargo test - # - build: - # release: <> - # with_cache: false - # working_directory: <> - - when: - condition: <> - steps: - - save_cache: - key: cargo-<>-{{ checksum "Cargo.lock" }} - paths: - - ~/.cargo - working_directory: <> + # lint-test-build: + # description: | + # Check the rust sub section + # executor: + # name: default + # tag: << parameters.version >> + # parameters: + # cache_version: + # default: v1 + # description: Cache version to use - increment this to build a fresh cache. + # type: string + # clippy_arguments: + # default: "" + # description: Arguments to pass to cargo run clippy. + # type: string + # release: + # default: false + # description: Whether to build the binary for release or debug. + # type: boolean + # version: + # default: 1.57.0 + # description: Version of Rust executor to utilize. + # type: string + # with_cache: + # default: true + # description: Whether to restore and save the cache or not - set to no if running multiple commands in one job. + # type: boolean + # working_directory: + # default: ~/robyn + # description: Path to the directory containing your Cargo.lock file. Not needed if Cargo.lock lives in the root. + # type: string + # steps: + # - checkout: + # path: /home/circleci/robyn + # - run: + # name: Update clippy + # command: | + # rustup update + # rustup component add clippy-preview + + # - when: + # condition: <> + # steps: + # - restore_cache: + # keys: + # - cargo-<>-{{ checksum "Cargo.lock" }} + + # - clippy: + # flags: <> + # with_cache: false + # working_directory: <> + # - test: + # with_cache: false + # working_directory: <> + # # command: cargo test + # # - build: + # # release: <> + # # with_cache: false + # # working_directory: <> + # - when: + # condition: <> + # steps: + # - save_cache: + # key: cargo-<>-{{ checksum "Cargo.lock" }} + # paths: + # - ~/.cargo + # working_directory: <> build-and-test: # This is the name of the job, feel free to change it to better match what you're trying to do! # These next lines defines a Docker executors: https://circleci.com/docs/2.0/executor-types/ diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..d352beb7a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +**Description** + +This PR fixes # + + \ No newline at end of file diff --git a/.github/workflows/ISSUE_TEMPLATE/bug_report.md b/.github/workflows/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..d13d2aec0 --- /dev/null +++ b/.github/workflows/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: 🐛 Bug Report +about: Report an issue to help us improve +title: '[BUG]' +labels: 'kind/bug' +assignees: '' +--- +**Description** + + +**Expected Behavior** + + +**Screenshots** + + +**Environment:** +- Host OS: +- Browser: + +--- + + +[Optional] **Additional Context** + + +--- \ No newline at end of file diff --git a/.github/workflows/ISSUE_TEMPLATE/feature_request.md b/.github/workflows/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..06d77568d --- /dev/null +++ b/.github/workflows/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: 💡 Feature request +about: Suggest an enhancement to Robyn +title: '[Feature Request]' +labels: 'kind/enhancement' +assignees: '' +--- +**Current Behavior** + + +**Desired Behavior** + + +--- +**Screenshots / Mockups** + + +**Alternatives** + + +--- \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 35301c039..1e38ebfda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,54 @@ # Changelog +## [Unreleased](https://github.com/sansyrox/robyn/tree/HEAD) + +[Full Changelog](https://github.com/sansyrox/robyn/compare/v0.9.0...HEAD) + +**Closed issues:** + +- Add PyPI classifiers [\#127](https://github.com/sansyrox/robyn/issues/127) +- Robyn version 0.9.0 doesn't work on Mac M1 Models [\#120](https://github.com/sansyrox/robyn/issues/120) +- Inconsistency in steps mentioned in Readme to run locally [\#119](https://github.com/sansyrox/robyn/issues/119) +- Async web socket support [\#116](https://github.com/sansyrox/robyn/issues/116) +- Reveal Logo to be removed from Future Roadmap [\#107](https://github.com/sansyrox/robyn/issues/107) +- Dead Link for Test Drive Button on Robyn Landing Page [\#106](https://github.com/sansyrox/robyn/issues/106) +- Add issue template, pr template and community guidelines [\#105](https://github.com/sansyrox/robyn/issues/105) +- For v0.7.0 [\#72](https://github.com/sansyrox/robyn/issues/72) +- Add better support for requests and response! [\#13](https://github.com/sansyrox/robyn/issues/13) + +**Merged pull requests:** + +- Add async support in WS [\#134](https://github.com/sansyrox/robyn/pull/134) ([sansyrox](https://github.com/sansyrox)) +- Add help messages and simplify 'dev' option [\#128](https://github.com/sansyrox/robyn/pull/128) ([Kludex](https://github.com/Kludex)) +- Apply Python highlight on api.md [\#126](https://github.com/sansyrox/robyn/pull/126) ([Kludex](https://github.com/Kludex)) +- Update comparison.md [\#124](https://github.com/sansyrox/robyn/pull/124) ([Kludex](https://github.com/Kludex)) +- Update comparison.md [\#123](https://github.com/sansyrox/robyn/pull/123) ([Kludex](https://github.com/Kludex)) +- Fix readme documentation [\#122](https://github.com/sansyrox/robyn/pull/122) ([sansyrox](https://github.com/sansyrox)) +- Release v0.9.0 Changelog [\#121](https://github.com/sansyrox/robyn/pull/121) ([sansyrox](https://github.com/sansyrox)) +- \[FEAT\] Open Source Contribution Templates [\#118](https://github.com/sansyrox/robyn/pull/118) ([shivaylamba](https://github.com/shivaylamba)) +- FIX : Wrong link for Test Drive [\#117](https://github.com/sansyrox/robyn/pull/117) ([shivaylamba](https://github.com/shivaylamba)) + +## [v0.9.0](https://github.com/sansyrox/robyn/tree/v0.9.0) (2021-12-01) + +[Full Changelog](https://github.com/sansyrox/robyn/compare/v0.8.1...v0.9.0) + +**Closed issues:** + +- Add more HTTP methods [\#74](https://github.com/sansyrox/robyn/issues/74) + +**Merged pull requests:** + +- Fix default url bug [\#111](https://github.com/sansyrox/robyn/pull/111) ([sansyrox](https://github.com/sansyrox)) +- Web socket integration attempt 2 [\#109](https://github.com/sansyrox/robyn/pull/109) ([sansyrox](https://github.com/sansyrox)) + +## [v0.8.1](https://github.com/sansyrox/robyn/tree/v0.8.1) (2021-11-17) + +[Full Changelog](https://github.com/sansyrox/robyn/compare/v0.8.0...v0.8.1) + +**Fixed bugs:** + +- The default start is running the server at '0.0.0.0' instead of '127.0.0.1' [\#110](https://github.com/sansyrox/robyn/issues/110) + ## [v0.8.0](https://github.com/sansyrox/robyn/tree/v0.8.0) (2021-11-10) [Full Changelog](https://github.com/sansyrox/robyn/compare/v0.7.1...v0.8.0) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..a88adde7e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,6 @@ +### Robyn Open Source Community Guidelines + +- **Be friendly and patient**. +- **Be welcoming**. +- **Be respectful**. +- **Be careful in the words that we choose**. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..9a8994019 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,122 @@ +## Contributing Guidelines + +First off, thank you for considering contributing to ```Robyn```. This guide details all the general information that one should know before contributing to the project. +Please stick as close as possible to the guidelines. That way we ensure that you have a smooth experience contributing to this project. + +### General Rules : +These are in general rules that you should be following while contributing to an Open Source project : + +- Be Nice, Be Respectful (BNBR) +- Check if the Issue you created, exists or not. +- While creating a new issue make sure you describe the issue clearly. +- Make proper commit messages and document your PR well. +- Always add Comments in your Code and explain it at points, if possible add Doctest. +- Always create a Pull Request from a Branch; Never from the Main. +- Follow proper code conventions because writing clean code is important. +- Issues would be assigned on a "First Come, First Serve" basis. +- Do mention (@maintainer) the project maintainer if your PR isn't reviewed within few days. + +## First time contributors : + +Pushing files in your own repository is easy but how to contribute to someone else's project? If you have the same question then below are the steps that you can follow +to make your first contribution in this repoitory. + + +### Pull Request + +**1.** The very first step includes forking the project. Click on the ```fork``` button as shown below to fork the project. +


+ +**2.** Clone the forked repository. Open up the GitBash/Command Line and type + +``` +git clone https://github.com//robyn.git +``` + +**3.** Navigate to the project directory. + +``` +cd robyn +``` +**4.** Add a reference to the original repository. + +``` +git remote add upstream https://github.com/sansyrox/robyn.git +``` + +**5.** See latest changes to the repo using + +``` +git remote -v +``` + +**6.** Create a new branch. + +``` +git checkout -b +``` + +**7.** Always take a pull from the upstream repository to your main branch to keep it even with the main project. This will save your from frequent merge conflicts. + +``` +git pull upstream main +``` + +**8.** You can make the required changes now. Make appropriate commits with proper commit messages. + +**9.** Add and then commit your changes. + +``` +git add . +``` +``` +git commit -m "" +``` + + +**10.** Push your local branch to the remote repository. + +``` +git push -u origin +``` + +**11.** Once you have pushed the changes to your repository, go to your forked repository. Click on the ```Compare & pull request``` button as shown below. +


+ + +**12.** The image below is how the new page would look like. Give a proper title to your PR and describe the changes made by you in the description box.(Note - Sometimes there are PR templates which is to be filled as instructed.) +


+ + +**13.** Open a pull request by clicking the ```Create pull request``` button. + +```Voila, you have made your first contribution to this project``` + +## Issue + +- Issues can be used to keep track of bugs, enhancements, or other requests. Creating an issue to let the project maintainers know about the changes your are + planning to make before raising a PR is a good open source practice. +
+ +Lets walkthrough the steps to create an issue: + +**1.** On GitHub navigate to the main page of the repository. [Here](https://github.com/sansyrox/robyn.git) in this case. + + +**2.** Under your repository name, click on the ```Issues``` button. +


+ + +**3.** Click on the ```New issue``` button. +


+ +**4.** Select one of the Issue Templates to get started. +


+ +**4.** Fill in the appropriate ```Title``` and ```Issue description``` and click on ```Submit new issue```. +


+ +### Tutorials that may help you : + +- [Git & GitHub Tutorial](https://www.youtube.com/watch?v=RGOj5yH7evk) +- [Resolve merge conflict](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/resolving-a-merge-conflict-on-github) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 1f7e23872..c14559cda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1236,7 +1236,7 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "robyn" -version = "0.8.1" +version = "0.10.0" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 663bb4c41..3a5decb31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "robyn" -version = "0.9.0" +version = "0.10.0" authors = ["Sanskar Jethi "] edition = "2018" description = "A web server that is fast!" -license = "MIT" +license = "BSD License (BSD)" homepage = "https://github.com/sansyrox/robyn" readme = "README.md" diff --git a/README.md b/README.md index 3c8070454..b089f8dee 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ [![Gitter](https://badges.gitter.im/robyn_/community.svg)](https://gitter.im/robyn_/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Downloads](https://static.pepy.tech/personalized-badge/robyn?period=total&units=international_system&left_color=grey&right_color=blue&left_text=Downloads)](https://pepy.tech/project/robyn) + +[Docs](https://sansyrox.github.io/robyn/#/) + Robyn is an async Python backend server with a runtime written in Rust, btw. Python server running on top of of Rust Async RunTime. @@ -34,6 +37,20 @@ app.start(port=5000) ``` +## Features +- Under active development! +- Written in Rust, btw xD +- A multithreaded Runtime +- Extensible +- A simple API +- Sync and Async Function Support +- Dynamic URL Routing +- Multi Core Scaling +- WebSockets! +- Hot Reloading (Still experimental) +- Community First and truly FOSS! + + ## Contributor Guidelines Feel free to open an issue for any clarification or for any suggestions. @@ -42,11 +59,11 @@ If you're feeling curious. You can take a look at a more detailed architecture [ ## To Run Locally -1. Add more routes in the test.py file(if you like). It only supports only get requests at the moment +1. Add more routes in the `integration_tests/base_routes.py` file(if you like). It only supports only get requests at the moment 2. Run `maturin develop` -3. Run `python3 test.py` +3. Run `python3 integration_tests/base_routes.py` ## To Run @@ -67,6 +84,8 @@ optional arguments: ## Contributors/Supporters +To contribute to Robyn, make sure to first go through the [CONTRIBUTING.md](./CONTRIBUTING.md). + Thanks to all the contributors of the project. Robyn will not be what it is without all your support :heart:. Special thanks to the [ PyO3 ](https://pyo3.rs/v0.13.2/) community and [ Andrew from PyO3-asyncio ](awestlake87/pyo3-asyncio) for their amazing libraries and their support for my queries. 💖 diff --git a/docs/README.md b/docs/README.md index 41a2d41e9..451992272 100644 --- a/docs/README.md +++ b/docs/README.md @@ -36,18 +36,19 @@ To read about the detailed architecture, you can read [here](https://sansyrox.gi ## Testing on Python -1. `cargo build --release` -2. `cp target/release/librobyn.dylib ./robyn.so` -3. `python3` -4. `import robyn` -5. `dir(robyn)` +1. Activate a virtual environment +2. Install maturin: `pip3 install maturin` +3. Create a Debug build: `maturin develop` +4. Test it out: `python3` +5. `import robyn` +6. `dir(robyn)` ## To Run ### Without hot reloading `python3 app.py` -### With hot reloading +### With hot reloading(still beta) `python3 app.py --dev=true` diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 723ccc42b..5721a66fd 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,6 +1,7 @@ - [Home](/) +- [Features](features.md) - [API](api.md) - [Architecture](architecture.md) - [Comparison](comparison.md) diff --git a/docs/api.md b/docs/api.md index f7a89ac51..c4ec8c475 100644 --- a/docs/api.md +++ b/docs/api.md @@ -51,43 +51,45 @@ The request object contains the `body` in PUT/POST/PATCH. The `header`s are avai Robyn supports every HTTP request method. The examples of some of them are below: ### GET Request - ```python3 - @app.get("/") - async def h(request): - return "Hello World" - ``` +```python3 +@app.get("/") +async def h(request): + return "Hello World" +``` ### POST Request - ```python3 - @app.post("/post") - async def postreq(request): - return bytearray(request["body"]).decode("utf-8") - ``` +```python3 +@app.post("/post") +async def postreq(request): + return bytearray(request["body"]).decode("utf-8") +``` ### PUT Request - ```python3 - @app.put("/put") - async def postreq(request): - return bytearray(request["body"]).decode("utf-8") - ``` +```python3 +@app.put("/put") +async def postreq(request): + return bytearray(request["body"]).decode("utf-8") +``` ### PATCH Request - ```python3 - @app.patch("/patch") - async def postreq(request): - return bytearray(request["body"]).decode("utf-8") - ``` + +```python3 +@app.patch("/patch") +async def postreq(request): + return bytearray(request["body"]).decode("utf-8") +``` ### DELETE Request - ```python3 - @app.delete("/delete") - async def postreq(request): - return bytearray(request["body"]).decode("utf-8") - ``` + +```python3 +@app.delete("/delete") +async def postreq(request): + return bytearray(request["body"]).decode("utf-8") +``` ### Having Dynamic Routes @@ -162,7 +164,7 @@ app = Robyn(__file__) websocket = WS(app, "/web_socket") ``` -Now, you can define 3 methods for every web_socket for their lifecycle, they are as follows: +Now, you can define 3 methods for every web_socket for their life cycle, they are as follows: ```python3 @websocket.on("message") @@ -189,12 +191,51 @@ def message(): ``` - -## MutliCore Scaling - The three methods: - "message" is called when the socket receives a message - "close" is called when the socket is disconnected - "connect" is called when the socket connects To see a complete service in action, you can go to the folder [../integration_tests/base_routes.py](../integration_tests/base_routes.py) + +### Update(20/12/21) + +Async functions are supported in Web Sockets now! + +Async functions are executed out of order for web sockets. i.e. the order of response is not guaranteed. This is done to achieve a non blocking concurrent effect. + +A blocking async web socket is in plans for the future. + +### Usage + +```python3 +@websocket.on("message") +async def connect(): + global i + i+=1 + if i==0: + return "Whaaat??" + elif i==1: + return "Whooo??" + elif i==2: + return "*chika* *chika* Slim Shady." + elif i==3: + i= -1 + return "" + +@websocket.on("close") +async def close(): + return "Goodbye world, from ws" + +@websocket.on("connect") +async def message(): + return "Hello world, from ws" + +``` + + +## MutliCore Scaling + +To run Robyn across multiple cores, you can use the following command: + +`python3 app.py --workers=N --processes=N` diff --git a/docs/comparison.md b/docs/comparison.md index f9ce31bed..c513d333f 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -2,7 +2,7 @@ ## Read this before you scroll down -The comparison is not meant to defame any of the of the developers or the frameworks listed below. The names of the frameworks have been listed for a clear comparison. All of these frameworks are the reason for me having a high inclination towards the python web ecosystem and I hope to have not caused any offence( to anyone) by listing these frameworks. +The comparison is not meant to defame any of the of the developers or the frameworks listed below. The names of the frameworks have been listed for a clear comparison. All of these frameworks are the reason for me having a high inclination towards the python web ecosystem and I hope to have not caused any offence (to anyone) by listing these frameworks. **Also, these tests were done on my development machine and the numbers portrayed below are not absolute by any means. These numbers only indicate the relative performance of these frameworks.** @@ -25,7 +25,7 @@ I used [oha](https://github.com/hatoo/oha) to perform the testing of 10000 reque Average: 0.0206 secs Requests/sec: 2420.4851 ``` -3. Django(Gunicron) +3. Django(Gunicorn) ``` Total: 13.5070 secs Slowest: 0.3635 secs @@ -42,6 +42,15 @@ I used [oha](https://github.com/hatoo/oha) to perform the testing of 10000 reque Requests/sec: 5457.2339 ``` +4. Robyn (5 workers) +``` + Total: 1.5592 secs + Slowest: 0.0211 secs + Fastest: 0.0017 secs + Average: 0.0078 secs + Requests/sec: 6413.6480 +``` + Robyn is able to serve the 10k requests in 1.8 seconds followed by Flask and FastAPI, which take around 5 seconds(using 5 workers on a dual core machine). Finally, Django takes around 13.5070 seconds. ## Verbose Logs @@ -222,3 +231,46 @@ Status code distribution: ``` +Robyn(with 5 workers) +``` +Summary: + Success rate: 1.0000 + Total: 1.5592 secs + Slowest: 0.0211 secs + Fastest: 0.0017 secs + Average: 0.0078 secs + Requests/sec: 6413.6480 + + Total data: 126.95 KiB + Size/request: 13 B + Size/sec: 81.42 KiB + +Response time histogram: + 0.002 [30] | + 0.004 [599] |■■■■■ + 0.005 [3336] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.007 [3309] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.009 [1614] |■■■■■■■■■■■■■■■ + 0.011 [749] |■■■■■■■ + 0.012 [253] |■■ + 0.014 [94] | + 0.016 [14] | + 0.018 [1] | + 0.019 [1] | + +Latency distribution: + 10% in 0.0055 secs + 25% in 0.0063 secs + 50% in 0.0074 secs + 75% in 0.0089 secs + 90% in 0.0107 secs + 95% in 0.0117 secs + 99% in 0.0142 secs + +Details (average, fastest, slowest): + DNS+dialup: 0.0022 secs, 0.0013 secs, 0.0028 secs + DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0001 secs + +Status code distribution: + [200] 10000 responses +``` diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 000000000..163ee608d --- /dev/null +++ b/docs/features.md @@ -0,0 +1,13 @@ +## Features +- Under active development! +- Written in Rust, btw xD +- A multithreaded Runtime +- Extensible +- A simple API +- Sync and Async Function Support +- Dynamic URL Routing +- Multi Core Scaling +- WebSocket! +- Hot Reloading (Still experimental) +- Community First and truly FOSS! + diff --git a/docs/landing_page/index.html b/docs/landing_page/index.html index 7763be2e6..b8799e869 100644 --- a/docs/landing_page/index.html +++ b/docs/landing_page/index.html @@ -232,7 +232,7 @@

A fast web framework!

  • Test Drive
  • diff --git a/docs/roadmap.md b/docs/roadmap.md index da5533524..409a75362 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,7 @@ ## Future Roadmap -- Integrate WebSockets - Add session/cookie plugins -- Add the plugin documentation +- Create a WEB3 plugin +- Add graphql integration +- And more... diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 5cf09d685..dbb6836cd 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -8,7 +8,7 @@ i = -1 @websocket.on("message") -def connect(): +async def connect(): global i i+=1 if i==0: @@ -21,12 +21,10 @@ def connect(): @websocket.on("close") def close(): - print("Hello world") - return "Hello world, from ws" + return "GoodBye world, from ws" @websocket.on("connect") def message(): - print("Hello world") return "Hello world, from ws" diff --git a/integration_tests/conftest.py b/integration_tests/conftest.py index ce256c7ad..3e56f0279 100644 --- a/integration_tests/conftest.py +++ b/integration_tests/conftest.py @@ -7,12 +7,12 @@ @pytest.fixture def session(): - subprocess.call(["freeport", "5000"]) + subprocess.call(["yes | freeport 5000"], shell=True) os.environ["ROBYN_URL"] = "127.0.0.1" current_file_path = pathlib.Path(__file__).parent.resolve() base_routes = os.path.join(current_file_path, "./base_routes.py") process = subprocess.Popen(["python3", base_routes]) - time.sleep(1) + time.sleep(5) yield process.terminate() del os.environ["ROBYN_URL"] diff --git a/integration_tests/index.py b/integration_tests/index.py new file mode 100644 index 000000000..39ff98554 --- /dev/null +++ b/integration_tests/index.py @@ -0,0 +1,9 @@ +from robyn import Robyn + +app = Robyn(__file__) + +@app.get("/") +async def h(): + return 'Hello, world!' + +app.start() diff --git a/integration_tests/test_web_sockets.py b/integration_tests/test_web_sockets.py index 69f71c72a..951f239b5 100644 --- a/integration_tests/test_web_sockets.py +++ b/integration_tests/test_web_sockets.py @@ -6,6 +6,7 @@ def test_web_socket(session): async def start_ws(uri): async with connect(uri) as websocket: + assert( await websocket.recv() == "Hello world, from ws") await websocket.send("My name is?") assert( await websocket.recv() == "Whaaat??") await websocket.send("My name is?") diff --git a/pyproject.toml b/pyproject.toml index c71cc1ea7..eb7cee3b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "robyn" -version = "0.9.0" +version = "0.10.0" description = "A web server that is fast!" authors = ["Sanskar Jethi "] @@ -24,5 +24,19 @@ dependencies = [ 'uvloop == 0.16.0; platform_machine == "x86_64"', 'uvloop == 0.16.0; platform_machine == "i686"' ] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Topic :: Internet :: WWW/HTTP", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: Implementation :: CPython", +] diff --git a/robyn/argument_parser.py b/robyn/argument_parser.py index 982ee18aa..fbfc89d64 100644 --- a/robyn/argument_parser.py +++ b/robyn/argument_parser.py @@ -1,13 +1,35 @@ import argparse + class ArgumentParser(argparse.ArgumentParser): def __init__(self): - self.parser = argparse.ArgumentParser(description="Robyn, a fast async web framework with a rust runtime.") - self.parser.add_argument('--processes', type=int, default=1, required=False) - self.parser.add_argument('--workers', type=int, default=1, required=False) - self.parser.add_argument('--dev', default=False, type=lambda x: (str(x).lower() == 'true')) + self.parser = argparse.ArgumentParser( + description="Robyn, a fast async web framework with a rust runtime." + ) + self.parser.add_argument( + "--processes", + type=int, + default=1, + required=False, + help="Choose the number of processes. [Default: 1]", + ) + self.parser.add_argument( + "--workers", + type=int, + default=1, + required=False, + help="Choose the number of workers. [Default: 1]", + ) + self.parser.add_argument( + "--dev", + dest="dev", + action="store_true", + default=False, + help="Development mode. It restarts the server based on file changes.", + ) + self.args = self.parser.parse_args() - + def num_processes(self): return self.args.processes @@ -16,7 +38,6 @@ def workers(self): def is_dev(self): _is_dev = self.args.dev - if _is_dev and ( self.num_processes() != 1 or self.workers() != 1 ): + if _is_dev and (self.num_processes() != 1 or self.workers() != 1): raise Exception("--processes and --workers shouldn't be used with --dev") return _is_dev - diff --git a/src/router.rs b/src/router.rs index 3b6c08d6c..c29e4e0ec 100644 --- a/src/router.rs +++ b/src/router.rs @@ -6,7 +6,6 @@ use pyo3::prelude::*; use pyo3::types::PyAny; use actix_web::http::Method; -use dashmap::DashMap; use matchit::Node; /// Contains the thread safe hashmaps of different routes @@ -21,7 +20,7 @@ pub struct Router { options_routes: Arc>>, connect_routes: Arc>>, trace_routes: Arc>>, - web_socket_routes: DashMap>, + web_socket_routes: Arc>>>, } impl Router { @@ -36,7 +35,7 @@ impl Router { options_routes: Arc::new(RwLock::new(Node::new())), connect_routes: Arc::new(RwLock::new(Node::new())), trace_routes: Arc::new(RwLock::new(Node::new())), - web_socket_routes: DashMap::new(), + web_socket_routes: Arc::new(RwLock::new(HashMap::new())), } } @@ -57,7 +56,9 @@ impl Router { } #[inline] - pub fn get_web_socket_map(&self) -> &DashMap> { + pub fn get_web_socket_map( + &self, + ) -> &Arc>>> { &self.web_socket_routes } @@ -117,26 +118,25 @@ impl Router { let (close_route_function, close_route_is_async, close_route_params) = close_route; let (message_route_function, message_route_is_async, message_route_params) = message_route; - let insert_in_router = |table: &DashMap>, - handler: Py, - is_async: bool, - number_of_params: u8, - socket_type: &str| { - let function = if is_async { - PyFunction::CoRoutine(handler) - } else { - PyFunction::SyncFunction(handler) + let insert_in_router = + |handler: Py, is_async: bool, number_of_params: u8, socket_type: &str| { + let function = if is_async { + PyFunction::CoRoutine(handler) + } else { + PyFunction::SyncFunction(handler) + }; + + println!("socket type is {:?} {:?}", table, route); + + table + .write() + .unwrap() + .entry(route.to_string()) + .or_default() + .insert(socket_type.to_string(), (function, number_of_params)) }; - let mut route_map = HashMap::new(); - route_map.insert(socket_type.to_string(), (function, number_of_params)); - - println!("{:?}", table); - table.insert(route.to_string(), route_map); - }; - insert_in_router( - table, connect_route_function, connect_route_is_async, connect_route_params, @@ -144,7 +144,6 @@ impl Router { ); insert_in_router( - table, close_route_function, close_route_is_async, close_route_params, @@ -152,7 +151,6 @@ impl Router { ); insert_in_router( - table, message_route_function, message_route_is_async, message_route_params, diff --git a/src/server.rs b/src/server.rs index 2149ff51d..a7c70c4bc 100644 --- a/src/server.rs +++ b/src/server.rs @@ -63,13 +63,10 @@ impl Server { return Ok(()); } - println!("{}", name); - let borrow = socket.try_borrow_mut()?; let held_socket: &SocketHeld = &*borrow; let raw_socket = held_socket.get_socket(); - println!("Got our socket {:?}", raw_socket); let router = self.router.clone(); let headers = self.headers.clone(); @@ -123,17 +120,23 @@ impl Server { .app_data(web::Data::new(headers.clone())); let web_socket_map = router_copy.get_web_socket_map(); - for elem in (web_socket_map).iter() { - let route = elem.key().clone(); - let params = elem.value().clone(); + for (elem, value) in (web_socket_map.read().unwrap()).iter() { + let route = elem.clone(); + let params = value.clone(); + let event_loop_hdl = event_loop_hdl.clone(); app = app.route( - &route, + &route.clone(), web::get().to( move |_router: web::Data>, _headers: web::Data>, stream: web::Payload, req: HttpRequest| { - start_web_socket(req, stream, Arc::new(params.clone())) + start_web_socket( + req, + stream, + Arc::new(params.clone()), + event_loop_hdl.clone(), + ) }, ), ); @@ -205,14 +208,13 @@ impl Server { /// Add a new web socket route to the routing tables /// can be called after the server has been started pub fn add_web_socket_route( - &self, + &mut self, route: &str, // handler, is_async, number of params connect_route: (Py, bool, u8), close_route: (Py, bool, u8), message_route: (Py, bool, u8), ) { - println!("WS Route added for {} ", route); self.router .add_websocket_route(route, connect_route, close_route, message_route); } diff --git a/src/web_socket_connection.rs b/src/web_socket_connection.rs index 33d1e3381..3a77e2f89 100644 --- a/src/web_socket_connection.rs +++ b/src/web_socket_connection.rs @@ -1,6 +1,7 @@ use crate::types::PyFunction; -use actix::{Actor, StreamHandler}; +use actix::prelude::*; +use actix::{Actor, AsyncContext, StreamHandler}; use actix_web::{web, Error, HttpRequest, HttpResponse}; use actix_web_actors::ws; use actix_web_actors::ws::WebsocketContext; @@ -10,8 +11,45 @@ use std::collections::HashMap; use std::sync::Arc; /// Define HTTP actor +#[derive(Clone)] struct MyWs { router: Arc>, + event_loop: PyObject, +} + +fn execute_ws_functionn( + handler_function: &PyFunction, + event_loop: PyObject, + ctx: &mut ws::WebsocketContext, + ws: &MyWs, +) { + match handler_function { + PyFunction::SyncFunction(handler) => Python::with_gil(|py| { + let handler = handler.as_ref(py); + // call execute function + let op = handler.call0().unwrap(); + let op: &str = op.extract().unwrap(); + ctx.text(op); + }), + PyFunction::CoRoutine(handler) => { + let fut = Python::with_gil(|py| { + let handler = handler.as_ref(py); + let coro = handler.call0().unwrap(); + pyo3_asyncio::into_future_with_loop(event_loop.as_ref(py), coro).unwrap() + }); + let f = async move { + let output = fut.await.unwrap(); + Python::with_gil(|py| { + let output: &str = output.extract(py).unwrap(); + output.to_string() + }) + }; + let f = f.into_actor(ws).map(|res, _, ctx| { + ctx.text(res); + }); + ctx.spawn(f); + } + } } // By default mailbox capacity is 16 messages. @@ -19,14 +57,26 @@ impl Actor for MyWs { type Context = ws::WebsocketContext; fn started(&mut self, ctx: &mut WebsocketContext) { + let handler_function = &self.router.get("connect").unwrap().0; + let _number_of_params = &self.router.get("connect").unwrap().1; + execute_ws_functionn(handler_function, self.event_loop.clone(), ctx, &self); + println!("Actor is alive"); } - fn stopped(&mut self, _ctx: &mut WebsocketContext) { - println!("Actor is alive"); + fn stopped(&mut self, ctx: &mut WebsocketContext) { + let handler_function = &self.router.get("close").expect("No close function").0; + let _number_of_params = &self.router.get("close").unwrap().1; + execute_ws_functionn(handler_function, self.event_loop.clone(), ctx, &self); + + println!("Actor is dead"); } } +#[derive(Message)] +#[rtype(result = "Result<(), ()>")] +struct CommandRunner(String); + /// Handler for ws::Message message impl StreamHandler> for MyWs { fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { @@ -36,19 +86,7 @@ impl StreamHandler> for MyWs { let handler_function = &self.router.get("connect").unwrap().0; let _number_of_params = &self.router.get("connect").unwrap().1; println!("{:?}", handler_function); - match handler_function { - PyFunction::SyncFunction(handler) => Python::with_gil(|py| { - let handler = handler.as_ref(py); - // call execute function - let op = handler.call0().unwrap(); - let op: &str = op.extract().unwrap(); - - println!("{}", op); - }), - PyFunction::CoRoutine(handler) => { - println!("Async functions are not supported in WS right now."); - } - } + execute_ws_functionn(handler_function, self.event_loop.clone(), ctx, &self); ctx.pong(&msg) } @@ -61,43 +99,15 @@ impl StreamHandler> for MyWs { // need to also passs this text as a param let handler_function = &self.router.get("message").unwrap().0; let _number_of_params = &self.router.get("message").unwrap().1; - println!("{:?}", handler_function); - match handler_function { - PyFunction::SyncFunction(handler) => Python::with_gil(|py| { - let handler = handler.as_ref(py); - // call execute function - let op = handler.call0().unwrap(); - let op: &str = op.extract().unwrap(); - - return ctx.text(op); - }), - PyFunction::CoRoutine(_handler) => { - println!("Async functions are not supported in WS right now."); - return ctx.text("Async Functions are not supported in WS right now."); - } - } + execute_ws_functionn(handler_function, self.event_loop.clone(), ctx, &self); } Ok(ws::Message::Binary(bin)) => ctx.binary(bin), Ok(ws::Message::Close(_close_reason)) => { println!("Socket was closed"); - let router = &self.router; - let handler_function = &self.router.get("close").unwrap().0; - let number_of_params = &self.router.get("close").unwrap().1; - println!("{:?}", handler_function); - match handler_function { - PyFunction::SyncFunction(handler) => Python::with_gil(|py| { - let handler = handler.as_ref(py); - // call execute function - let op = handler.call0().unwrap(); - let op: &str = op.extract().unwrap(); - - println!("{:?}", op); - }), - PyFunction::CoRoutine(handler) => { - println!("Async functions are not supported in WS right now."); - } - } + let handler_function = &self.router.get("close").expect("No close function").0; + let _number_of_params = &self.router.get("close").unwrap().1; + execute_ws_functionn(handler_function, self.event_loop.clone(), ctx, &self); } _ => (), } @@ -108,9 +118,17 @@ pub async fn start_web_socket( req: HttpRequest, stream: web::Payload, router: Arc>, + event_loop: PyObject, ) -> Result { // execute the async function here - let resp = ws::start(MyWs { router }, &req, stream); + let resp = ws::start( + MyWs { + router, + event_loop: event_loop.clone(), + }, + &req, + stream, + ); println!("{:?}", resp); resp }