Skip to content

Instantly share code, notes, and snippets.

@oxtopus
Forked from bcavagnolo/asset-store-spec.md
Last active June 10, 2017 19:02
Show Gist options
  • Save oxtopus/5d46d4ac85ed3902990a394679b29fdf to your computer and use it in GitHub Desktop.
Save oxtopus/5d46d4ac85ed3902990a394679b29fdf to your computer and use it in GitHub Desktop.
asset store spec

Asset Store

webapp.py comprises a simple web application to host assets such that:

  • Every asset that we store has an "asset name". There are some rules about asset names:
    • They must be globally unique across all assets
    • They can only contain alphanumeric ascii characters, underscores, and dashes
    • They cannot start with an underscore or dash
    • They must be between 4 and 64 characters long
  • Every asset has an "asset type" which is a string that must either be "satellite" or "antenna".
  • Every asset has an "asset class" which depends on its asset type and is a string. For "satellite" assets it can be "dove" or "rapideye". For "antenna" assets it can be "dish" or "yagi".
  • Users can create assets. To do so, they must supply the asset name, asset type, and asset class.
  • Users can retrieve the whole list of assets.
  • Users can retrieve a single asset by name.
  • An asset cannot be deleted. Also its name, type, and class cannot be changed.

Before you begin, install requirements:

$ pip install [--user] -r requirements.txt

You may also run unit tests with py.test or nosetests.

Start Web App

Upon executing the below command, a web server will be listening on port 8080, ready to handle requests for assets.

$ python webapp.py 8080

Usage

Command-line use-cases are provided below.

GET /

Gets a list of all assets, one-per-line and urlencoded.

$ curl -i http://0.0.0.0:8080/
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Sat, 10 Jun 2017 09:29:14 GMT
Server: localhost

name=aaaa&type=satellite&class=dove
name=aaab&type=antenna&class=dish

GET /{name}

Gets a specific urlencoded asset.

$ curl -i http://0.0.0.0:8080/aaaa
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Sat, 10 Jun 2017 09:30:54 GMT
Server: localhost

name=aaaa&type=satellite&class=dove

PUT /{name}

Create an asset at /{name}

$ curl -X PUT -d "name=aaaa&type=satellite&class=dove" -i http://0.0.0.0:8080/aaaa
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Sat, 10 Jun 2017 09:32:29 GMT
Server: localhost

name=aaaa&type=satellite&class=dove

Subsequent calls to PUT /{name} where {name} already exists will return a 403 Forbidden error, e.g.

$ curl -X PUT -d "name=aaaa&type=satellite&class=dove" -i http://0.0.0.0:8080/aaaa
HTTP/1.1 403 Forbidden
Content-Type: text/html
Transfer-Encoding: chunked
Date: Sat, 10 Jun 2017 10:38:25 GMT
Server: localhost

forbidden
``

Asset Store Coding Challenge

The doves are loose and lots of other satellites are showing up to the party. Antennas to communicate with the ever expanding fleet are popping up like wildflowers. To keep track of them all, we need an asset store. We've already worked out the initial requirements and made a few design decisions:

  • The asset store is going to be exposed as a custom RESTful web API written in python.

  • Every asset that we store has an "asset name". There are some rules about asset names:

    • They must be globally unique across all assets
    • They can only contain alphanumeric ascii characters, underscores, and dashes
    • They cannot start with an underscore or dash
    • They must be between 4 and 64 characters long
  • Every asset has an "asset type" which is a string that must either be "satellite" or "antenna".

  • Every asset has an "asset class" which depends on its asset type and is a string. For "satellite" assets it can be "dove" or "rapideye". For "antenna" assets it can be "dish" or "yagi".

  • Users can create assets. To do so, they must supply the asset name, asset type, and asset class.

  • Users can retrieve the whole list of assets.

  • Users can retrieve a single asset by name.

  • An asset cannot be deleted. Also its name, type, and class cannot be changed.

