How does allocateIds() work in Cloud Datastore Mode? - python

In the new Datastore Mode documentation, there is mention of allocateIds() method. However, beyond a single paragraph, there isn't an example code that illustrates how this method is used.
I am trying to allocate an ID each time I create a new entity so that I can save the ID as a property of the entity itself.
I assume that in pseudocode, it works like this:
user_id = allocateIds(number_id_ids=1)
user_key = datastore_client.key(kind='User', user_id)
user = datastore.Entity(key=user_key)
user.update({ 'user_id': user_id }) # Allows a get_user_by_id() query
datastore_client.put(user)
How exactly does allocateIds() work in practice?

When you call the allocateIds() function it invokes a new instance of class Key(object) when the consturctor of "Key" is called it takes all of the arguments you provided allocateIds and recombines them through a _combine_args method. That is what produces your key.
(and if you want to see the code yourself)
source: https://googleapis.dev/python/datastore/latest/_modules/google/cloud/datastore/key.html#Key

Yes, allocateIds() should work for the case where you want to get an ID from Datastore mode and use it as both an ID and property value:
from google.cloud import datastore
client = datastore.Client()
# Allocate a single ID in kind User
# Returns list of keys
keys = client.allocate_ids(client.key('User'), 1)
# Get key from list
key = keys[0]
print(key.id)
# Create a User entity using our key
user = datastore.Entity(key)
# Add ID as a field
user.update({
'user_id': key.id
})
# Commit to database
client.put(user)
# Query based on full key
query = client.query(kind='User')
query.key_filter(user.key, '=')
results = list(query.fetch())
print(results)
For most other cases where you just want a single auto-ID, you can skip allocate_ids:
# Create a User entity
# Use an incomplete key so Datastore assigns an ID
user = datastore.Entity(client.key('User'))
# Add some data
user.update({
'foo': 'bar'
})
# Datastore allocates an ID when you call client.put
client.put(user)
# user.key now contains an ID
user_id = user.key.id
print(user_id)
# Query with the ID and key
query = client.query(kind='User')
query.key_filter(user.key, '=')
results = list(query.fetch())
print(results)

Related

Getting back property of object in a query list SQLAlchemy

I have a join table called UserServices. Which has a FK of service_id and makes a back ref to a service. Below I get all the userServices in which the id in the route param matches the user_id (another FK)
I am then trying to access all the service properties on the all_user_services list.
My current code only returns one dict instead of a list of dicts. What am i doing wrong?
#bp.route('/user/<id>/services', methods=['GET'])
def get_services_from_user(id):
all_user_services = db_session.query(UserService).filter(UserService.user_id == id).all()
for service in all_user_services:
result = service_schema.dump(service.service)
return jsonify(result)
You just return on first for iteration. You need to create result list:
dumped = [service_schema.dump(s.service) for s in all_user_services]
return jsonify(dumped)

Updating a MongoDB document if field doesn't exist

Whenever I updated my insert_one with a new field to use, I had to always delete the old posts in the collection. I know there are manual methods of updating such fields using update_many but I know it's inefficient.
For example:
posts.insert_one({
"id": random.randint(1,10000)
"value1": "value1",
"value2": "value2"
})
I use the following code to check if the document exists or not. How would this work for a field?
if posts.find({'id': 12312}).count() > 0:
I know I can easily overwrite the previous data but I know people won't enjoy having their data wiped every other month.
Is there a way to add the field to a document in Python?
How would this work for a field?
You can use $exists to check whether a field exists in a doc.
In your case, you can combine this with find
find({ 'id':1, "fieldToCheck":{$exists:"true"}})
It will return the doc if it exists with id = 1, fieldToCheck is present in doc with id = 1
You can skip id=1, in that case, it will return all docs where fieldToCheck exists
Is there a way to add the field to a document in Python?
You could use update with new field, it will update if it is present else it will insert.
update({"_id":1}, {field:"x"})
If field is present, it will set to x else it will add with field:x
Beware of update options like multi, upsert
Yes you can you use update command in mongoDB shell to do that. check here
This is the command to use...
db.collection.update({},{$set : {"newfield":1}},false,true)
The above will work in the mongoDB shell. It will add newfield in all the documents, if it is not present.
If you want to use Python, use pymongo.
For python, following command should work
db.collection.update({},{"$set" : {"newfield":1}},False, True)
Thanks to john's answer I have made an entire solution that automatically updates documents without the need to run a task meaning you don't update inactive documents.
import datetime
import pymongo
database = pymongo.MongoClient("mongodb://localhost:27017") # Mongodb connection
db = database.maindb # Database
posts = db.items # Collection within a database
# A schema equivalent function that returns the object
def user_details(name, dob):
return {
"username": name, # a username/id
"dob": dob, # some data
"level": 0, # some other data
"latest_update": datetime.datetime.fromtimestamp(1615640176)
# Must be kept to ensure you aren't doing it that often
}
# The first schema changed for example after adding a new feature
def user_details2(name, dob, cake):
return {
"username": name, # a username/id
"dob": dob, # Some data
"level": 0, # Some other data
"cake": cake, # Some new data that isn't in the document
"latest_update": datetime.datetime.utcnow() # Must be kept to ensure you aren't doing it that often
}
def check_if_update(find, main_document,
collection): # parameters: What you find a document with, the schema dictionary, then the mongodb collection
if collection.count_documents(find) > 0: # How many documents match, only proceed if it exists
fields = {} # Init a dictionary
for x in collection.find(find): # You only want one for this to work
fields = x
if "latest_update" in fields: # Just in case it doesn't exist yet
last_time = fields["latest_update"] # Get the time that it was last updated
time_diff = datetime.datetime.utcnow() - last_time # Get the time difference between the utc time now and the time it was last updated
if time_diff.total_seconds() < 3600: # If the total seconds of the difference is smaller than an hour
print("return")
return
db_schema = main_document # Better naming
db_schema["_id"] = 0 # Adds the _id schema_key into the dictionary
if db_schema.keys() != fields:
print("in")
for schema_key, schema_value in db_schema.items():
if schema_key not in fields.keys(): # Main key for example if cake is added and doesn't exist in db fetched fields
collection.update_one(find, {"$set": {schema_key: schema_value}})
else: # Everything exists and you want to check for if a dictionary within that dictionary is changed
try:
sub_dict = dict(schema_value) # Make the value of it a dictionary
# It exists in the schema dictionary but not in the db fetched document
for key2, value2 in sub_dict.items():
if key2 not in fields[schema_key].keys():
new_value = schema_value
new_value[
key2] = value2 # Adding the key and value from the schema dictionary that was added
collection.update_one(find,
{"$set": {schema_key: new_value}})
# It exists in the db fetched document but not in the schema dictionary
for key2, value2 in fields[schema_key].items():
if key2 not in sub_dict.keys():
new_dict = {} # Get all values, filter then so that only the schema existent ones are passed back
for item in sub_dict:
if item != key2:
new_dict[item] = sub_dict.get(item)
collection.update_one(find, {"$set": {schema_key: new_dict}})
except: # Wasn't a dict
pass
# You removed a value from the schema dictionary and want to update it in the db
for key2, value2 in fields.items():
if key2 not in db_schema:
collection.update_one(find, {"$unset": {key2: 1}})
else:
collection.insert_one(main_document) # Insert it because it doesn't exist yet
print("start")
print(posts.find_one({"username": "john"}))
check_if_update({"username": "john"}, user_details("john", "13/03/2021"), posts)
print("inserted")
print(posts.find_one({"username": "john"}))
check_if_update({"username": "john"}, user_details2("john", "13/03/2021", "Lemon drizzle"), posts)
print("Results:")
print(posts.find_one({"username": "john"}))
It is available as a gist

