SQLAlchemy: how to extend hybrid attributes? - python

I'm working with a MSSQL database with no control over the DB setup nor the (read-only) data in it. One table is represented in SQLAlchemy like this:
class pdAnlage(pdBase):
__tablename__ = "Anlage"
typ = Column(CHAR(4), primary_key=True)
nummer = Column(CHAR(4), primary_key=True)
In accessing the database, I need a property "name" that is just a concatenation of "typ" and "nummer" with a dot between them. So I did this:
#hybrid_property
def name(self):
return self.typ + '.' + self.nummer
Looks simple and works as expected. There are two caveats though, one general and one special. The general one: The table is quite big, and i'd like to make queries against Anlage.name, like this:
db.query(Anlage).filter(Anlage.name.like('A%.B'))
db.query(Anlage).filter(Anlage.name == 'X.Y')
This works but it is inefficient as the SQL server first has to concatenate all "typ" and "nummer" columns of the (large) table before doing the test. So I've defined a classmethods like this one:
#classmethod
def name_like(self, pattern):
p = pattern.split('.', 2)
if len(p) == 1 or not p[1]:
return self.typ.like(p[0])
else:
return and_(self.typ.like(p[0]), self.nummer.like(p[1]))
This isn't elegant, but it does the job just fine. It would be nicer to overload "==" and "like()", is there a way to do that?
Now to the special case: Both name and typ columns can contain trailing spaces in the DB. But the name property must not have spaces, especially not before the dot. So I tried to rewrite the name hybrid property like this:
#hybrid_property
def name(self):
return self.typ.rstrip() + '.' + self.nummer.rstrip()
This doesn't work because SQLAlchemy doesn't know how to translate the rstrip() python method to the MSSQL RTRIM() function. How can I accomplish that?

You could implement a custom comparator that handles string operands in a special way (and others as necessary):
from sqlalchemy.ext.hybrid import Comparator
_sep = '.'
def _partition(s):
typ, sep, nummer = s.partition(_sep)
return typ, nummer
class NameComparator(Comparator):
def __init__(self, typ, nummer):
self.typ = typ
self.nummer = nummer
super().__init__(func.rtrim(typ) + _sep + func.rtrim(nummer))
def operate(self, op, other, **kwgs):
if isinstance(other, str):
typ, nummer = _partition(other)
expr = op(self.typ, typ, **kwgs)
if nummer:
expr = and_(expr, op(self.nummer, nummer, **kwgs))
return expr
else:
# Default to using the "slow" method of concatenating first that
# hides the columns from the index created for the primary key.
return op(self.__clause_element__(), other, **kwgs)
and use it with your hybrid attribute:
class pdAnlage(Base):
__tablename__ = "Anlage"
typ = Column(CHAR(4), primary_key=True)
nummer = Column(CHAR(4), primary_key=True)
#hybrid_property
def name(self):
return self.typ.rstrip() + _sep + self.nummer.rstrip()
#name.comparator
def name(cls):
return NameComparator(cls.typ, cls.nummer)

Related

sqlalchemy #hybrid_property expressions for folder and filename from filepath. Very easy to write in pure python

I have a database I'm using sqlalchemy with, which involves storing the locations of files.
I have something like:
class FileLocation(ORMBase):
id = Column('id', Integer, primary_key=True)
filepath = Column('filepath', String)
and I want to add hybrid expressions of the folder and filename corresponding to each filepath. This is pretty easy to do with regular python strings, of course, but I can't find a way to do this sort of string manipulation in sqlalchemy expressions.
from sqlalchemy import func
class FileLocation(ORMBase):
id = Column('id', Integer, primary_key=True)
filepath = Column('filepath', String)
#hybrid_property
def folder(self):
return os.path.dirname(self.filepath)
#folder.expression
def folder(cls):
# How to get folder for sql queries???
last_pathsep_index = # ???
return func.substr(cls.filepath, 0, last_pathsep_index)
#hybrid_property
def filename(self):
return os.path.basename(self.filepath)
#filename.expression
def filename(cls):
# How to get filename for sql queries???
last_pathsep_index = # ???
return func.substr(cls.filepath, last_pathsep_index+1, func.len(cls.filepath))
How would I go about writing the #filename.expression and #folder.expression attributes, which currently are shown with incomplete implementations?
Here is the solution I ended up with, which I found through #AnthonyCarapetis' suggested link.
This ended up being more just a straight SQL (SQLite) question than about sqlalchemy, but I'll leave the sqlalchemy code here in case it helps anyone else.
import os
import sqlalchemy as sa
from sqlalchemy import case, func
def getpathsep(osname=os.name):
"""use os.name to determine osname"""
return case([(osname == 'nt', '\\')], else_='/')
def dirname(filepath, osname=os.name):
"""use os.name to determine osname"""
pathsep = getpathsep(osname)
replaced = func.replace(filepath, pathsep, '')
filename = func.rtrim(filepath, replaced)
return func.substr(filepath, 0, func.length(filename))
def pathsplit(filepath, osname=os.name):
"""use os.name to determine osname"""
folder = dirname(filepath, osname)
l = func.length(folder) + 1 # add 1 for (back) slash char
basename = func.substr(filepath, l + 1) # First index is 1
return folder, basename
def basename(filepath, osname=os.name):
"""use os.name to determine osname"""
return pathsplit(filepath, osname)[1]
class FileLocation(ORMBase):
id = Column('id', Integer, primary_key=True)
osname = Column('osname', String)
filepath = Column('filepath', String)
#hybrid_property
def folder(self):
return os.path.dirname(self.filepath)
#folder.expression
def folder(cls):
return dirname(cls.filepath, cls.osname)
#hybrid_property
def filename(self):
return os.path.basename(self.filepath)
#filename.expression
def filename(cls):
return basename(cls.filepath, cls.osname)

peewee + MySQL, How to create a custom field type that wraps SQL-built ins?

I'd like to create a custom UUID field in peewee (over MySQL).
In python, I'm using the UUID as a hexified string, e.g.:
uuid = '110e8400-e29b-11d4-a716-446655440000'
But I want to store it in the database to a column of type BINARY(16) to save space.
MySQL has built-in HEX() and UNHEX()methods to convert back and forth between a string and binary.
So my question is how do I tell peewee to generate SQL that uses a built-in function? Here's an idea for the code I want to work:
class UUIDField(Field):
db_field='binary(16)'
def db_value(self, value):
if value is not None:
uuid = value.translate(None, '-') # remove dashes
# HERE: How do I let peewee know I want to generate
# a SQL string of the form "UNHEX(uuid)"?
def python_value(self, value):
if value is not None:
# HERE: How do I let peewee know I want to generate
# a SQL string of the form "HEX(value)"?
Note that I'm specifically asking how to get peewee to wrap or unwrap a value in custom SQL. I realize I could probably do the value conversion entirely in python, but I'm looking for the more general-purpose SQL-based solution.
EDIT: For future reference, here is how I made it work doing the conversions in python. It doesn't answer the question though, so any ideas are appreciated!
import binascii
from peewee import *
db = MySQLDatabase(
'database',
fields={'binary(16)': 'BINARY(16)'} # map the field type
)
# this does the uuid conversion in python
class UUIDField(Field):
db_field='binary(16)'
def db_value(self, value):
if value is None: return None
value = value.translate(None, '-')
value = binascii.unhexlify(value)
return value
def python_value(self, value):
if value is None: return None
value = '{}-{}-{}-{}-{}'.format(
binascii.hexlify(value[0:4]),
binascii.hexlify(value[4:6]),
binascii.hexlify(value[6:8]),
binascii.hexlify(value[8:10]),
binascii.hexlify(value[10:16])
)
return value
Using a SelectQuery you can invoke internal SQL functions like so:
from peewee import SelectQuery
# this does the uuid conversion in python
class UUIDField(Field):
db_field = 'binary(16)'
def db_value(self, value):
if value is None: return None
value = value.translate(None, '-')
query = SelectQuery(self.model_class, fn.UNHEX(value).alias('unhex'))
result = query.first()
value = result.unhex
return value
def python_value(self, value):
if value is None: return None
query = SelectQuery(self.model_class, fn.HEX(value).alias('hex'))
result = query.first()
value = '{}-{}-{}-{}-{}'.format(
result.hex[0:8],
result.hex[8:12],
result.hex[12:16],
result.hex[16:20],
result.hex[20:32]
)
return value

How to intercept a specific tuple lookup in python

I'm wondering how could one create a program to detect the following cases in the code, when comparing a variable to hardcoded values, instead of using enumeration, dynamically?
class AccountType:
BBAN = '000'
IBAN = '001'
UBAN = '002'
LBAN = '003'
I would like the code to report (drop a warning into the log) in the following case:
payee_account_type = self.get_payee_account_type(rc) # '001' for ex.
if payee_account_type in ('001', '002'): # Report on unsafe lookup
print 'okay, but not sure about the codes, man'
To encourage people to use the following approach:
payee_account_type = self.get_payee_account_type(rc)
if payee_account_type in (AccountType.IBAN, AccountType.UBAN):
print 'do this for sure'
Which is much safer.
It's not a problem to verify the == and != checks like below:
if payee_account_type == '001':
print 'codes again'
By wrapping payee_account_type into a class, with the following __eq__ implemented:
class Variant:
def __init__(self, value):
self._value = value
def get_value(self):
return self._value
class AccountType:
BBAN = Variant('000')
IBAN = Variant('001')
UBAN = Variant('002')
LBAN = Variant('003')
class AccountTypeWrapper(object):
def __init__(self, account_type):
self._account_type = account_type
def __eq__(self, other):
if isinstance(other, Variant):
# Safe usage
return self._account_type == other.get_value()
# The value is hardcoded
log.warning('Unsafe comparison. Use proper enumeration object')
return self._account_type == other
But what to do with tuple lookups?
I know, I could create a convention method wrapping the lookup, where the check can be done:
if IbanUtils.account_type_in(account_type, AccountType.IBAN, AccountType.UBAN):
pass
class IbanUtils(object):
def account_type_in(self, account_type, *types_to_check):
for type in types_to_check:
if not isinstance(type, Variant):
log.warning('Unsafe usage')
return account_type in types_to_check
But it's not an option for me, because I have a lot of legacy code I cannot touch, but still need to report on.

Need to print with spaces

I have to print name with spaces, can u help me please?
I got the code like this:
class Perfil:
def __init__(self,email,nome,cidade):
self.email=email
self.nome=nome
self.cidade=cidade
def __str__(self):
return "Perfil de "+self.nome+" ""("+self.email+")"" de "+self.cidade
def getCidade(self):
return self.cidade
def setCidade(self,novo):
self.cidade=novo
def getDominio(self):
t=self.email.rpartition("#")
return t[2]
def limpaNome(self):
new=""
if self.nome.isalpha()==True:
return self.nome
else:
for i in self.nome:
if i.isalpha()==True:
new +=i
return new
When i run the program:
>>> p=Perfil("lol#mail.pt","Ze Car231los", "Porto")
>>> p.limpaNome()
'ZeCarlos'
I need a print like 'Ze Carlos' (with space)
Basically i need to wrote a program using abstract data types (class Profile) to save information for each user. Each object got the following attributes:
email
name
city
The class should have the following methods to manipulate the objects above
Method
__init__(self, email, name, city) - constructor
__str__(self)
getCity(self) - return the value of atribute city
getCity(self.new) - return the atribute city with a new value
getDomain(self) - example: lol#mail.com sugestion: use the method partition (i have to return mail.com only)
cleanName(self) - change the atribute name, deleting characters WICH are not alphabetic or spaces sugestion: use method isalpha
If all you want to do is remove all occurrences of '0','1','2',...,'9' from the string, then you could use str.translate like this:
def limpaNome(self):
return self.nome.translate({ord(c):None for c in '0123456789'})
Note that there is no need for getters/setters like this in Python:
def getCidade(self):
return self.cidade
def setCidade(self,novo):
self.cidade=novo
Instead, just let the user access/set the attribute directly: self.cidade. If, at some point, you'd like to run a function whenever the attribute is accessed or assigned to, then you can make cidade a property without having to change the usage syntax.
You could even make getDominio and limpaNome properties too:
#property
def dominio(self):
t=self.email.rpartition("#")
return t[2]
#property
def limpaNome(self):
return self.nome.translate({ord(c):None for c in '0123456789'})
Notice you don't need paretheses when accessing or setting the property. The syntax looks the same as though lipaNome were a plain attribute:
>>> p=Perfil("lol#mail.pt","Ze Car231los", "Porto")
>>> p.limpaNome
Ze Carllos
>>> p.dominio
mail.pt
import string
# ... the rest of your code
# ...
def limpaNome(self):
whitelist = set(string.ascii_uppercase+string.ascii_lowercase+" ")
if self.nome.isalpha():
return self.nome
else:
return ''.join(ch for ch in self.nome if ch in whitelist)
Or with regex:
import re
# ...
# ...
def limpaNome(self):
return re.sub(r"[^a-zA-Z ]",'',self.nome)
Note that if I were you, I'd do:
class Perfil:
def __init__(self, email, nome, cidade):
self.email = email
self.cidade = cidade
self.nome = limpaNome(nome)

sqlalchemy with dynamic mapping and complex object querying

I have the following situation:
class MyBaseClass(object):
def __init__(self, name):
self.name = name
self.period = None
self.foo = None
def __getitem__(self, item):
return getattr(self, item)
def __setitem__(self, item, value):
return setattr(self, item, value)
If in running time I need to add some additional columns we could do:
my_base_class_table = Table("MyBaseClass", metadata,
Column('name', String, primary_key=True),
Column('period', DateTime),
Column('foo', Float),
)
my_base_class_table = Table("MyBaseClass", metadata, extend_existing=True)
column_list = ["value_one", "other_name", "random_XARS123"]
for col in column_list:
my_base_class_table.append_column(Column(col, Float))
create_all()
mapper(MyBaseClass, my_base_class_table)
Until here we have a fully functional dynamic table mapping with extended columns.
Now, using the sqlalchemy's ORM you could easily instantiate a MyBaseClass and modify it to reflect changes in the database:
base_class = MyBaseClass(name="Something")
base_class.period = "2002-10-01"
And using the dynamic columns with unknown column names:
for col in column_list:
base_class[col] = 10
session.add(base_class)
But actually only if you know the column names:
t_query = session.query(func.strftime('%Y-%m-%d', MyBaseClass.period),
func.sum(MyBaseClass.foo), \
func.sum(MyBaseClass.other_name*MyBaseClass.value_one))
Is possible to repeat the last query (t_query) without knowing the column names? I've already tried different cases with no luck:
func.sum(MyBaseClass[column_list[0]]*MyBaseClass.[column_list[1]])
The only thing that actually work is doing the extended text sql like:
text_query = text("SELECT strftime('%Y-%m-%d', period) as period, sum(foo) as foo, sum({0}*{1}) as bar FROM {2} ".format(column_list[0], column_list[1], "MyBaseClass")
Simple getattr will do the trick:
t_query = session.query(func.strftime('%Y-%m-%d', getattr(MyBaseClass, "period")),
func.sum(getattr(MyBaseClass, "foo")),
func.sum(getattr(MyBaseClass, "other_name") * getattr(MyBaseClass, "value_one"))
)

Categories

Resources