ALICE’S COFFEELICIOUS IDEA - RDM 1 - EASY - 250 POINTS
ALICE SHARES HER COFFEELICIOUS RESEARCH - 500 POINTS - MEDIUM
BOB BECOMES COFFEELICIOUS - 800 POINTS - HARD
Alice wants to declutter the directory where she downloaded text files and images of each type of coffee. We are provided with the following metadata for the downloaded files:
Alice only downloads .png image files
The images files are of the following format -{species}-.png where {species} denote the name of the species of coffee and is surrounded by a trailing and leading hyphen (-) character. It is guaranteed that we are given two hyphen (-) characters in the file name
The text files are of the format {species}.txt where {species} denote the name of the species of coffee.
Image files are only downloaded for species whose description has been noted in a text file.
Alice wants the files to be arranged in the same with the following hierarchy:
e-commerce/coffee/{species}/about/desc.txt
e-commerce/coffee/{species}/images/{img}
,where {species} denote the name of the species and {img} denotes the original file name of the downloaded image of the corresponding species.
We need to help Alice with an executable that takes a path parameter (-path) and rearrange the files in the same directory as she wants it to be.
Solution:
There can be multiple programmatical ways by which we can solve this. We will be discussing koyumeishi’s Solution (#405852) here. He used Python to rearrange the files. We can choose to write the solution in any language we want, it can be a golang CLI or a bash script too.
Remember, the following is expected out of the cli
It should have an input parameter -path for the directory path
The cli, when executed with the input parameter should process the parameter and execute as necessary and exit.
Here’s how you can approach this problem:
We start by defining some variables that will help in generating paths. We also define a regex that will help us match our image name pattern to select only those images.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# Destination path format for species’ description desc_file_dest = 'e-commerce/coffee/{species}/about/desc.txt' # Destination path format for species’ images img_file_dest = 'e-commerce/coffee/{species}/images/{img}' # Regex for * -{ species } - * .png img_file_pattern = re.compile(r '[^\-]+\-([^\-]+)\-[^\-]+\.png') # Set Log Level logging.basicConfig(format = '%(levelname)s:%(message)s', level = logging.WARNING) # Get a logging instance logger = logging.getLogger('rearange_log')
The executable accepts a -path variable which contains the path to the folder containing .txt and .png files. This folder will be referred to as the target folder throughout this post. The below code demonstrates parsing the required parameter -path along with an additional parameter added for setting log level. The path is then passed on to the rearrange method which we discuss later.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# Code execution starts from here if __name__ == '__main__': # Define required arguments for the CLI parser = argparse.ArgumentParser( description = 'rearrange image files and txt files.') parser.add_argument('-path', help = 'path to the directory which contains ' + 'the descriptions and images. ', required = True) parser.add_argument('--silent', '-s', action = 'store_true', help = 'enable silent mode') # Parse arguments args = parser.parse_args() # Check argument value for setting required logelevel if args.silent is False: logger.setLevel('DEBUG') # Call the function to rearrange the files with the path argument. rearrange(args.path)
We were asked that the executable should rearrange the files as defined here:
a. Species description should be moved to e-commerce/coffee/{species}/about/desc.txt
b. Species images should be moved to e-commerce/coffee/{species}/images/{img}
The executable needs to ensure that no other files are moved and only .txt/.png files are moved to their respective folders.
This can be done by simply fetching the list of png images and text files
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# get file list
try:
entries = os.scandir(target_dir)
except OSError as e:
logger.error(e)
return
# Filter out all the files.(i.e.exclude the directories)
files = list(filter(lambda x: x.is_file(), entries))
# Filter out all.txt files
descriptions = filter(
lambda x: os.path.splitext(x.path)[1] == '.txt', files)
# Filter out all.png files
images = filter(
lambda x: os.path.splitext(x.path)[1] == '.png', files)
We can then choose one of them. Let’s take text files and move them as required
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# move txt files
for src in descriptions:
# Fetch the first part of filename without file extension
species = os.path.splitext(os.path.basename(src.name))[0]
species = species.lower()
try:
# Generate target path and move file
dst = desc_file_dest.format(species = species)
dst = os.path.join(target_dir, dst)
os.renames(src.path, dst)
logger.info(f 'moved description file.\n\t$src={src.path}\n\t$dst={dst}')
except OSError as e:
logger.warning(e)
Similarly, we can move the image files.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# move image files
for src in images:
# Fetch filename without file extension
name = os.path.basename(src.name)
# Match with regex to check
if matches our text file pattern
match = img_file_pattern.fullmatch(name)
if match:
species = match[1].lower()
else:
continue
try:
# Generate target path and move file
dst = img_file_dest.format(species = species, img = name)
dst = os.path.join(target_dir, dst)
os.renames(src.path, dst)
logger.info(f 'moved image file.\n\t$src={src.path}\n\t$dst={dst}')
except OSError as e:
logger.warning(e)
koyumeishi’s Full Solution (#405852)
Alice has collected some data about different species and has arranged them in the following file system hierarchy:
e-commerce/coffee/{species}/images/30sdfknl09123-arabica-23ij09d67as5123.png
e-commerce/coffee/{species}/about/desc.txt
e-commerce/coffee/{species}/about/price.txt
where {species} is the name of the coffee species, images contain multiple images of the respective species and about contains description and price of the respective species in a single file named desc.txt and price.txt respectively. Price.txt contains price with upto 2 decimal places without currency.
Alice wants to share this data through an api in a secure way.
We will help Alice build an api that would serve the above data to the outside world.
Solution:
The simplest way to solve this is to create an API that serves directly from the files system. Easiest but not the best way. We will be discussing better ways later in this post.
We will be discussing Ansary’s (#406021) solution here who used Node.js to. We will focus primarily on question related code snippets and will ignore the supporting classes (logging etc.).
We first need to define routes via which we will be serving our data. We can have our routes define like the following:
/species - This will fetch the list of coffee species via this route.
/species/:name - This will fetch the data for a particular type of species
/species/:name/image/:id - This will fetch the image for the provide coffee species and its associated file name.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const controller = require("./controller");
module.exports = {
"/species": {
get: {
method: controller.list,
},
},
"/species/:name": {
get: {
method: controller.get,
},
},
"/species/:name/image/:id": {
get: {
method: controller.download,
},
},
};
We now define our logic for all our routes.
The list route can simply list our files from our data directory with rootPath set as e-commerce/coffee.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function list() {
return new Promise((resolve, reject) => {
fs.readdir(rootPath, (err, files) => {
if (err || files == null) {
reject({
status: 400,
message: err.code == "ENOENT" ?
"Data directory is missing" :
"Invalid request. Please try again later.",
});
} else {
files = files.map((file) => get(file));
Promise.all(files)
.then((results) => resolve(results));
}
});
});
}
The route for specific species methods can simply read from the respective species directory and read the text files and provide metadata about the image files.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
async function get(name) {
return new Promise((resolve, reject) => {
fs.lstat(path.resolve(rootPath, name), (err, stats) => {
if (err)
reject({
status: 400,
message: "Invalid species name!",
});
else {
const description = path.join(rootPath, name, "about", "desc.txt");
const price = path.join(rootPath, name, "about", "price.txt");
const imagesDirectory = path.join(rootPath, name, "images");
Promise.all([
readText(description),
readText(price),
listImages(imagesDirectory),
])
.then((results) => {
resolve({
name,
description: results[0],
price: results[1],
images: results[2],
});
});
}
});
});
}
The image route can simply return the images stored under the images directory of the corresponding species specified in the route.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async function download(name, imageId) {
try {
return new Promise((resolve, reject) => {
const imagePath = path.resolve(
path.join(rootPath, name, "images", imageId + ".png")
);
fs.lstat(path.resolve(rootPath, name), (err) => {
if (err) {
reject({
status: 400,
message: `No image for species $\{name\} with imageId $\{imageId\} found.`,
});
} else {
resolve({
path: imagePath,
filename: imageId + ".png",
});
}
});
});
} catch (err) {
throw new Error({
status: 400,
message: "Not found",
});
}
}
And finally, a basic Docker setup.
Dockerfile
1
2
3
4
5
6
7
8
9
FROM node: 12
VOLUME["/data"]
COPY.. / species_api
WORKDIR / species_api
RUN npm install
CMD npm start
Take a official base image for our language runtime (nodejs in this case)
Then specify the mount points for our data directory where the images are stored
Copy our code into the docker image.
And finally install the required dependencies for the project and run it.
docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
version: "3"
services:
species_api:
image: species_api: latest
build:
context: .. /
dockerfile: docker / Dockerfile
env_file:
-api.env
volumes:
-.. / data: /data/
ports:
-3002: 3002
Specify our app container with the build context and Dockerfile location.
Provide any environment variables if requried (Here it’s done via env var file).
Mount the required data directories
Map the app ports to the port required be accessible from outside the docker network.
Learn more about docker here.
Ansary’s (#406021) full solution
Alice needs to sell a variety of coffee species and we need to help her out with an online website. She has exposed all the data through an api and has shared the swagger spec as well.
The website should provide the following functionalities:
List Coffee bean species on the landing page. Listing should include the name, a resized image of the species, and the price of the species in USD ($), the price is the unit price of 500gms.
Clicking on any list item should open up a view that helps the user to read about the coffee species and add units of 500gms each to their cart.
After adding coffee beans, users can proceed to order with their details like name, address, email, and phone number.
We are free to choose any web framework to get the site up and running.
Solution:
Here we have to help Alice set up her ecommerce store and that store should have the mentioned functionalities.
We start with defining the architecture for the ecommerce website. While every framework has its own way of defining a website architecture, an ideal website framework consists of the following:
A web server for serving data or even rendered pages (E.g. React.js + Node.js / ASP .NET). The web server is also used in scenarios where private api(s) / databases need to be consumed.
Client side code usually involving resource friendly operations.
We will be discussing birdofpreyru’s (#405790) solution here. It uses React in.frontend and node.js in the backend.
From the routes we can know React frontend consists of following routes (pages)
Landing page
Coffee page
Checkout page
Error 404 page.
Landing page: We first define our landing page. It contains the listing for each type of coffee species returned by the api.
Api response for GET /api/v1/coffee/species:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[
{
"id": "ae997efd-64ae-4ace-bf5b-45fad5525e43",
"name": "Arabica",
"imageUrl": "https://alicecoffeeshop.devtesting.live/images/arabica.png",
"price": 120.99,
"description": "This coffee bean with low caffeine and a smoother taste is aromatic and delicious. 80% of the coffee in the world is produced from these types of beans."
},
{
"id": "2d586cfb-554d-499b-aa5c-c5edafbe76f3",
"name": "Liberica",
"imageUrl": "https://alicecoffeeshop.devtesting.live/images/liberica.png",
"price": 89.99,
"description": "Liberica is a low yield type of coffee compared to Arabica and Robusta."
},
{
"id": "a28c12a1-2d46-4c23-a4b3-e7f8ec82792e",
"name": "Robusta",
"imageUrl": "https://alicecoffeeshop.devtesting.live/images/robusta.png",
"price": 110.99,
"description": "This type of coffee, which contains 2.5% more caffeine than other types, has a pretty strong taste."
}
]
The same can be rendered as shown below. We can also show our cart status and a landing banner to style it on the lines of coffee.
Now let’s see how the landing page is implemented. The Landing component structure is
We can see it renders the listing for each type of coffee species in line 55-63. Each type of coffee species is rendered by a CoffeeItem component. The structure of the CoffeeItem component is
We can see the CoffeeItem links to the coffee page, it also shows the coffee species images, name and unit price.
The Cart component is on the top-right corner, if you selected the species, it shows like
The component structure is
Coffee page. When clicked on the corresponding coffee species, it navigates to the coffee page and shows the description for each coffee. “Add to cart” provision can be provided in that section as well.
The page is rendered by the CoffeePage component. The component struct is as below.
(1) Render coffee species image, name, unit price, description and current order.
(2) The two buttons Add 500g and Remove 500g.
Checkout page. After adding to cart, users can edit cart items or continue to checkout on the checkout page. For checkout, the user would require to enter their details and proceed to submit.
The page is rendered by the Checkout component. The component struct is as below.
(1) The Your Order table
(2) Your details form for entering their details.
Finally, on submitting the order, we can show an order confirmation page.
birdofpreyru’s (#405790) Full Solution