I'm trying to write a python script using boto3 in order to get hourly prices of an instance, given the instance ID. I should remark that I'm not speaking about costs that you can get from cost explorer, I'm speaking about nominal hourly price, for example for an 'ec2' instance.
I've already found some examples using "boto3.client('pricing',...)" and a bunch of parameters and filters as in:
https://www.saisci.com/aws/how-to-get-the-on-demand-price-of-ec2-instances-using-boto3-and-python/
which also requires region code to region name conversion.
I would like not to have to specify every instance detail and parameter for that query.
Can anybody help me to find a way to get that info just having the ec2 instance ID?
Thanks in advance.
You have to pass all that information. If you want to write a script that takes an instance ID and returns the hourly price, then you would first need to use the instance ID to lookup the instance details, and then pass those details to the pricing query.
You have to specify most of the information but not all of it.
For example, region_name is optional if you:
Have configured AWS CLI on the machine on which your Python script is running (ie. ~/.aws/config is present and the region is configured).
OR
You are running the Python script on an AWS resource that has a role attached to it with a policy that allows you to retrieve the spot pricing information.
For example, I am able to run this script that retrieves my current spot instances and gets their current hourly cost, and calculates a bid price for me based on the spot price history for that particular instance type without specifying the region anywhere:
#!/usr/bin/env python3
import boto3
import json
from datetime import datetime
from datetime import timedelta
from collections import namedtuple
def get_current_pricing():
pricing = []
ec2_client = boto3.client('ec2')
ec2_resource = boto3.resource('ec2')
response = ec2_client.describe_spot_instance_requests()
spot_instance_requests = response['SpotInstanceRequests']
for instance_request in spot_instance_requests:
if instance_request['State'] == 'active':
instance = ec2_resource.Instance(instance_request['InstanceId'])
for tag in instance.tags:
if tag['Key'] == 'Name':
application = tag['Value']
break
price = {
'application': application,
'instance_type': instance_request['LaunchSpecification']['InstanceType'],
'current_price': float(instance_request['SpotPrice']),
'bid_price': get_bid_price(instance_request['LaunchSpecification']['InstanceType'])
}
pricing.append(price)
return pricing
def get_bid_price(instancetype):
instance_types = [instancetype]
start = datetime.now() - timedelta(days=1)
ec2 = boto3.client('ec2')
price_dict = ec2.describe_spot_price_history(
StartTime=start,
InstanceTypes=instance_types,
ProductDescriptions=['Linux/UNIX']
)
if len(price_dict.get('SpotPriceHistory')) > 0:
PriceHistory = namedtuple('PriceHistory', 'price timestamp')
for item in price_dict.get('SpotPriceHistory'):
price_list = [PriceHistory(round(float(item.get('SpotPrice')), 5), item.get('Timestamp'))]
price_list.sort(key=lambda tup: tup.timestamp, reverse=True)
bid_price = round(float(price_list[0][0]), 5)
leeway = round(float(bid_price / 100 * 10), 5)
bid_price = round(float(bid_price + leeway), 5)
return bid_price
else:
raise ValueError(f'Invalid instance type: {instancetype} provided. '
'Please provide correct instance type.')
if __name__ == '__main__':
current_pricing = get_current_pricing()
print(json.dumps(current_pricing, indent=4, default=str))
So I've queried the location_view resource using the code shown below. I've tried something similar using the geographic_view, but it also leads to the same problem. This problem is that metrics like average_cost or average_cpc are ~2800000. Now this obviously can't be correct. I was wondering if anyone has had a similar problem or if maybe, the actual value is multiplied by a factor of 1M or something
from google.ads.googleads.client import GoogleAdsClient
credentials = {
"developer_token": "xx",
"refresh_token": "xx",
"client_id":"xx",
"client_secret": "xx"}
client = GoogleAdsClient.load_from_dict(credentials)
query = """
SELECT location_view.resource_name, segments.date, metrics.average_cost, metrics.average_cpc, metrics.clicks, metrics.conversions, metrics.cost_micros, metrics.cost_per_conversion, metrics.impressions, metrics.interactions, metrics.interaction_rate, campaign.id, campaign.name, campaign.end_date, campaign.start_date FROM location_view WHERE segments.date DURING LAST_30_DAYS"""
ga_service = client.get_service("GoogleAdsService")
search_request = client.get_type("SearchGoogleAdsStreamRequest")
search_request.customer_id = "xx"
search_request.query = query
test = []
response = ga_service.search_stream(search_request)
for batch in response:
for row in batch.results:
test.append(row)
Yes, this is expected behavior. The cost metrics are micros, so need to be divided by 1,000,000. There's more detail on a similar thread on the API forum: https://groups.google.com/g/adwords-api/c/K4ux3hmlego?pli=1
I am coming today following an issue that doesn't make sense to me using python and mongodb. I am a Go/C# developer so maybe I am missing something but I have the following case:
from datetime import datetime
from bson import ObjectId
class DailyActivity:
user_ids = []
date = None
def __init__(self, user_ids : [ObjectId] = [], date : datetime = None):
self.user_ids = user_ids
self.date = date
class ActivitiesThroughDays:
daily_activies = []
def add_daily_activity(self, daily_activity : DailyActivity = None):
daily_activies.append(daily_activity)
I then have these 2 classes but also another file containing some helper to use mongodb:
from pymongo import MongoClient
def get_client():
return MongoClient('localhost', 27017)
def get_database(database_name: str = None):
if database_name is None:
raise AttributeError("database name is None.")
return get_client().get_database(database_name)
def get_X_database():
return get_database("X")
And here we get to the issue.. I am now building a simple ActivitiesThroughDays object which has only one DailyActivity containing X user ids (as ObjectId array/list).
However, when I try to insert_one, I get the following:
TypeError: document must be an instance of dict, bson.son.SON, bson.raw_bson.RawBSONDocument, or a type that inherits from collections.MutableMapping
this is the piece of code that raise the exception:
def insert_activities_though_days(activities_through_days: ActivitiesThroughDays = None):
if activities_through_days is None:
raise AttributeError("activities_through_days is None.")
col = get_EM_column("activities_through_days")
col.insert_one(activities_through_days)
Based on the above issue, I then tried to convert my ActivitiesThroughDays into dic/json:
col.insert_one(activities_through_days.__dict__)
bson.errors.InvalidDocument: cannot encode object: models. DailyActivity. DailyActivity object at 0x10eea0320, of type: class 'models. DailyActivity. DailyActivity'
col.insert_one(json.dumps(activities_through_days))
TypeError: Object of type ActivitiesThroughDays is not JSON serializable
So based on this, I began to search for different solutions over google and found out solutions such as :
def to_dict(obj):
if not hasattr(obj,"__dict__"):
return obj
result = {}
for key, val in obj.__dict__.items():
if key.startswith("_"):
continue
element = []
if isinstance(val, list):
for item in val:
element.append(to_dict(item))
else:
element = to_dict(val)
result[key] = element
return result
But I got :
bson.errors.InvalidDocument: cannot encode object: property object at 0x10229aa98, of type: class 'property'
For each step I move forward, another issue comes up... To me, all of this doesn't make sense at all because.. there should be a generic serializer/deserializer somewhere that would, from 1 line, convert any nested objects/arrays to be inserted in mongodb..
Also, from one of the solution I tried, I found out that ObjectId were ignored while mapping to json/dict (I don't remember which one)
I am not at all a Python developer so please, feel free to give any tips :)
Thanks
pymongo's interface expects dict and .__dict__ is a very low level attribute.
I'm afraid you'll spend a lot of energy if you try to build an ORM/ODM for mongodb from scratch.
There are existing ORM/ODM libraries that exist for mongodb in python (mongoengine, pymodm which are quite similar) and that could help you to get something working quickly.
Here are a few lines that shows how the models would look with mongoengine and how to save them:
import datetime as dt
from mongoengine import *
connect(host='mongodb://localhost:27017/testdb')
class User(Document):
email = EmailField(required=True)
class DailyActivity(Document):
users = ListField(ReferenceField(User))
date = DateTimeField(default=dt.datetime.utcnow)
user = User(email='test#garbage.com').save()
user2 = User(email='test2#garbage.com').save()
activity = DailyActivity(users=[user, user2]).save()
I hope this helps
Currently I am using Django 1.10.
For example we have the models below:
class ModelOne(models.Model):
description = models.CharField(max_length = 50)
class ModelTwo(models.Model):
description = models.CharField(max_length = 50)
m1 = models.ForeignKey( ModelOne)
class ModelThree(models.Model):
description = models.CharField(max_length = 50)
m2 = models.ForeignKey(ModelTwo)
Everytime a request is made then a JSON response is return using the code below:
from app.models import *
from django.http import HttpRequest,JsonResponse
def ViewThatReceiveRequest(request):
qs = list(ModelThree.objects.filter(description__icontains='beauty').select_related('m2__m1')
data = []
for key in qs:
temp_obj = {}
# if necessary I will convert the values before equating
temp_obj['desc3'] = key.description
temp_obj['desc2'] = key.m2.description
temp_obj['desc1'] = key.m2.m1.description
data.append(temp_obj)
return JsonResponse( {'message' : 'success', 'qs' : data }, safe = False)
Note:
- I am using Django as my back-end and ReactJs for my front-end so I only need JSON Response
My question,
- Do I need to do these for every views that receive request?
- Do we have other way to solve these simpler?
Since you only need description field from all 3 models, you can also use values method or maybe values_list of the queryset and get data.
So this new query would be
ModelThree.objects.filter(description__icontains='beauty').select_related('m2__m1).values_list('description','m2__description','m2__m1__description')
Next if you want to run some changes to your data, you can run a map method in case you want to make similar changes to all the data.
Next you can make the appropriate json and send to to client side.
One feature I have been struggling to implement in flask-admin is when the user edits a form, to constrain the value of Field 2 once Field 1 has been set.
Let me give a simplified example in words (the actual use case is more convoluted). Then I will show a full gist that implements that example, minus the "constrain" feature.
Let's say we have a database that tracks some software "recipes" to output reports in various formats. The recipe table of our sample database has two recipes: "Serious Report", "ASCII Art".
To implement each recipe, we choose one among several methods. The method table of our database has two methods: "tabulate_results", "pretty_print".
Each method has parameters. The methodarg table has two parameter names for "tabulate_results" ("rows", "display_total") and two parameters for "pretty_print" ("embellishment_character", "lines_to_jump").
Now for each of the recipes ("Serious Report", "ASCII Art") we need to provide the value of the arguments of their respective methods ("tabulate_results", "pretty_print").
For each record, the recipearg table lets us select a recipe (that's Field 1, for instance "Serious Report") and an argument name (that's Field 2). The problem is that all possible argument names are shown, whereas they need to be constrained based on the value of Field 1.
What filtering / constraining mechanism can we implement such that once we select "Serious Report", we know we will be using the "tabulate_results" method, so that only the "rows" and "display_total" arguments are available?
I'm thinking some AJAX wizardry that checks Field 1 and sets a query for Field 2 values, but have no idea how to proceed.
You can see this by playing with the gist: click on the Recipe Arg tab. In the first row ("Serious Report"), if you try to edit the "Methodarg" value by clicking on it, all four argument names are available, instead of just two.
# full gist: please run this
from flask import Flask
from flask_admin import Admin
from flask_admin.contrib import sqla
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
# Create application
app = Flask(__name__)
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///a_sample_database.sqlite'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
# Create admin app
admin = Admin(app, name="Constrain Values", template_mode='bootstrap3')
# Flask views
#app.route('/')
def index():
return 'Click me to get to Admin!'
class Method(db.Model):
__tablename__ = 'method'
mid = Column(Integer, primary_key=True)
method = Column(String(20), nullable=False, unique=True)
methodarg = relationship('MethodArg', backref='method')
recipe = relationship('Recipe', backref='method')
def __str__(self):
return self.method
class MethodArg(db.Model):
__tablename__ = 'methodarg'
maid = Column(Integer, primary_key=True)
mid = Column(ForeignKey('method.mid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
methodarg = Column(String(20), nullable=False, unique=True)
recipearg = relationship('RecipeArg', backref='methodarg')
inline_models = (Method,)
def __str__(self):
return self.methodarg
class Recipe(db.Model):
__tablename__ = 'recipe'
rid = Column(Integer, primary_key=True)
mid = Column(ForeignKey('method.mid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
recipe = Column(String(20), nullable=False, index=True)
recipearg = relationship('RecipeArg', backref='recipe')
inline_models = (Method,)
def __str__(self):
return self.recipe
class RecipeArg(db.Model):
__tablename__ = 'recipearg'
raid = Column(Integer, primary_key=True)
rid = Column(ForeignKey('recipe.rid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
maid = Column(ForeignKey('methodarg.maid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
strvalue = Column(String(80), nullable=False)
inline_models = (Recipe, MethodArg)
def __str__(self):
return self.strvalue
class MethodArgAdmin(sqla.ModelView):
column_list = ('method', 'methodarg')
column_editable_list = column_list
class RecipeAdmin(sqla.ModelView):
column_list = ('recipe', 'method')
column_editable_list = column_list
class RecipeArgAdmin(sqla.ModelView):
column_list = ('recipe', 'methodarg', 'strvalue')
column_editable_list = column_list
admin.add_view(RecipeArgAdmin(RecipeArg, db.session))
# More submenu
admin.add_view(sqla.ModelView(Method, db.session, category='See Other Tables'))
admin.add_view(MethodArgAdmin(MethodArg, db.session, category='See Other Tables'))
admin.add_view(RecipeAdmin(Recipe, db.session, category='See Other Tables'))
if __name__ == '__main__':
db.drop_all()
db.create_all()
db.session.add(Method(mid=1, method='tabulate_results'))
db.session.add(Method(mid=2, method='pretty_print'))
db.session.commit()
db.session.add(MethodArg(maid=1, mid=1, methodarg='rows'))
db.session.add(MethodArg(maid=2, mid=1, methodarg='display_total'))
db.session.add(MethodArg(maid=3, mid=2, methodarg='embellishment_character'))
db.session.add(MethodArg(maid=4, mid=2, methodarg='lines_to_jump'))
db.session.add(Recipe(rid=1, mid=1, recipe='Serious Report'))
db.session.add(Recipe(rid=2, mid=2, recipe='ASCII Art'))
db.session.commit()
db.session.add(RecipeArg(raid=1, rid=1, maid=2, strvalue='true' ))
db.session.add(RecipeArg(raid=2, rid=1, maid=1, strvalue='12' ))
db.session.add(RecipeArg(raid=3, rid=2, maid=4, strvalue='3' ))
db.session.commit()
# Start app
app.run(debug=True)
I see two ways of tacking this problem:
1- When Flask-Admin generate the form, add data attributes with the mid of each methodArg on each option tag in the methodArg select. Then have some JS code filter the option tags based on the recipe selected.
EDIT
Here is a tentative try at putting a data-mid attribute on each option:
def monkeypatched_call(self, field, **kwargs):
kwargs.setdefault('id', field.id)
if self.multiple:
kwargs['multiple'] = True
html = ['<select %s>' % html_params(name=field.name, **kwargs)]
for (val, label, selected), (_, methodarg) in zip(field.iter_choices(), field._get_object_list()):
html.append(self.render_option(val, label, selected, **{'data-mid': methodarg.mid}))
html.append('</select>')
return HTMLString(''.join(html))
Select.__call__ = monkeypatched_call
The blocker is in the fact that those render calls are triggered from the jinja templates, so you are pretty much stuck updating a widget (Select being the most low-level one in WTForms, and is used as a base for Flask-Admin's Select2Field).
After getting those data-mid on each of your options, you can proceed with just binding an change on your recipe's select and display the methodarg's option that have a matching data-mid. Considering Flask-Admin uses select2, you might have to do some JS tweaking (easiest ugly solution would be to clean up the widget and re-create it for each change event triggered)
Overall, I find this one less robust than the second solution. I kept the monkeypatch to make it clear this should not be used in production imho. (the second solution is slightly less intrusive)
2- Use the supported ajax-completion in Flask-Admin to hack your way into getting the options that you want based on the selected recipe:
First, create a custom AjaxModelLoader that will be responsible for executing the right selection query to the DB:
class MethodArgAjaxModelLoader(sqla.ajax.QueryAjaxModelLoader):
def get_list(self, term, offset=0, limit=10):
query = self.session.query(self.model).filter_by(mid=term)
return query.offset(offset).limit(limit).all()
class RecipeArgAdmin(sqla.ModelView):
column_list = ('recipe', 'methodarg', 'strvalue')
form_ajax_refs = {
'methodarg': MethodArgAjaxModelLoader('methodarg', db.session, MethodArg, fields=['methodarg'])
}
column_editable_list = column_list
Then, update Flask-Admin's form.js to get the browser to send you the recipe information instead of the methodArg name that needs to be autocompleted. (or you could send both in query and do some arg parsing in your AjaxLoader since Flask-Admin does no parsing whatsoever on query, expecting it to be a string I suppose [0]. That way, you would keep the auto-completion)
data: function(term, page) {
return {
query: $('#recipe').val(),
offset: (page - 1) * 10,
limit: 10
};
},
This snippet is taken from Flask-Admin's form.js [1]
Obviously, this needs some tweaking and parametrising (because doing such a hacky solution would block you from using other ajax-populated select in the rest of your app admin + the update on form.js directly like that would make upgrading Flask-Admin extremely cumbersome)
Overall, I am unsatisfied with both solutions and this showcase that whenever you want to go out of the tracks of a framework / tool, you can end up in complex dead ends. This might be an interesting feature request / project for someone willing to contribute a real solution upstream to Flask-Admin though.
There is another easy solution that I made and it works
1- Create your first select option normally with data loaded on it and add a hook to it which will add js event listener when it selects change like this.
from wtforms import SelectField
form_extra_fields = {
'streetname': SelectField(
'streetname',
coerce=str,
choices=([street.streetname for street in StreetsMetadata.query.all()]),
render_kw={'onchange': "myFunction()"}
)
}
**2- Add a JavaScript URL file to the view you want to use this function in, for example.
def render(self, template, **kwargs):
#using extra js in render method allow use url_for that itself requires an app context
self.extra_js = [url_for("static", filename="admin/js/users.js")]
response = render_miror(self, template,**kwargs)
return response
3- Create a role-protected endpoint that you used for this view that will accept a GET request from JS based on the first value specified for the entry, for example this route returns a list of house numbers by querying the street name that came from the first entry
#super_admin_permission.require(http_exception=403)
#adminapp.route('/get_houses_numbers')
def gethouses():
request_data = request.args
if request_data and 'street' in request_data:
street = StreetsMetadata.query.filter(StreetsMetadata.streetname == request_data['street']).one_or_none()
street_houses = lambda:giveMeAllHousesList(street.excluded, street.min, street.max)
if street_houses:
return jsonify({'code': 200, 'houses': street_houses()})
else:
return jsonify({'code': 404, 'houses': []})
else:
return jsonify({'code': 400, 'street': []})
now python part completed time for JavaScript
4- We have to define three functions, the first of which will be called when the form build page is loaded and which do two things first,
A dummy select entry will be created using JS and append that entry to the same string input container
Make string entry read-only to improve user experience
Second, it will send a GET request to the specified route to get a list of house numbers using the specified street input value
Then get the result and create the option elements and append these options to the dummy selection, you can also select the first option while appending the options.
5- The second function "myFunction" is the hook defined in Python in this part
render_kw={'onchange': "myFunction()"}
This function will do nothing new, it will only send a GET request when the first specified input value is changed, send a GET request to get a list of new house numbers based on the given street name input value by doing a query on the database, then dump the inner HTML of the dummy selection entry , then create and append new options to it.
6- The last function is the callback function which listens for the change on the dummy select entry created with JS when the user chooses the house number which will be reflected in the main string entry, finally you can click save and you will see it working
Note that this whole idea I created is not as good as the built in flask admin, but if you are looking for the end goal and without any problems you can use it
My JS code
/*
This Function when run when a form included it will create JS select input with the
default loaded streetname and add house number on that select this select will used
to guide creator of the house number or to select the house number
*/
async function onFlaskFormLoad(){
const streetSelect = document.querySelector("#streetname");
const checkIfForm = document.querySelector("form.admin-form");
if (checkIfForm){
let checkSelect = document.querySelector("#realSelect");
if (!checkSelect){
const mySelectBox = document.createElement("select");
const houseString = document.querySelector("#housenumber");
const houseStringCont = houseString.parentElement;
mySelectBox.classList.add("form-control")
mySelectBox.id = "realSelect";
houseStringCont.appendChild(mySelectBox);
mySelectBox.addEventListener("change", customFlaskAdminUnpredefinedSelect);
houseString.setAttribute("readonly", "readonly");
const res = await fetch(`/get_houses_numbers?street=${streetSelect.value}`);
const data = await res.json();
console.log(data);
if (data.code == 200 && mySelectBox){
data.houses.forEach( (houseOption, index)=>{
if (index == 0){
houseString.value = houseOption;
}
let newHouse = document.createElement("option");
newHouse.setAttribute("value", houseOption);
newHouse.innerText = houseOption;
mySelectBox.appendChild(newHouse);
});
}
}
}
}
onFlaskFormLoad();
/*
this function will called to change the string input value to my custom js select
value and then use that string to house number which required by flask-admin
*/
function customFlaskAdminUnpredefinedSelect(){
const theSelect = document.querySelector("#realSelect");
const houseString = document.querySelector("#housenumber");
houseString.value = theSelect.value;
return true;
}
/*
flask admin hook that will listen on street input change and then it will send
get request to secured endpoint with role superadmin required and get the housenumbers
using the streetname selected and then create options and add to my select input
*/
async function myFunction(){
const streetSelect = document.querySelector("#streetname");
const houseString = document.querySelector("#housenumber");
const houseStringCont = houseString.parentElement;
const theSelect = document.querySelector("#realSelect");
const res = await fetch(`/get_houses_numbers?street=${streetSelect.value}`);
const data = await res.json();
console.log(data);
if (data.code == 200 && theSelect){
theSelect.innerHTML = "";
data.houses.forEach( (houseOption, index)=>{
if (index == 0){
houseString.value = houseOption;
}
let newHouse = document.createElement("option");
newHouse.setAttribute("value", houseOption);
newHouse.innerText = houseOption;
theSelect.appendChild(newHouse);
});
}
}
Now if I change the street name of the first specified input I will get a new list containing the numbers based on the first input value, note if you have a way to create a python field that accepts the non-predefined options then there is no need to create dummy input you can create and append the new options Directly to second select input
final result