Implementation Notes:

Any design decisions not specified herein are fair game. Completed projects will be evaluated on how closely they follow the spec, their design, and cleanliness of implementation.

Completed projects must be delivered in a git repo (or similar) available to the reviewer.

Completed projects must include a README with enough instructions for evaluators to build and run the code. Bonus points for builds which require minimal manual steps.

This project should not take more than 3-4 hours to complete. Do not get hung up on scaling or persistence issues. This is a project used to evaluate your design and implementation skills only.

Please include any unit or integration tests used to verify correctness.

Extra Credit

You are not expected to spend more than 3-4 hours on this exercise. Indeed, you may not even need that much time. But here are some ideas if you want to go bigger:

  • Implement "asset details". Every asset has "asset details". This is a list of key/value pairs that describe the asset. The list of expected/acceptable key/value pairs depends on the asset class. Specifically:

    • dish: dishes have a "diameter" key whose value is a float and a "radome" key whose value is a boolean.
    • yagi: yagis have a "gain" key whose value is a float.

    Users can set the details of an asset.

  • Require an X-User header that is set to the username. The user can only create assets if their name is admin. Don't worry about actually implementing some kind of authentication scheme. Just use the header if it's available.

  • Implemenet filtering. When a user specifies an asset_class or asset_type to the "whole list of assets" feature, the returned list should be appropriately filtered.

