An easy solution to this is to construct your url dynamically based on user input (you can use str.format() to do that). For example:
#!/usr/bin/env python
from requests import get
from bs4 import BeautifulSoup
import sys
v1 = sys.argv[1]
v2 = sys.argv[2]
amount = sys.argv[3]
# check if the values passed are valid
# and construct the url like so:
currency_page = 'http://www......../convert/?Amount={}&From={}&To={}'.format(amount,v1,v2)
currency = get(currency_page).text
currency_data = BeautifulSoup(currency, 'html.parser')
USD = currency_data.find('span', attrs={'class': 'uccResultUnit'})
USD_PKR = USD.text.strip()
print(USD_PKR)
Results:
$ ./test.py EUR PKR 1
1 EUR = 125.790 PKR
The other solution, as also mentioned in the comments, is to use
- an API or
- a module instead.
Videos
If i understand correctly, the user will input either "0", "1" or "2" as a input, for the convertion for dollar, euro or yen respectively. So you need to change the initial values of dollar, euro and yen. Changing that, the version with integer input will work.
dollar = 0
euro = 1
yen = 2
If the input is a string such as "dollar", "euro" or "yen", the variables need to be changed to these respective strings, so firstly you need to set the variable as:
dollar = "dollar"
euro = "euro"
yen = "yen"
Then change the input type for raw_input, as python requires this type of input to recognize a String input. So change convert_to to:
convert_to = raw_input ("What currency do you want to convert to? ")
If you try to pass a string to a input variable it will always return 0. And since the initial values for all the variable are 0, the if statements returns true when comparing convert_to to dollar , euro or yen. Because the comparsion to the dollar is hapening first, it always goes to that case.
These are the changes necessary for the code to run.
Extra: You also don't need the variables in this case, since you're only using them in one field. So on your if statements, you can just compare them to the specific strings, as:
if convert_to == "dollars":
amount *= 1.3
elif convert_to == "euros":
amount *= 1.17
elif convert_to == "yen":
amount *= 133.66
else:
print "You must pick either dollar, euro or yen."
Hope it helps.
Please try to change to this:
if convert_to == "dollar":
amount = float(amount) * 1.3
elif convert_to == "euro":
amount = float(amount) * 1.17
elif convert_to == "yen":
amount = float(amount) * 133.66
There are 2 changes. First one is to set currencies (dollar , euro , yen) to string cause that way it will be possible to compare it with the user's input which will be also a string. Second, amount that is entered by user is string again so we have to convert it to float in order to calculate the converted amount.
Sorry to hear that; at a first glance the code looks okay, naming and splitting into functionality looks good, as well as the use of appropriate libraries, but there's a number of things that could be improved too.
For starters, on the GitHub README.md you're listing a couple of
libraries that have to be installed - so it would be easy enough to add
a requirements.txt for pip to install them (or an equivalent setup
via virtualenv etc.). As an interviewer the smoother the whole thing
goes, from downloading to seeing the expected result, the better!
Consider next time how someone else runs your project, make it as easy
as possible for them, while not going overboard (like, unless you're
going to provide a Docker image for them (which is obviously an option
...) for a Python project: Requiring them to have Python 3.X installed
is okay, same for pip, but the specific libraries not; give them a
Makefile perhaps such that make dependencies runs
pip install -r requirements.txt or so).
Next, the README.md says Run a main.py file..., but instead you're
using __main__.py, so the documentation should be updated; like I'd
rather have it say "Run PYTHONPATH=. python3 -m pyapi." since I might
not be familiar with all variants of how Python programs can be invoked
(e.g. I had to play with a few different variants till I figured out how
to run it ... an interviewer likely has less patience and time than me.)
Okay, so I run pyapi, then curl the examples from the description page
(not yours mind you, it'd be a good idea to have it all on your
repository too so it doesn't depend on someone else's files!), the first
example fails, the second one looks okayish:
0 ~ % curl "http://localhost:5000/currency_converter?amount=0.9&input_currency=¥&output_currency=AUD"
Currency not recognized%
0 ~ % curl "http://localhost:5000/currency_converter?amount=10.92&input_currency=£"
{
"input": {
"amount": 10.92,
"currency": "\u00c2\u00a3"
},
"output": {
"ALL": null
}
}%
Though, well, the output is empty. Can't really tell why except it
looks like I'm inputting the wrong currencies? Using EUR actually
works and gives me output:
0 ~ % curl "http://localhost:5000/currency_converter?amount=10.92&input_currency=EUR"
{
"input": {
"amount": 10.92,
"currency": "EUR"
},
"output": {
"ALL": 1380.78
}
}%
Now CLI, much better, first example works out of the box, second and
third don't, same problem, the currency unit isn't converted somehow.
Though if I do it myself it works (JPY/GBP). Ah, right, there's
some where the currency unit isn't unique, that's fine I guess.
However, if the output currency isn't specified I read the problem
statement such that it should then return the conversion to all other
currencies ("if output_currency param is missing, convert to all known
currencies"), but that's not what's happening:
0 kiwi-currencies % PYTHONPATH=. python3 -m pycli --amount 10.92 --input_currency EUR
INFO: FUNC: create_currencies_dict
INFO: FUNC: sign_to_abbreviation parameters: curr:EUR
INFO: FUNC: sign_to_abbreviation parameters: curr:None
INFO: FUNC: create_json parameters: inp:EUR out:None am=10.92
INFO: FUNC: convert parameters: inp:EUR out:ALL am=10.92
INFO: FUNC: contact_api parameters: inp:EUR out:ALL
INFO: FUNC: contact_api Loading from CACHE: True
INFO: FUNC: convert rate: {'EUR_ALL': 126.445488}
{
"input": {
"amount": 10.92,
"currency": "EUR"
},
"output": {
"ALL": 1380.78
}
}
What does ALL mean, "Albanian Leak"?
Now to the actual code, both of the __main__.py look mostly okay to
me, but, this isn't production quality code: There are no error checks
or fallbacks. If an expected argument is missing e.g. the HTTP API
crashes (the CLI is better since the command line argument parser checks
for it).
test.py looks pretty good! Though I'd suggest not to use assert like this. It could trivially be disabled when optimisations are turned
on:
0 kiwi-currencies % python3 test.py
INFO: FUNC: create_currencies_dict
INFO: TEST: test_contact_api <MagicMock name='contact_api' id='140590866107192'>
INFO: TEST: simple_contact_api
.INFO: TEST: test_sign_to_abbreviation
INFO: FUNC: sign_to_abbreviation parameters: curr:€
INFO: FUNC: sign_to_abbreviation parameters: curr:£
.INFO: TEST: test_unknown_input
INFO: FUNC: convert parameters: inp:USD out:EgUR am=45.2
test.py:54: DeprecationWarning: Please use assertEqual instead.
"Currency not recognized")
.
----------------------------------------------------------------------
Ran 3 tests in 0.007s
OK
0 kiwi-currencies % python3 -O test.py
INFO: FUNC: create_currencies_dict
INFO: TEST: test_contact_api <MagicMock name='contact_api' id='139676823006232'>
.INFO: TEST: test_sign_to_abbreviation
.INFO: TEST: test_unknown_input
INFO: FUNC: convert parameters: inp:USD out:EgUR am=45.2
test.py:54: DeprecationWarning: Please use assertEqual instead.
"Currency not recognized")
.
----------------------------------------------------------------------
Ran 3 tests in 0.018s
OK
Notice how the second run doesn't exercise some of the tests!
Now for service.py.
- You have comments, good, though I also imagine having some of those as
docstrings (
contact_api) might be nicer. - Logging is on, great, that's helpful once you've read the code.
sign_to_abbreviation, theforloop should be a dictionary lookup? Also the assignment tocurrcan be removed and directlyreturned.create_json,dict = {}is overwritten immediately after. Same withdict["output"] = {}. Alsonot x == yshould bex != y.create_currencies_dict, it might make sense to check for the inverse and return early to keep the indendation shorter, e.g.if currencies_symbols: returnandif response.status_code != 200: return None.else: passat the end of the loop can also be omitted.return Nonecan also be omitted in general, or even justreturn. If you want to be explicit that's fine though.
So overall there are priorities: make it work, show the user how (not just by test cases, by writing down the exact invocation for them to use), then make it nicer. You're on the right way though!
First of all, many thanks for your reply. It has brought many interesting information to my attention resulting in an increased quality of the project.
To start with, I must say there were few mistakes in a code resulting in an improper functionality. All the calls without a stated
output_currency should have given a different output, the problem was in a wrong indentation of the last line of a code snippet below (it was on the same level as the second-to-last line and therefore always being rewritten instead of added):service.py, create_json:
..
output_currencies = output_currency.split(",")
for curr in output_currencies:
if curr != input_currency:
if "output" not in dict:
dict["output"] = {}
dict["output"].update({curr: convert(input_currency, curr, amount)})
..
To continue, here is an updated version of
README.md:
Currency Converter
A practical task for a position of Junior Python Developer. Task entry: https://gist.github.com/MichalCab/c1dce3149d5131d89c5bbddbc602777c
Prerequisites
python 3.4
Requirements
Required libraries can be found in
requirements.txtand installed viapip3 install -r requirements.txtRun application
Run a
__main__.pyfile, either in pyapi or pycli folder depending on a desired form of usage.Parameters
amount- amount which we want to convert - floatinput_currency- input currency - 3 letters name or currency symboloutput_currency- requested/output currency - 3 letters name or currency symbolNote that a single currency symbol can represent several currencies:
- in case this happens withoutput_currency, convert to all known currencies with such symbol
- in case this happens withinput_currency, conversion is not performer. Rather, an info message with currencies having such symbol is shown, so a user can specifyinput_currencymore preciselyOutput Possibilities:
- json with a following structure:
Single input and output currency:
{ "input": { "amount": <float>, "currency": <3 letter currency code> } "output": { <3 letter currency code>: <float> } }Single input and multiple output currencies (in case a currency sign represents more currencies):
{ "input": { "amount": <float>, "currency": <3 letter currency code> } "output": { <corresponding 3 letter currency code>: <float> <corresponding 3 letter currency code>: <float> . . } }Single input and no output currency - convert to all known currencies:
{ "input": { "amount": <float>, "currency": <3 letter currency code> } "output": { <3 letter currency code>: <float> <3 letter currency code>: <float> <3 letter currency code>: <float> . . } }
- Info message:
Multiple input currencies (in case a currency sign represents more currencies):
"Input currency not clearly defined. Possible currencies with such symbol: <possible currencies>"Unknown input currency:
"Input currency not recognized"Unknown output currency:
"Output currency not recognized"Examples
CLI
./currency_converter.py --amount 100.0 --input_currency EUR --output_currency CZK { "input": { "amount": 100.0, "currency": "EUR" }, "output": { "CZK": 2561.78 } }
./currency_converter.py --amount 0.9 --input_currency € --output_currency AUD { "input": { "amount": 0.9, "currency": "EUR" }, "output": { "AUD": 1.46 } }
./currency_converter.py --amount 10.92 --input_currency zł { "input": { "amount": 10.92, "currency": "PLN" }, "output": { "HRK": 18.84, "UZS": 24006.34, "RUB": 196.93, "BOB": 20.64, . . . } }
./currency_converter.py --amount 10.92 --input_currency EUR --output_currency £ { "input": { "amount": 10.92, "currency": "EUR" }, "output": { "GBP": 9.79, "FKP": 9.77, "LBP": 19462.11, "SHP": 16.97, "SYP": 6617.36, "EGP": 230.18, "GIP": 9.77 }
}
./currency_converter.py --amount 10.92 --input_currency Nonsense_curr Input currency not recognizedAPI
Note: When using curl, currencies symbols are not decoded properly and therefore not recognised. A recommended tool is Postman.
GET /currency_converter?amount=4.5&input_currency=₱&output_currency=VEF HTTP/1.1 { "input": { "amount": 4.5, "currency": "PHP" }, "output": { "VEF": 20633.77 } }
GET /currency_converter?amount=10.92&input_currency=£ HTTP/1.1 Input currency not clearly defined. Possible currencies with such symbol: SHP,FKP,EGP,LBP,SYP,GIP,GBP
GET /currency_converter?amount=10.92&input_currency=₦ HTTP/1.1 { "input": { "amount": 10.92, "currency": "NGN" }, "output": { "HRK": 0.19, "UZS": 241.47, "RUB": 1.98, "BOB": 0.21, "TZS": 68.63, "GBP": 0.02, "GIP": 0.02, "GTQ": 0.23, . . . } }
Now, you mentioned "this isn't production quality code: There are no error checks or fallbacks. If an expected argument is missing e.g. the HTTP API crashes". These are the measures I came up with:
pyapi/__main__.py:
(simple check if arguments are present)
def get():
if 'amount' in request.args and 'input_currency' in request.args:
if 'output_currency' in request.args:
return service.create_json(service.sign_to_abbreviation(request.args['input_currency']),
service.sign_to_abbreviation(request.args['output_currency']),
request.args['amount'])
else:
return service.create_json(service.sign_to_abbreviation(request.args['input_currency']),
"None",
request.args['amount'])
return "Missing arguments"
service.py, contact_api:
(exception if external site is unreachable (+logging of a time needed for a request))
# external converter service
def contact_api(inp, out):
logging.info(" FUNC: contact_api parameters: inp:%s out:%s", inp, out)
api_url_base = 'http://free.currencyconverterapi.com/api/v5/convert'
conversion = inp + "_" + out
payload = {"q": conversion, "compact": "ultra"}
try:
start_time = time.time()
response = requests.get(api_url_base, params=payload, timeout=1) # we have 1 sec to get a response
logging.info(" FUNC: contact_api request elapsed time: %s", time.time() - start_time)
except requests.exceptions.ConnectionError as e:
logging.error(" FUNC: contact_api CONNECTION ERROR: ", e)
return None
logging.info(" FUNC: contact_api Loading from CACHE: %s", response.from_cache)
if response.status_code == 200:
return json.loads(response.content.decode('utf-8'))
return None
I consider this to be simple and functional, could it be done better?
And to finish with, tests could get improved too but that is for another day. And for a reply to be complete, here is a link to the project: https://github.com/ciso112/kiwi-currencies
Probably more elegant ways to do this, but it works.
currency_in = 'USD'
currency_out = 'NOK'
import urllib2
req = urllib2.urlopen('http://finance.yahoo.com/d/quotes.csv?e=.csv&f=sl1d1t1&s='+currency_in+currency_out+'=X')
result = req.read()
# result = "USDNOK=X",5.9423,"5/3/2010","12:39pm"
Then you can split() the result for the modifier.
# Install google-currency package
# pip install google-currency
>>> from google_currency import convert
>>> convert('usd', 'bdt', 1)
Output:
{"from": "USD", "to": "BDT", "amount": "85.30", "converted": true}
You might not have realised it yet, but all your unimplemented methods will very much look like real_to_dollar:
- ask the user how much of the original currency he wants converted;
- apply a specific conversion rate;
- tell the user the result of the conversion in the desired currency.
So that should be a single function, parametrized by the elements that differ between various calls: name of the origin currency, name of the converted currency, exchange rate.
def convert_currency(from, to, rate):
user_value = raw_input("How many {}? ".format(from))
amount = float(user_value)
conversion = amount * rate
print " {} {} is equal to {} {}.".format(user_value, from, conversion, to)
Also note the use of format to simplify building a string with numbers.
You would then call it convert_currency("reais", "dollars", 0.26) or convert_currency(" dollars", "reias", 3.85).
However, having to use a bunch of ifs to know what parameters to use (like you would have used a bunch of ifs to know which function to call) is not that great and can be a burden to maintain and expand. You could use a second dictionary to map couples of currencies to exchange rates:
RATES = {
("BRL", "USD"): 0.26,
("USD", "BRL"): 3.85,
# Expand as necessary
}
And just have to get the rate using RATES[(user_choice1, user_choice2)].
Exchange rates change every day, so your program would quickly become either obsolete or unmaintainable by having the conversion factor as a constant in the program. I suggest you to use an API (for example: http://fixer.io/).
You have a supported currencies dictionary, but you don't test if the user input is among the supported ones. This can be done with a while True: loop and then break if the user choice is ok.
I also believe that the user would be bummed for having to digit the currency abbreviation :p...
Taking those considerations in mind, here's an example code:
import requests
SUPPORTED_CURRENCIES = {
"EUR": "European euro",
"USD": "US dollar",
"GBP": "Pound sterling",
"BRL": "Brazilian real"
}
CURRENCY_CODES = {
1: "EUR",
2: "USD",
3: "GBP",
4: "BRL"
}
def get_exchange_rate(base_currency, target_currency):
if not (base_currency in SUPPORTED_CURRENCIES.keys()):
raise ValueError("base currency {} not supported".format(base_currency))
if not (target_currency in SUPPORTED_CURRENCIES.keys()):
raise ValueError("target currency {} not supported".format(target_currency))
if base_currency == target_currency:
return 1
api_uri = "https://api.fixer.io/latest?base={}&symbols={}".format(base_currency, target_currency)
api_response = requests.get(api_uri)
if api_response.status_code == 200:
return api_response.json()["rates"][target_currency]
if __name__ == '__main__':
print("Welcome to Currency Converter")
amount = float(input("Enter the amount you wish to convert: "))
print("Choose a base currency among our supported currencies:")
while True:
for code, currency in CURRENCY_CODES.items():
print("code {}: base {}".format(code, currency))
base_currency_code = int(input("Please digit the code: "))
if base_currency_code in CURRENCY_CODES.keys():
break
else:
print("Invalid code")
base_currency = CURRENCY_CODES[base_currency_code]
print("Choose a target currency among our supported currencies:")
while True:
for code, currency in CURRENCY_CODES.items():
print("code {}: target {}".format(code, currency))
target_currency_code = int(input("Please digit the code: "))
if target_currency_code in CURRENCY_CODES.keys():
break
else:
print("Invalid code")
target_currency = CURRENCY_CODES[target_currency_code]
exchange_rate = get_exchange_rate(base_currency, target_currency)
print("{} {} is {} {}".format(amount, base_currency, amount * exchange_rate, target_currency))
The code uses the requests library, which is awesome and I suggest you to check it out.
Never assume that your user doesn't make mistakes. I ran your program with no arguments ... error message, fine. I ran it with a single argument of 4 ... error message, fine. I ran it with two arguments, 4 and 5 ...:
Traceback (most recent call last):
File "test.py", line 17, in <module>
rate = checker(args.curency_for_convert, args.curency_converted_to)
File "test.py", line 9, in checker
rate = content["rates"][symbol.upper()]
KeyError: 'rates'
For debugging purposes, I put print content above that line and ran again:
{'error': 'Invalid base'}
If I run the program with a valid base, but an invalid symbol:
Traceback (most recent call last):
File "test.py", line 16, in <module>
rate = checker(args.curency_for_convert, args.curency_converted_to)
File "test.py", line 8, in checker
rate = content["rates"][symbol.upper()]
KeyError: '8'
You should catch these errors and display something that is a little more user-friendly.
I would recommend that you read PEP 8, the Python style guide. There are a couple things you aren't following:
Imports should usually be on separate lines, e.g.:
Yes:
import os import sysNo:
import sys, osIt's okay to say this though:
from subprocess import Popen, PIPE
Don't use spaces around the
=sign when used to indicate a keyword argument or a default parameter value.
Your whitespace is a little strange. I would have at least one blank line above and below the function definition. I would also put one after the if block and maybe even before it too. You have " =", " = ", and "=" used in just three lines. PEP 8 says that A Foolish Consistency is the Hobgoblin of Little Minds, but that doesn't mean never be consistent. It is only the foolish consitency which is a hobgoblin. In this case, you have nothing to lose.
Your naming is a little strange. n even shows up in the help messages. One-letter variable names are generally not recommended because they give very little idea of what the variable is for. curency_for_convert and curency_converted_to, besides mis-spelling "currency", are ungrammatical. You could say "for conversion" or "to convert", but "for convert"? I would suggest "convert_from" and "convert_to".
if len(sys.argv) > 3: parser.add_argument("n", type =int, help = "Amount of curency_for_convert", default=1)
Instead of manually parsing an argument, make it optional with nargs='?'
print rate if len(sys.argv) <= 3 else rate*args.n
Since our argument will always be present (either with the default of 1 or with a user-supplied number), we can now do print rate * args.n.
Full Program
#!/usr/bin/env python
import argparse
import ast
import requests
import sys
def checker(base, symbol):
url = "http://api.fixer.io/latest?base=%s" % base.upper()
r = requests.get(url)
content = ast.literal_eval(r.content)
rate = content["rates"][symbol.upper()]
return rate
parser = argparse.ArgumentParser(description=
"Currency converter. For instance: to convert 100 USD to RUB, it would be "
"100 USD RUB. If you need only the rate: USD RUB"
)
parser.add_argument("amount", type=int, help="Amount of currency to convert", default=1, nargs='?')
parser.add_argument('convert_from', type=str, help="Currency to convert")
parser.add_argument('convert_to', type=str, help = "Currency of converted")
args = parser.parse_args()
try:
rate = checker(args.convert_from, args.convert_to)
except KeyError:
print "Invalid argument(s)"
else:
print rate*args.amount
Style
Give your program some love. As it stand it is just a big pile of instructions. Add a bit of vertical spacing to separate logical sections, limit your line length so we can get most things at a glance.
You should also read and follow PEP8, the official Python's style guide:
- each import statement on its own line;
- consistent use of spaces around the
=sign; - meaningful variable names (
ris not).
The url variable isn't really one. You should extract it out of the function and give it a proper capitalized name as the constant it is. Also you should use str.format instead of % as it is the new standard.
Top-level code
Even if it is not that important for such "short" code, it is always a good practice to wrap your top-level code under an if __name__ == '__main__' statement. At the very least, it lets you import your code into an interactive session to test your function without triggering all its logic.
Requests
The responses from api.fixer.io are json strings, not Python dictionaries. You can convert back and forth between the two using the json module. However, requests provide a builtin hook for that so you don't even have to import the module:
content = r.json()
Argparse
You can totally get the help usage you’re looking for if you provide more than two parameters to your program. For instance:
$ python currency.py a b --help
However it is less than ideal. Argparse provide a way to have optional required arguments (sounds weird anyway):
- set the
nargsparameter to'?'to indicate it is optional; - set a
defaultvalue.
Since there is only one optional parameter, argparse will figure out things pretty well base on the length of sys.argv. Now how do you figure it out? Easily enough, if default=1 then you perform rate * args.n blindlessly.
Oh, and type=str is useless as it is the default.
Proposed improvements
#!/usr/bin/env python
import requests
import argparse
URL = "http://api.fixer.io/latest?base={}"
def get_rate(base, symbol):
url = URL.format(base.upper())
content = requests.get(url).json()
return content['rates'][symbol.upper()]
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description="Currency converter. For instance : 'Convert 100"
" USD to RUB' Will be: 100 USD RUB. If You need only rate: "
"USD RUB")
parser.add_argument("n", type=int, nargs="?", default=1,
help="Amount of currency_for_convert")
parser.add_argument("base_currency", help="Currency for convert")
parser.add_argument("converted_currency", help="Currency that converted to")
args = parser.parse_args()
converted = args.n * get_rate(args.base_currency, args.converted_currency)
print converted
You may also be interested into handling networking issues, conversions issues or bad currencies.