flask-admin form: Constrain Value of Field 2 depending on Value of Field 1

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

How to compute a databasefield with the field-id

Model:
db.define_table('orders',
Field('customer_id', db.customer)
Field('order_id', 'string')
)
I want to get a special order_id like XY-150012 where XY is part of the customer name, 15 is the year and 12 the id the actual record-id of orders. I tried in the model:
db.orders.order_id.compute = lambda r: "%s-%s00%s" % (db.customer(r['customer_id']).short, str(request.now.year)[2:], r['id'])
The id is never recognized, the computation ends up as None. If I remove r['id'] from the compute-line it works.
EDIT:
After adding an extra field field('running_number', 'integer') to the model I can access this fields content.
Is there a easy way to set this fields default=db.orders.id?
SOLUTION:
With Anthony´s Input, and reading about recursive selects I came up with this solution:
db.define_table('orders',
Field('customer_id', db.customer),
Field('order_id', 'string', default = None))
def get_order_id(id, short):
y = str(request.now.year)[2:]
return '%s-%s00%s' % (short, y, id)
def set_id_after_insert(fields,id):
fields.update(id=id)
def set_order_id_after_update(s,f):
row = s.select().first()
if row['order_id'] == None:
s.update_naive(order_id=get_order_id(row['id'], row['customer_id'].short)
else:
return
db.orders._after_insert.append(lambda f,id: set_id_after_insert(f,id))
db.orders._after_update.append(lambda s,f: set_order_id_after_update(s,f))
The problem is that the record ID is not known until after the record has been inserted in the database, as the id field is an auto-incrementing integer field whose value is generated by the database, not by web2py.
One option would be to define an _after_insert callback that updates the order_id field after the insert:
def order_after_insert(fields, id):
fields.update(id=id)
db(db.order.id == id).update(order_id=db.order.order_id.compute(fields))
db.order._after_insert.append(order_after_insert)
You might also want to create an _after_update callback, but in that case, be sure to use the update_naive argument in both callbacks when defining the Set (see above link for details).
Depending on how the order_id is used, another option might be a virtual field.

Google App Engine: defining custom id and querying

I want to define a custom string as an ID so I created the following Model:
class WikiPage(ndb.Model):
id = ndb.StringProperty(required=True, indexed=True)
content = ndb.TextProperty(required=True)
history = ndb.DateTimeProperty(repeated=True)
Based on this SO thread, I believe this is right.
Now I try to query by this id by:
entity = WikiPage.get_by_id(page) # page is an existing string id, passed in as an arg
This is based on the NDB API.
This however isn't returning anything -- entity is None.
It only works when I run the following query instead:
entity = WikiPage.query(WikiPage.id == page).get()
Am I defining my custom key incorrectly or misusing get_by_id() somehow?
Example:
class WikiPage(ndb.Model):
your_id = ndb.StringProperty(required=True)
content = ndb.TextProperty(required=True)
history = ndb.DateTimeProperty(repeated=True)
entity = WikiPage(id='hello', your_id='hello', content=...., history=.....)
entity.put()
entity = WikiPage.get_by_id('hello')
or
key = ndb.Key('WikiPage','hello')
entity = key.get()
entity = WikiPage.get_by_id(key.id())
and this still works:
entity = WikiPage.query(WikiPage.your_id == 'hello').get()

Categories

Resources