import webapp
from mock import patch, mock_open
from pathlib2 import Path
import os
def test_isValidName():
# 4-64 chars
assert not webapp.isValidName("a")
assert not webapp.isValidName("aa")
assert not webapp.isValidName("aaa")
assert webapp.isValidName("aaaa")
assert webapp.isValidName("0000")
assert webapp.isValidName("0a0a")
assert webapp.isValidName("a0a0")
assert webapp.isValidName("a0A0")
assert webapp.isValidName("Aa0A")
assert webapp.isValidName("a0A-")
assert webapp.isValidName("a0A_" * 8) # 32 chars
assert webapp.isValidName("a0A_" * 16) # 64 chars
assert not webapp.isValidName(("a0A_" * 16) + "a") # >64 chars
# cannot start with an underscore or dash
assert not webapp.isValidName("-aaa")
assert not webapp.isValidName("_aaa")
# can only contain alphanumeric ascii characters, underscores, and dashes
assert not webapp.isValidName("aaaa!")
assert not webapp.isValidName("aa aa")
assert webapp.isValidName("this-is-totally-valid")
assert not webapp.isValidName("this is not!")
def test_isValidAsset():
"""
- Every asset has an "asset type" which is a string that must either be
"satellite" or "antenna".
- Every asset has an "asset class" which depends on its asset type and is a
string. For "satellite" assets it can be "dove" or "rapideye". For
"antenna" assets it can be "dish" or "yagi".
"""
assert webapp.isValidAsset(webapp.Asset(name="0aA-", type="satellite", class_="dove"))
assert webapp.isValidAsset(webapp.Asset(name="0aA-", type="satellite", class_="rapideye"))
assert webapp.isValidAsset(webapp.Asset(name="0aA-", type="antenna", class_="dish"))
assert webapp.isValidAsset(webapp.Asset(name="0aA-", type="antenna", class_="yagi"))
assert not webapp.isValidAsset(webapp.Asset(name="0aA-", type="satellite", class_="dish"))
assert not webapp.isValidAsset(webapp.Asset(name="0aA-", type="satellite", class_="yagi"))
assert not webapp.isValidAsset(webapp.Asset(name="0aA-", type="satellite", class_="abc"))
assert not webapp.isValidAsset(webapp.Asset(name="0aA-", type="antenna", class_="dove"))
assert not webapp.isValidAsset(webapp.Asset(name="0aA-", type="antenna", class_="rapideye"))
assert not webapp.isValidAsset(webapp.Asset(name="0aA-", type="antenna", class_="cba"))
@patch("webapp.databasePath", new=Path("test.pkl"))
def test_assetPutAndGet():
if os.path.exists("test.pkl"):
os.unlink("test.pkl")
asset = webapp.Asset(name="aaaa", type="satellite", class_="dove")
webapp.putAsset(asset)
newAsset = webapp.getAsset(asset.name)
assert newAsset == asset
@patch("webapp.databasePath", new=Path("test.pkl"))
def test_webapp():
if os.path.exists("test.pkl"):
os.unlink("test.pkl")
webapp.putAsset(webapp.Asset(name="aaaa", type="satellite", class_="dove"))
webapp.putAsset(webapp.Asset(name="A0a-", type="antenna", class_="dish"))
resp = webapp.app.request("/")
assert resp.status == "200 OK"
assert resp.data == 'name=A0a-&type=antenna&class=dish\nname=aaaa&type=satellite&class=dove\n'
resp = webapp.app.request("/A0a-")
assert resp.status == "200 OK"
assert resp.data == "name=A0a-&type=antenna&class=dish\n"
import cPickle
from collections import namedtuple
from pathlib2 import Path
import re
import urllib
import web
urls = (
"/(.+?)", "AssetHandler", # Capture name
"/", "AssetHandler"
)
Asset = namedtuple("Asset", "name type class_")
validNamePattern=re.compile("^([a-zA-Z0-9]{1}[a-zA-Z0-9_\-]{3,63})$")
databasePath = Path("database.pkl")
def isValidName(name):
match = validNamePattern.match(name)
if match:
return match.group(0) == name
return False
def isValidAsset(asset):
if not isValidName(asset.name):
return False
if asset.type == "satellite":
return asset.class_ in {"dove", "rapideye"}
elif asset.type == "antenna":
return asset.class_ in {"dish", "yagi"}
return False
def getAllAssets():
# Users can retrieve the whole list of assets.
db = {}
if not databasePath.is_file():
with open(str(databasePath), "w") as fd:
cPickle.dump(db, fd)
with open(str(databasePath), "r") as fd:
database = cPickle.load(fd)
for values in database.values():
yield values
def getAsset(name):
# Users can retrieve a single asset by name.
db = {}
if not databasePath.is_file():
with open(str(databasePath), "w") as fd:
cPickle.dump(db, fd)
with open(str(databasePath), "r") as fd:
db = cPickle.load(fd)
values = db.get(name)
if values is not None:
return Asset._make(values)
def putAsset(asset):
# Users can create assets.
db = {}
if not databasePath.is_file():
with open(str(databasePath), "w") as fd:
cPickle.dump(db, fd)
with open(str(databasePath), "r+") as fd:
fd.seek(0)
try:
db = cPickle.load(fd)
except EOFError:
print "EOFError", str(databasePath), fd
db = {}
if asset.name in db:
# Assets are immutable
raise web.forbidden()
db[asset.name] = tuple(asset)
fd.seek(0)
fd.truncate()
cPickle.dump(db, fd)
fd.flush()
def render(values):
"""
Render an asset for display/consumption by client
:param values: Asset field values
:return: url-encoded str representing an asset
"""
asset = Asset._make(values)._asdict()
# Need to hide "class_" implementation detail from user
asset["class"] = asset["class_"]
del asset["class_"]
return urllib.urlencode(asset) + "\n"
class AssetHandler(object):
def GET(self, name=None):
if name:
if isValidName(name):
# Retrieve asset by name, return to user
try:
yield render(getAsset(name))
except:
raise web.notfound()
else:
raise web.forbidden()
else:
for asset in sorted(getAllAssets()):
yield render(asset)
def PUT(self, name):
kwargs = {"name": name}
kwargs.update(**web.input())
kwargs["class_"] = kwargs["class"]
del kwargs["class"]
putAsset(Asset(**kwargs))
return render(getAsset(name))
app = web.application(urls, globals())
def main():
app.run()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment