This repository contains my solution for a coding challenge as part of Crossmint's Internship program.
- Project Structure π
- Setup π§
- Running Instructions π¦Ύ
- Documentation π
- Challenge Approach π§©
- Personal improvements π
- Contact me π¬
crossmint-challenge/
βββ πsrc
β βββ main.py
β βββ api_interactions.py # API interactions
β βββ astral_objects.py # Definition of astral objects using subclasses
β βββ utils.py # Util functions for development
βββ πdocs
β βββ πbuild
β β βββ πhtml # HTML documentation files β¨
β β β βββ (HTML documentation files)
β βββ (...)
βββ .env.template # Template for environment variables
βββ requirements.txt # Python package dependencies
βββ .gitignore
βββ README.md
-
Clone the repository and install the required packages:
git clone https://github.com/jandrana/crossmint-challenge && cd crossmint-challenge && pip install -r requirements.txt
-
Configure environment variables
- Copy the
.env.template
to a new.env
filecp .env.template .env
- Edit and fill the
.env
file with the correct values
- Copy the
To run the application use the following command:
python src/main.py
This project uses Sphinx to generate documentation from the Google-style docstrings added in the code.
If you want to view the documentation, open the index.html
file in your browser.
To update/create the documentation, run the following command in the project's root directory:
cd docs && sphinx-apidoc -o source ../src && make html
This command builds the HTML documentation from the docstrings added in the application
During this section you can find a detailed explanation of the approach and steps taken in order to solve Crossmint's Internship Challenge.
Development Time Summary:
- Preparation, analysis and planning of approach π (~1 hour)
- Development for Phase 1 π (2-3 hours)
- Development for Phase 2 π (~1 hours)
Total: 4-5 hours
π§ Warning: If you're not in the mood for detailed explanations of code or my solving/thought processes, feel free to skip to the next Personal improvements section! π
- Thorough understanding of the challenge, its requirements and the evaluation criteria.
- Reading and understanding of the provided Megaverse API Documentation while quickly refreshing my knowledge on REST APIs
- Considered different ways of approaching the problem and thought of potential Python packages dependencies.
- Break down of the problem into small tasks to make it easier to solve.
Click here to see the steps taken!
- Intalled and added to
requirements.txt
basic needed Python packages likerequests
for API interactions andpython-dotenv
for environment variables. - Created source files with pseudo-code depending on their code functionality
main.py
as main entry point of the programapi_interactions.py
for code regarding API interactions.astral_objects.py
for defining the astral objects of the megaverse
Project Structure Details
Designed a basic structure for the project to ensure that the project is: modular, scalable, mantainable and easy to understand:
crossmint-challenge/
βββ πsrc # Folder with source code files
β βββ *.py
βββ requirements.txt # To fill during development with package dependencies
βββ .gitignore # Basic .gitignore for temporary/cache/private files
βββ README.md
Packages and source files pseudo-code
- New package needed:
python-dotenv
(added torequirements.txt
). - Implemented error handling for inexistent or empty variables in
.env
file. - Implementation of the
__init__
method using thecandidate_id
andbase_url
of the API. - Implementation of the
get_goal_map
method for retrieving my current goal map and validating that the interaction with the API worked as expected with the givencandidate_id
and url. - Implementation of the
create_polyanet
method - Implementation of the
_post
helper method
1. Handling private information needed for interacting with the API
I considered some information needed for interacting with the API to be private (my candidate ID and the API base URL).
Implementing a solution for this:
This situation lead me to store this information in 2 environment variables located in a new ignored .env
file in the root directory of the repository.
Implemented a new function get_env_variable
for retrieving the value of both variables stored in the .env
file.
2. Creation of the API Client.
Implemented a new ApiClient
class with the goal of handling interactions with the megaverse API. Steps taken:
- Retrieving
candidate_id
andbase_url
values using theget_env_variable
function - Created
api_client
variable and assigned it to the initilization ofApiClient
withcandidate_id
andbase_url
. - Retrieved
goal_map
value using the methodget_goal_map
fromapi_client
Details of new main.py
Created main
function for testing the functionality of api_interactions.py
by:
After finishing this step I figured it would be a good idea to implement a new helper method in the ApiClient
for handling potential API errors. See implementation in: 2.5 Handling API Errors
- Implemented
__init__
method usingrow
andcolumn
attributes that indicate an object's position - Implemented
create
abstract method for ensuring that every child class ofAstralObject
implements acreate
method. If it's doesn't, it raises a NotImplementedError. Polyanet
: Implementedcreate
method, which usesapi_client.create_polyanet
and the object's position for creating the desired object.Soloon
andCometh
sketched for now, since they were not needed yet
Created parent class AstralObject
Defined child classes for AstralObject
representing each megaverse objects
Updated main.py
for evaluating the progress made until now by incorporating the functions and classes developed.
- Parses the given
goal_map
and creates a listmegaverse_objects
based on the retrieved information. Each element of the list contains an astral object class with its position (e.g. "Polyanet(row, column)"). - Iterates
megaverse_objects
and creates each object using thecreate
method of such object class using the variableapi_client
as argument. - At the end, it returns the
megaverse_objects
list. This return simply facilitates the usage of util functions likeprint_megaverse
for development and debugging uses.
1. Created new function create_megaverse
Note: Function located in main.py
due to its importance and direct relationship with the main
Functionality:
2. Updated main
to incorporate the new create_megaverse
Integrating this new function in the main
allowed me to integrate all the functions and classes developed for completing my first megaverse!
Details of the new implementation for error handling
Even though the first phase was successfully finished, before moving to the second phase I prioritized the implementation of a clean and comprehensive error handler regarding API exceptions. This way I could get rid of repetitive handling of exceptions found in various methods.
Steps taken to achieve this:
- Created new
_handle_error
helper method inside theApiClient
class. - Added different messages depending on multiple types of request exceptions. This way I made sure of handling diverse HTTP and connection errors, timeouts and other unexpected errors.
- For improving the error handling output, I added an extra argument
re_raise=False
. If re_raise isTrue
, after printing the specific error message of an exception, the caught exception is re raised and outputs the Traceback of the exception. It isFalse
by default in order to simplify the output of the error. It can be easily changed toTrue
if futher detail of the Exception is needed. - Finally, I updated the other methods so that they manage exceptions using this new
_handle_error
helper method.
This step required further testing of the program when encountering diverse types of errors.
Implemented create_soloon
and create_cometh
methods
Implemented both of these new methods to the ApiClient
Class with their respective url.
Both methods are very similar to the create_polyanet
but they include an extra key in their data dictionary (color
for soloons and direction
for comeths).
Implemented exponential backoff logic to the _post
method
Implemented exponential backoff retry logic when receiving exceptions regarding "Too Many Request for URL". Regarding any other exceptions, it uses the _handle_error
helper method.
Note: The retry method is performed as long as the indicated exception occurs less than 5 times in a row.
Finished the missing child classes of AstralObject
: Soloon
and Cometh
Both are very similar to the Polyanet
subclass with some extra things to take into account.
- Both of these child classes include an
__init__
method that inherits therow
andcolumn
attributes from their parent classAstralObject
and contain an extra attribute that is specific to each class.Soloon
's specific attribute iscolor
Cometh
's specific attribute isdirection
- Implemented
create
method to both new child classes, which usesapi_client.create_[object_name]
(where [object name] is soloon or cometh) providing as arguments the object's inherited and specific attributes (row
,column
andcolor
/direction
).
Finally, I will provide a much more detailed explanation of how I managed to finish the second phase of the challenge by updating create_megaverse
. By making the function able to successfully interpret the objects present in the goal_map
.
Click here for Details! | Updated create_megaverse
It is important to understand that in order to create a Soloon
or Cometh
we need not only the row
and column
attributes for knowing that object's position but we also need to know its specific attributes. For Soloon
objects we need its color
and, for Cometh
objects we need its direction
.
To achieve this, I updated the goal_map
parsing to also look for values ending with the word SOLOON
and COMETH
. If it finds a value with such characteristics, it retrieves the object's specific attribute (color
/direction
) from the object's name.
EXAMPLE: Detailed functionality explanation:
Let me explain the map parsing functionality with an example where [goal_map : ["BLUE_SOLOON", ...]]
:
-
What us mortal should interpret:
goal_map
says that there should be a Blue Soloon at the firstrow
andcolumn
of our map. -
REMINDER: Attributes needed for creating a Soloon are: row, column and color.
How does create_megavese
interpret this?
-
During the map parsing, at the first column and row of the map we find the value
"BLUE_SOLOON"
, which ends with the word SOLOON.Great! Now I know I have to create a new
Soloon
atrow=0
andcolumn=0
-
Current missing information for creating a
Soloon
: desired value for thecolor
attribute. -
To find out the
color
of theSoloon
, the function uses the following line of code:color = value.split('_')[0].lower()
Let's break down in a "code-like" format what this line does following the same example:
# Reminder: in this example, value = "BLUE_SOLOON" # 1. Splits into two `"BLUE_SOLOON"` with `"_"` as delimiter --> ["BLUE", "SOLOON"] "BLUE_SOLOON".split('_') = ["BLUE", "SOLOON"] # 2. Retrieves the 1st value of the splitted string [BLUE, SOLOON][0] = "BLUE" # 3. Converts to lowercase the retrieved value "BLUE".lower() = "blue": # 4. Assigns the converted value ("blue") to the missing attribute (color) color = "blue"
Truth is, as much as I love learning and solving challenges, I also love giving some personal enhancements to the project's I develop. Which is why I took the freedom of making the following improvements.
Since I considered some needed variables to be private (my candidate ID and the metaverse api base URL), I preferred to store this information in environment variables at the .env
file (which of course I added to the .gitignore).
Since the .env
file is not present in this public repository, I created a new .env.template
which serves as an example of what the .env
should look like. Naturally, this new file needs to be renamed and edited filling the values of each variable. See the Setup Section for an easy setup.
If you have seen my GitHub, you should have noticed by now that I love making clean and documented code for others to see and understand. Following this practice has helped countless times, it daily helps me when mentoring other 42 students by making it much easier for everyone to understand a code that they didn't develop. But it has specially helped me when developing collaborative projects.
This project couldn't be less! So I decided to implement Docstrings (following Google's Style Guide) to all the functions and classes present in the src/
folder of this repository.
After doing some research regarding the usage of Docstrings for documenting code I found an amazing Python package: Sphinx
Sphinx is a documentation generator that easily generates documentation for your project!
It retrieves all of your Docstrings present in your source code and with a few configurations, it generates multiple .html
files containing your project documentation.
You can see the generated documentation if you open in your browser the .html
files present at docs/build/html.
Note: In order to be able to see the html files (in their proper format), make sure to clone this repository and open the downloaded files in your browser.
During the development of the project I created a couple of helper functions made to "see" what was happening inside the program. After finishing the project I thought the best option was to move them to the utils/
folder, since they are not actively useful for the program to work but they are very useful for developing/debugging cases.
During the project I also created a test/
folder, making use of the pytest
python package. Truth is, it is my first experience using this package and I just wanted to play around and learn about it, which is why I deleted such folder from this main
branch of the repo. BUT! If you are very very curious about it, you can take a look to the test
branch of this repo and see it!
Finally, I culminated the project by creating this READMEβbecause a finished project seems too empty without it
I love making README's for a project's presentation which is why I do it in all most of my reposβI'm getting there π«
Anyways, In this README, as you have seen, I added some important and basic information regarding this project. Since I was already working on this, I wanted to add what was going to be a "small" section regarding my Challenge Experience and Approach. I know, it definitely ended up being bigger than "small" but I am sure it served well its purpose.
I'm always happy to talk about experiences and share thoughts with anyone interested in coding, so feel free to contact me!
- Github: @jandrana
- Mail: [email protected]